Identify text fields as such to a11y (#12804)
* Identify text fields as such to a11y * focus * make travis happy * review comments
This commit is contained in:
parent
005a8e4c8e
commit
1d4607ff04
@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
import 'box.dart';
|
import 'box.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
|
import 'semantics.dart';
|
||||||
import 'viewport_offset.dart';
|
import 'viewport_offset.dart';
|
||||||
|
|
||||||
const double _kCaretGap = 1.0; // pixels
|
const double _kCaretGap = 1.0; // pixels
|
||||||
@ -105,6 +106,7 @@ class RenderEditable extends RenderBox {
|
|||||||
TextAlign textAlign: TextAlign.start,
|
TextAlign textAlign: TextAlign.start,
|
||||||
Color cursorColor,
|
Color cursorColor,
|
||||||
ValueNotifier<bool> showCursor,
|
ValueNotifier<bool> showCursor,
|
||||||
|
bool hasFocus,
|
||||||
int maxLines: 1,
|
int maxLines: 1,
|
||||||
Color selectionColor,
|
Color selectionColor,
|
||||||
double textScaleFactor: 1.0,
|
double textScaleFactor: 1.0,
|
||||||
@ -125,6 +127,7 @@ class RenderEditable extends RenderBox {
|
|||||||
),
|
),
|
||||||
_cursorColor = cursorColor,
|
_cursorColor = cursorColor,
|
||||||
_showCursor = showCursor ?? new ValueNotifier<bool>(false),
|
_showCursor = showCursor ?? new ValueNotifier<bool>(false),
|
||||||
|
_hasFocus = hasFocus ?? false,
|
||||||
_maxLines = maxLines,
|
_maxLines = maxLines,
|
||||||
_selection = selection,
|
_selection = selection,
|
||||||
_offset = offset {
|
_offset = offset {
|
||||||
@ -227,6 +230,17 @@ class RenderEditable extends RenderBox {
|
|||||||
markNeedsPaint();
|
markNeedsPaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the editable is currently focused.
|
||||||
|
bool get hasFocus => _hasFocus;
|
||||||
|
bool _hasFocus;
|
||||||
|
set hasFocus(bool value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_hasFocus == value)
|
||||||
|
return;
|
||||||
|
_hasFocus = value;
|
||||||
|
markNeedsSemanticsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
/// The maximum number of lines for the text to span, wrapping if necessary.
|
/// The maximum number of lines for the text to span, wrapping if necessary.
|
||||||
///
|
///
|
||||||
/// If this is 1 (the default), the text will not wrap, but will extend
|
/// If this is 1 (the default), the text will not wrap, but will extend
|
||||||
@ -303,6 +317,15 @@ class RenderEditable extends RenderBox {
|
|||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||||
|
super.describeSemanticsConfiguration(config);
|
||||||
|
|
||||||
|
config
|
||||||
|
..isFocused = hasFocus
|
||||||
|
..isTextField = true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void attach(PipelineOwner owner) {
|
void attach(PipelineOwner owner) {
|
||||||
super.attach(owner);
|
super.attach(owner);
|
||||||
|
@ -783,7 +783,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
if (_hasFlag(SemanticsFlags.hasCheckedState))
|
if (_hasFlag(SemanticsFlags.hasCheckedState))
|
||||||
properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked'));
|
properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked'));
|
||||||
properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected'));
|
properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected'));
|
||||||
|
properties.add(new FlagProperty('isFocused', value: _hasFlag(SemanticsFlags.isFocused), ifTrue: 'focused'));
|
||||||
properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button'));
|
properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button'));
|
||||||
|
properties.add(new FlagProperty('isTextField', value: _hasFlag(SemanticsFlags.isTextField), ifTrue: 'textField'));
|
||||||
properties.add(new StringProperty('label', _label, defaultValue: ''));
|
properties.add(new StringProperty('label', _label, defaultValue: ''));
|
||||||
properties.add(new StringProperty('value', _value, defaultValue: ''));
|
properties.add(new StringProperty('value', _value, defaultValue: ''));
|
||||||
properties.add(new StringProperty('increasedValue', _increasedValue, defaultValue: ''));
|
properties.add(new StringProperty('increasedValue', _increasedValue, defaultValue: ''));
|
||||||
@ -1233,11 +1235,21 @@ class SemanticsConfiguration {
|
|||||||
_setFlag(SemanticsFlags.isChecked, value);
|
_setFlag(SemanticsFlags.isChecked, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the owning [RenderObject] currently holds the user's focus.
|
||||||
|
set isFocused(bool value) {
|
||||||
|
_setFlag(SemanticsFlags.isFocused, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the owning [RenderObject] is a button (true) or not (false).
|
/// Whether the owning [RenderObject] is a button (true) or not (false).
|
||||||
set isButton(bool value) {
|
set isButton(bool value) {
|
||||||
_setFlag(SemanticsFlags.isButton, value);
|
_setFlag(SemanticsFlags.isButton, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the owning [RenderObject] is a text field.
|
||||||
|
set isTextField(bool value) {
|
||||||
|
_setFlag(SemanticsFlags.isTextField, value);
|
||||||
|
}
|
||||||
|
|
||||||
// TAGS
|
// TAGS
|
||||||
|
|
||||||
/// The set of tags that this configuration wants to add to all child
|
/// The set of tags that this configuration wants to add to all child
|
||||||
|
@ -638,6 +638,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
style: widget.style,
|
style: widget.style,
|
||||||
cursorColor: widget.cursorColor,
|
cursorColor: widget.cursorColor,
|
||||||
showCursor: _showCursor,
|
showCursor: _showCursor,
|
||||||
|
hasFocus: _hasFocus,
|
||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
selectionColor: widget.selectionColor,
|
selectionColor: widget.selectionColor,
|
||||||
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
|
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
|
||||||
@ -663,6 +664,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
this.style,
|
this.style,
|
||||||
this.cursorColor,
|
this.cursorColor,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
|
this.hasFocus,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.selectionColor,
|
this.selectionColor,
|
||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
@ -681,6 +683,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
final Color cursorColor;
|
final Color cursorColor;
|
||||||
final ValueNotifier<bool> showCursor;
|
final ValueNotifier<bool> showCursor;
|
||||||
|
final bool hasFocus;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final Color selectionColor;
|
final Color selectionColor;
|
||||||
final double textScaleFactor;
|
final double textScaleFactor;
|
||||||
@ -699,6 +702,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
text: _styledTextSpan,
|
text: _styledTextSpan,
|
||||||
cursorColor: cursorColor,
|
cursorColor: cursorColor,
|
||||||
showCursor: showCursor,
|
showCursor: showCursor,
|
||||||
|
hasFocus: hasFocus,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
selectionColor: selectionColor,
|
selectionColor: selectionColor,
|
||||||
textScaleFactor: textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
@ -717,6 +721,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
..text = _styledTextSpan
|
..text = _styledTextSpan
|
||||||
..cursorColor = cursorColor
|
..cursorColor = cursorColor
|
||||||
..showCursor = showCursor
|
..showCursor = showCursor
|
||||||
|
..hasFocus = hasFocus
|
||||||
..maxLines = maxLines
|
..maxLines = maxLines
|
||||||
..selectionColor = selectionColor
|
..selectionColor = selectionColor
|
||||||
..textScaleFactor = textScaleFactor
|
..textScaleFactor = textScaleFactor
|
||||||
|
@ -3,12 +3,14 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' show SemanticsFlags;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../widgets/semantics_tester.dart';
|
||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
|
|
||||||
class MockClipboard {
|
class MockClipboard {
|
||||||
@ -1637,4 +1639,25 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('5 / 10'), findsOneWidget);
|
expect(find.text('5 / 10'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new MaterialApp(
|
||||||
|
home: const Material(
|
||||||
|
child: const DefaultTextStyle(
|
||||||
|
style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
|
||||||
|
child: const Center(
|
||||||
|
child: const TextField(
|
||||||
|
maxLength: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(semantics, includesNodeWith(flags: <SemanticsFlags>[SemanticsFlags.isTextField]));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ void main() {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
|
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
|
||||||
'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isButton: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n',
|
'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isFocused: false, isButton: false, isTextField: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n'
|
||||||
);
|
);
|
||||||
|
|
||||||
final SemanticsConfiguration config = new SemanticsConfiguration()
|
final SemanticsConfiguration config = new SemanticsConfiguration()
|
||||||
|
@ -2,11 +2,16 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui' show SemanticsFlags;
|
||||||
|
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'semantics_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final TextEditingController controller = new TextEditingController();
|
final TextEditingController controller = new TextEditingController();
|
||||||
final FocusNode focusNode = new FocusNode();
|
final FocusNode focusNode = new FocusNode();
|
||||||
@ -250,4 +255,33 @@ void main() {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: new FocusScope(
|
||||||
|
node: focusScopeNode,
|
||||||
|
autofocus: true,
|
||||||
|
child: new EditableText(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(semantics, includesNodeWith(flags: <SemanticsFlags>[SemanticsFlags.isTextField]));
|
||||||
|
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.idle();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(semantics, includesNodeWith(flags: <SemanticsFlags>[SemanticsFlags.isTextField, SemanticsFlags.isFocused]));
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui' show SemanticsFlags;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -314,11 +316,13 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
this.label,
|
this.label,
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.actions,
|
this.actions,
|
||||||
}) : assert(label != null || actions != null);
|
this.flags,
|
||||||
|
}) : assert(label != null || actions != null || flags != null);
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
final List<SemanticsAction> actions;
|
final List<SemanticsAction> actions;
|
||||||
|
final List<SemanticsFlags> flags;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
|
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
|
||||||
@ -348,6 +352,12 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
if (expectedActions != actualActions)
|
if (expectedActions != actualActions)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (flags != null) {
|
||||||
|
final int expectedFlags = flags.fold(0, (int value, SemanticsFlags flag) => value | flag.index);
|
||||||
|
final int actualFlags = node.getSemanticsData().flags;
|
||||||
|
if (expectedFlags != actualFlags)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,22 +372,16 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get _configAsString {
|
String get _configAsString {
|
||||||
String string = '';
|
final List<String> strings = <String>[];
|
||||||
if (label != null) {
|
if (label != null)
|
||||||
string += 'label "$label"';
|
strings.add('label "$label"');
|
||||||
if (textDirection != null)
|
if (textDirection != null)
|
||||||
string += ' (${describeEnum(textDirection)})';
|
strings.add(' (${describeEnum(textDirection)})');
|
||||||
if (actions != null)
|
if (actions != null)
|
||||||
string += ' and ';
|
strings.add('actions "${actions.join(', ')}"');
|
||||||
} else if (textDirection != null) {
|
if (flags != null)
|
||||||
string += 'direction ${describeEnum(textDirection)}';
|
strings.add('flags "${flags.join(', ')}"');
|
||||||
if (actions != null)
|
return strings.join(', ');
|
||||||
string += ' and ';
|
|
||||||
}
|
|
||||||
if (actions != null) {
|
|
||||||
string += 'actions "${actions.join(', ')}"';
|
|
||||||
}
|
|
||||||
return string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,10 +389,16 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
/// `textDirection`, and `actions`.
|
/// `textDirection`, and `actions`.
|
||||||
///
|
///
|
||||||
/// If null is provided for an argument, it will match against any value.
|
/// If null is provided for an argument, it will match against any value.
|
||||||
Matcher includesNodeWith({ String label, TextDirection textDirection, List<SemanticsAction> actions }) {
|
Matcher includesNodeWith({
|
||||||
|
String label,
|
||||||
|
TextDirection textDirection,
|
||||||
|
List<SemanticsAction> actions,
|
||||||
|
List<SemanticsFlags> flags,
|
||||||
|
}) {
|
||||||
return new _IncludesNodeWith(
|
return new _IncludesNodeWith(
|
||||||
label: label,
|
label: label,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
|
flags: flags,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user