Add a11y support for selected text (#14254)
Framework side for https://github.com/flutter/engine/pull/4584 & https://github.com/flutter/engine/pull/4587.
Also rolls engine to 4c82c566edf394a5cfc237a266aea5bd37a6c172.
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 1d937e4..61f9bc0 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-93296fb4ea653a3064643266d89dddd97d062f4a
+4c82c566edf394a5cfc237a266aea5bd37a6c172
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 3a4a36c..63b95b8 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -357,6 +357,7 @@
..isTextField = true;
if (_selection?.isValid == true) {
+ config.textSelection = _selection;
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index 11ad6f0..407d1ae 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -90,6 +90,7 @@
@required this.hint,
@required this.textDirection,
@required this.rect,
+ @required this.textSelection,
this.tags,
this.transform,
}) : assert(flags != null),
@@ -143,6 +144,10 @@
/// [increasedValue], and [decreasedValue].
final TextDirection textDirection;
+ /// The currently selected text (or the position of the cursor) within [value]
+ /// if this node represents a text field.
+ final TextSelection textSelection;
+
/// The bounding box for this node in its coordinate system.
final Rect rect;
@@ -189,6 +194,8 @@
properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
properties.add(new StringProperty('hint', hint, defaultValue: ''));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
+ if (textSelection?.isValid == true)
+ properties.add(new MessageProperty('text selection', '[${textSelection.start}, ${textSelection.end}]'));
}
@override
@@ -206,11 +213,12 @@
&& typedOther.textDirection == textDirection
&& typedOther.rect == rect
&& setEquals(typedOther.tags, tags)
+ && typedOther.textSelection == textSelection
&& typedOther.transform == transform;
}
@override
- int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, rect, tags, transform);
+ int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, rect, tags, textSelection, transform);
}
class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
@@ -840,6 +848,7 @@
_increasedValue != config.increasedValue ||
_flags != config._flags ||
_textDirection != config.textDirection ||
+ _textSelection != config._textSelection ||
_actionsAsBits != config._actionsAsBits ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
@@ -906,6 +915,11 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection = _kEmptyConfig.textDirection;
+ /// The currently selected text (or the position of the cursor) within [value]
+ /// if this node represents a text field.
+ TextSelection get textSelection => _textSelection;
+ TextSelection _textSelection;
+
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
@@ -936,6 +950,7 @@
_textDirection = config.textDirection;
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
_actionsAsBits = config._actionsAsBits;
+ _textSelection = config._textSelection;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
@@ -965,6 +980,7 @@
String decreasedValue = _decreasedValue;
TextDirection textDirection = _textDirection;
Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
+ TextSelection textSelection = _textSelection;
if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
@@ -972,6 +988,7 @@
flags |= node._flags;
actions |= node._actionsAsBits;
textDirection ??= node._textDirection;
+ textSelection ??= node._textSelection;
if (value == '' || value == null)
value = node._value;
if (increasedValue == '' || increasedValue == null)
@@ -1010,6 +1027,7 @@
rect: rect,
transform: transform,
tags: mergedTags,
+ textSelection: textSelection,
);
}
@@ -1043,6 +1061,8 @@
increasedValue: data.increasedValue,
hint: data.hint,
textDirection: data.textDirection,
+ textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
+ textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
transform: data.transform?.storage ?? _kIdentityTransform,
children: children,
);
@@ -1110,6 +1130,8 @@
properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
properties.add(new StringProperty('hint', _hint, defaultValue: ''));
properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
+ if (_textSelection?.isValid == true)
+ properties.add(new MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
}
/// Returns a string representation of this node and its descendants.
@@ -1819,6 +1841,16 @@
_setFlag(SemanticsFlag.isTextField, value);
}
+ /// The currently selected text (or the position of the cursor) within [value]
+ /// if this node represents a text field.
+ TextSelection get textSelection => _textSelection;
+ TextSelection _textSelection;
+ set textSelection(TextSelection value) {
+ assert(value != null);
+ _textSelection = value;
+ _hasBeenAnnotated = true;
+ }
+
// TAGS
/// The set of tags that this configuration wants to add to all child
@@ -1901,6 +1933,7 @@
_actions.addAll(other._actions);
_actionsAsBits |= other._actionsAsBits;
_flags |= other._flags;
+ _textSelection ??= other._textSelection;
textDirection ??= other.textDirection;
_label = _concatStrings(
@@ -1941,6 +1974,7 @@
.._hint = _hint
.._flags = _flags
.._tagsForChildren = _tagsForChildren
+ .._textSelection = _textSelection
.._actionsAsBits = _actionsAsBits
.._actions.addAll(_actions);
}
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 2cd1afc..6123185 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -1808,6 +1808,7 @@
id: 2,
textDirection: TextDirection.ltr,
value: 'Guten Tag',
+ textSelection: const TextSelection.collapsed(offset: 9),
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
@@ -1828,6 +1829,7 @@
new TestSemantics.rootChild(
id: 2,
textDirection: TextDirection.ltr,
+ textSelection: const TextSelection.collapsed(offset: 4),
value: 'Guten Tag',
actions: <SemanticsAction>[
SemanticsAction.tap,
@@ -1851,6 +1853,7 @@
new TestSemantics.rootChild(
id: 2,
textDirection: TextDirection.ltr,
+ textSelection: const TextSelection.collapsed(offset: 0),
value: 'Schönen Feierabend',
actions: <SemanticsAction>[
SemanticsAction.tap,
@@ -1867,4 +1870,84 @@
semantics.dispose();
});
+ testWidgets('TextField semantics for selections', (WidgetTester tester) async {
+ final SemanticsTester semantics = new SemanticsTester(tester);
+ final TextEditingController controller = new TextEditingController()
+ ..text = 'Hello';
+ final Key key = new UniqueKey();
+
+ await tester.pumpWidget(
+ overlay(
+ child: new TextField(
+ key: key,
+ controller: controller,
+ )
+ ),
+ );
+
+ expect(semantics, hasSemantics(new TestSemantics.root(
+ children: <TestSemantics>[
+ new TestSemantics.rootChild(
+ id: 2,
+ value: 'Hello',
+ textDirection: TextDirection.ltr,
+ actions: <SemanticsAction>[
+ SemanticsAction.tap,
+ ],
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ ],
+ ),
+ ],
+ ), ignoreTransform: true, ignoreRect: true));
+
+ // Focus the text field
+ await tester.tap(find.byKey(key));
+ await tester.pump();
+
+ expect(semantics, hasSemantics(new TestSemantics.root(
+ children: <TestSemantics>[
+ new TestSemantics.rootChild(
+ id: 2,
+ value: 'Hello',
+ textSelection: const TextSelection.collapsed(offset: 5),
+ textDirection: TextDirection.ltr,
+ actions: <SemanticsAction>[
+ SemanticsAction.tap,
+ SemanticsAction.moveCursorBackwardByCharacter,
+ ],
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.isFocused,
+ ],
+ ),
+ ],
+ ), ignoreTransform: true, ignoreRect: true));
+
+ controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3);
+ await tester.pump();
+
+ expect(semantics, hasSemantics(new TestSemantics.root(
+ children: <TestSemantics>[
+ new TestSemantics.rootChild(
+ id: 2,
+ value: 'Hello',
+ textSelection: const TextSelection(baseOffset: 5, extentOffset: 3),
+ textDirection: TextDirection.ltr,
+ actions: <SemanticsAction>[
+ SemanticsAction.tap,
+ SemanticsAction.moveCursorBackwardByCharacter,
+ SemanticsAction.moveCursorForwardByCharacter,
+ ],
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.isFocused,
+ ],
+ ),
+ ],
+ ), ignoreTransform: true, ignoreRect: true));
+
+ semantics.dispose();
+ });
+
}
diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart
index 58791af..d99bbfd 100644
--- a/packages/flutter/test/widgets/semantics_tester.dart
+++ b/packages/flutter/test/widgets/semantics_tester.dart
@@ -44,6 +44,7 @@
this.textDirection,
this.rect,
this.transform,
+ this.textSelection,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>),
@@ -68,6 +69,7 @@
this.hint: '',
this.textDirection,
this.transform,
+ this.textSelection,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : id = 0,
@@ -103,6 +105,7 @@
this.textDirection,
this.rect,
Matrix4 transform,
+ this.textSelection,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>),
@@ -195,6 +198,8 @@
/// parent).
final Matrix4 transform;
+ final TextSelection textSelection;
+
static Matrix4 _applyRootChildScale(Matrix4 transform) {
final Matrix4 result = new Matrix4.diagonal3Values(3.0, 3.0, 1.0);
if (transform != null)
@@ -251,6 +256,9 @@
return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
if (!ignoreTransform && transform != nodeData.transform)
return fail('expected node id $id to have transform $transform but found transform:\n${nodeData.transform}.');
+ if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
+ return fail('expected node id $id to have textDirection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
+ }
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (children.length != childrenCount)
return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $childrenCount.');
@@ -293,6 +301,8 @@
buf.writeln('$indent hint: \'$hint\',');
if (textDirection != null)
buf.writeln('$indent textDirection: $textDirection,');
+ if (textSelection?.isValid == true)
+ buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
if (rect != null)
buf.writeln('$indent rect: $rect,');
if (transform != null)