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;
|
||||
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user