Unverified Commit 34ff00a7 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

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.
parent 97b9579e
93296fb4ea653a3064643266d89dddd97d062f4a
4c82c566edf394a5cfc237a266aea5bd37a6c172
......@@ -357,6 +357,7 @@ class RenderEditable extends RenderBox {
..isTextField = true;
if (_selection?.isValid == true) {
config.textSelection = _selection;
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
......
......@@ -90,6 +90,7 @@ class SemanticsData extends Diagnosticable {
@required this.hint,
@required this.textDirection,
@required this.rect,
@required this.textSelection,
this.tags,
this.transform,
}) : assert(flags != null),
......@@ -143,6 +144,10 @@ class SemanticsData extends Diagnosticable {
/// [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 @@ class SemanticsData extends Diagnosticable {
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 @@ class SemanticsData extends Diagnosticable {
&& 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 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_increasedValue != config.increasedValue ||
_flags != config._flags ||
_textDirection != config.textDirection ||
_textSelection != config._textSelection ||
_actionsAsBits != config._actionsAsBits ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
......@@ -906,6 +915,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
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 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_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 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
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 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
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 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
rect: rect,
transform: transform,
tags: mergedTags,
textSelection: textSelection,
);
}
......@@ -1043,6 +1061,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
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 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
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 @@ class SemanticsConfiguration {
_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 @@ class SemanticsConfiguration {
_actions.addAll(other._actions);
_actionsAsBits |= other._actionsAsBits;
_flags |= other._flags;
_textSelection ??= other._textSelection;
textDirection ??= other.textDirection;
_label = _concatStrings(
......@@ -1941,6 +1974,7 @@ class SemanticsConfiguration {
.._hint = _hint
.._flags = _flags
.._tagsForChildren = _tagsForChildren
.._textSelection = _textSelection
.._actionsAsBits = _actionsAsBits
.._actions.addAll(_actions);
}
......
......@@ -1808,6 +1808,7 @@ void main() {
id: 2,
textDirection: TextDirection.ltr,
value: 'Guten Tag',
textSelection: const TextSelection.collapsed(offset: 9),
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -1828,6 +1829,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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();
});
}
......@@ -44,6 +44,7 @@ class TestSemantics {
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 @@ class TestSemantics {
this.hint: '',
this.textDirection,
this.transform,
this.textSelection,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : id = 0,
......@@ -103,6 +105,7 @@ class TestSemantics {
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 @@ class TestSemantics {
/// 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 @@ class TestSemantics {
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 @@ class TestSemantics {
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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment