mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
97b9579e55
commit
34ff00a752
@ -1 +1 @@
|
|||||||
93296fb4ea653a3064643266d89dddd97d062f4a
|
4c82c566edf394a5cfc237a266aea5bd37a6c172
|
||||||
|
@ -357,6 +357,7 @@ class RenderEditable extends RenderBox {
|
|||||||
..isTextField = true;
|
..isTextField = true;
|
||||||
|
|
||||||
if (_selection?.isValid == true) {
|
if (_selection?.isValid == true) {
|
||||||
|
config.textSelection = _selection;
|
||||||
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
|
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
|
||||||
config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
|
config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
|
||||||
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
|
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
|
||||||
|
@ -90,6 +90,7 @@ class SemanticsData extends Diagnosticable {
|
|||||||
@required this.hint,
|
@required this.hint,
|
||||||
@required this.textDirection,
|
@required this.textDirection,
|
||||||
@required this.rect,
|
@required this.rect,
|
||||||
|
@required this.textSelection,
|
||||||
this.tags,
|
this.tags,
|
||||||
this.transform,
|
this.transform,
|
||||||
}) : assert(flags != null),
|
}) : assert(flags != null),
|
||||||
@ -143,6 +144,10 @@ class SemanticsData extends Diagnosticable {
|
|||||||
/// [increasedValue], and [decreasedValue].
|
/// [increasedValue], and [decreasedValue].
|
||||||
final TextDirection textDirection;
|
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.
|
/// The bounding box for this node in its coordinate system.
|
||||||
final Rect rect;
|
final Rect rect;
|
||||||
|
|
||||||
@ -189,6 +194,8 @@ class SemanticsData extends Diagnosticable {
|
|||||||
properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
|
properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
|
||||||
properties.add(new StringProperty('hint', hint, defaultValue: ''));
|
properties.add(new StringProperty('hint', hint, defaultValue: ''));
|
||||||
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
||||||
|
if (textSelection?.isValid == true)
|
||||||
|
properties.add(new MessageProperty('text selection', '[${textSelection.start}, ${textSelection.end}]'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -206,11 +213,12 @@ class SemanticsData extends Diagnosticable {
|
|||||||
&& typedOther.textDirection == textDirection
|
&& typedOther.textDirection == textDirection
|
||||||
&& typedOther.rect == rect
|
&& typedOther.rect == rect
|
||||||
&& setEquals(typedOther.tags, tags)
|
&& setEquals(typedOther.tags, tags)
|
||||||
|
&& typedOther.textSelection == textSelection
|
||||||
&& typedOther.transform == transform;
|
&& typedOther.transform == transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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> {
|
class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
|
||||||
@ -840,6 +848,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
_increasedValue != config.increasedValue ||
|
_increasedValue != config.increasedValue ||
|
||||||
_flags != config._flags ||
|
_flags != config._flags ||
|
||||||
_textDirection != config.textDirection ||
|
_textDirection != config.textDirection ||
|
||||||
|
_textSelection != config._textSelection ||
|
||||||
_actionsAsBits != config._actionsAsBits ||
|
_actionsAsBits != config._actionsAsBits ||
|
||||||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
|
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
|
||||||
}
|
}
|
||||||
@ -906,6 +915,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
TextDirection get textDirection => _textDirection;
|
TextDirection get textDirection => _textDirection;
|
||||||
TextDirection _textDirection = _kEmptyConfig.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);
|
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
|
||||||
|
|
||||||
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
|
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
|
||||||
@ -936,6 +950,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
_textDirection = config.textDirection;
|
_textDirection = config.textDirection;
|
||||||
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
|
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
|
||||||
_actionsAsBits = config._actionsAsBits;
|
_actionsAsBits = config._actionsAsBits;
|
||||||
|
_textSelection = config._textSelection;
|
||||||
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
|
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
|
||||||
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
|
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
|
||||||
|
|
||||||
@ -965,6 +980,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
String decreasedValue = _decreasedValue;
|
String decreasedValue = _decreasedValue;
|
||||||
TextDirection textDirection = _textDirection;
|
TextDirection textDirection = _textDirection;
|
||||||
Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
|
Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
|
||||||
|
TextSelection textSelection = _textSelection;
|
||||||
|
|
||||||
if (mergeAllDescendantsIntoThisNode) {
|
if (mergeAllDescendantsIntoThisNode) {
|
||||||
_visitDescendants((SemanticsNode node) {
|
_visitDescendants((SemanticsNode node) {
|
||||||
@ -972,6 +988,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
flags |= node._flags;
|
flags |= node._flags;
|
||||||
actions |= node._actionsAsBits;
|
actions |= node._actionsAsBits;
|
||||||
textDirection ??= node._textDirection;
|
textDirection ??= node._textDirection;
|
||||||
|
textSelection ??= node._textSelection;
|
||||||
if (value == '' || value == null)
|
if (value == '' || value == null)
|
||||||
value = node._value;
|
value = node._value;
|
||||||
if (increasedValue == '' || increasedValue == null)
|
if (increasedValue == '' || increasedValue == null)
|
||||||
@ -1010,6 +1027,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
rect: rect,
|
rect: rect,
|
||||||
transform: transform,
|
transform: transform,
|
||||||
tags: mergedTags,
|
tags: mergedTags,
|
||||||
|
textSelection: textSelection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1043,6 +1061,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
increasedValue: data.increasedValue,
|
increasedValue: data.increasedValue,
|
||||||
hint: data.hint,
|
hint: data.hint,
|
||||||
textDirection: data.textDirection,
|
textDirection: data.textDirection,
|
||||||
|
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
|
||||||
|
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
|
||||||
transform: data.transform?.storage ?? _kIdentityTransform,
|
transform: data.transform?.storage ?? _kIdentityTransform,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
@ -1110,6 +1130,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
|
properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
|
||||||
properties.add(new StringProperty('hint', _hint, defaultValue: ''));
|
properties.add(new StringProperty('hint', _hint, defaultValue: ''));
|
||||||
properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
|
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.
|
/// Returns a string representation of this node and its descendants.
|
||||||
@ -1819,6 +1841,16 @@ class SemanticsConfiguration {
|
|||||||
_setFlag(SemanticsFlag.isTextField, value);
|
_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
|
// 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
|
||||||
@ -1901,6 +1933,7 @@ class SemanticsConfiguration {
|
|||||||
_actions.addAll(other._actions);
|
_actions.addAll(other._actions);
|
||||||
_actionsAsBits |= other._actionsAsBits;
|
_actionsAsBits |= other._actionsAsBits;
|
||||||
_flags |= other._flags;
|
_flags |= other._flags;
|
||||||
|
_textSelection ??= other._textSelection;
|
||||||
|
|
||||||
textDirection ??= other.textDirection;
|
textDirection ??= other.textDirection;
|
||||||
_label = _concatStrings(
|
_label = _concatStrings(
|
||||||
@ -1941,6 +1974,7 @@ class SemanticsConfiguration {
|
|||||||
.._hint = _hint
|
.._hint = _hint
|
||||||
.._flags = _flags
|
.._flags = _flags
|
||||||
.._tagsForChildren = _tagsForChildren
|
.._tagsForChildren = _tagsForChildren
|
||||||
|
.._textSelection = _textSelection
|
||||||
.._actionsAsBits = _actionsAsBits
|
.._actionsAsBits = _actionsAsBits
|
||||||
.._actions.addAll(_actions);
|
.._actions.addAll(_actions);
|
||||||
}
|
}
|
||||||
|
@ -1808,6 +1808,7 @@ void main() {
|
|||||||
id: 2,
|
id: 2,
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
value: 'Guten Tag',
|
value: 'Guten Tag',
|
||||||
|
textSelection: const TextSelection.collapsed(offset: 9),
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.tap,
|
SemanticsAction.tap,
|
||||||
SemanticsAction.moveCursorBackwardByCharacter,
|
SemanticsAction.moveCursorBackwardByCharacter,
|
||||||
@ -1828,6 +1829,7 @@ void main() {
|
|||||||
new TestSemantics.rootChild(
|
new TestSemantics.rootChild(
|
||||||
id: 2,
|
id: 2,
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
textSelection: const TextSelection.collapsed(offset: 4),
|
||||||
value: 'Guten Tag',
|
value: 'Guten Tag',
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.tap,
|
SemanticsAction.tap,
|
||||||
@ -1851,6 +1853,7 @@ void main() {
|
|||||||
new TestSemantics.rootChild(
|
new TestSemantics.rootChild(
|
||||||
id: 2,
|
id: 2,
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
textSelection: const TextSelection.collapsed(offset: 0),
|
||||||
value: 'Schönen Feierabend',
|
value: 'Schönen Feierabend',
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.tap,
|
SemanticsAction.tap,
|
||||||
@ -1867,4 +1870,84 @@ void main() {
|
|||||||
semantics.dispose();
|
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.textDirection,
|
||||||
this.rect,
|
this.rect,
|
||||||
this.transform,
|
this.transform,
|
||||||
|
this.textSelection,
|
||||||
this.children: const <TestSemantics>[],
|
this.children: const <TestSemantics>[],
|
||||||
Iterable<SemanticsTag> tags,
|
Iterable<SemanticsTag> tags,
|
||||||
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
||||||
@ -68,6 +69,7 @@ class TestSemantics {
|
|||||||
this.hint: '',
|
this.hint: '',
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.transform,
|
this.transform,
|
||||||
|
this.textSelection,
|
||||||
this.children: const <TestSemantics>[],
|
this.children: const <TestSemantics>[],
|
||||||
Iterable<SemanticsTag> tags,
|
Iterable<SemanticsTag> tags,
|
||||||
}) : id = 0,
|
}) : id = 0,
|
||||||
@ -103,6 +105,7 @@ class TestSemantics {
|
|||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.rect,
|
this.rect,
|
||||||
Matrix4 transform,
|
Matrix4 transform,
|
||||||
|
this.textSelection,
|
||||||
this.children: const <TestSemantics>[],
|
this.children: const <TestSemantics>[],
|
||||||
Iterable<SemanticsTag> tags,
|
Iterable<SemanticsTag> tags,
|
||||||
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
||||||
@ -195,6 +198,8 @@ class TestSemantics {
|
|||||||
/// parent).
|
/// parent).
|
||||||
final Matrix4 transform;
|
final Matrix4 transform;
|
||||||
|
|
||||||
|
final TextSelection textSelection;
|
||||||
|
|
||||||
static Matrix4 _applyRootChildScale(Matrix4 transform) {
|
static Matrix4 _applyRootChildScale(Matrix4 transform) {
|
||||||
final Matrix4 result = new Matrix4.diagonal3Values(3.0, 3.0, 1.0);
|
final Matrix4 result = new Matrix4.diagonal3Values(3.0, 3.0, 1.0);
|
||||||
if (transform != null)
|
if (transform != null)
|
||||||
@ -251,6 +256,9 @@ class TestSemantics {
|
|||||||
return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
|
return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
|
||||||
if (!ignoreTransform && transform != nodeData.transform)
|
if (!ignoreTransform && transform != nodeData.transform)
|
||||||
return fail('expected node id $id to have transform $transform but found transform:\n${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;
|
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
|
||||||
if (children.length != childrenCount)
|
if (children.length != childrenCount)
|
||||||
return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $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\',');
|
buf.writeln('$indent hint: \'$hint\',');
|
||||||
if (textDirection != null)
|
if (textDirection != null)
|
||||||
buf.writeln('$indent textDirection: $textDirection,');
|
buf.writeln('$indent textDirection: $textDirection,');
|
||||||
|
if (textSelection?.isValid == true)
|
||||||
|
buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
|
||||||
if (rect != null)
|
if (rect != null)
|
||||||
buf.writeln('$indent rect: $rect,');
|
buf.writeln('$indent rect: $rect,');
|
||||||
if (transform != null)
|
if (transform != null)
|
||||||
|
Loading…
Reference in New Issue
Block a user