Skip to content

Commit 671c110

Browse files
authored
Change text selection (or cursor position) via a11y (flutter#14275)
Roll engine to 7c34dfa
1 parent c23509e commit 671c110

File tree

9 files changed

+199
-9
lines changed

9 files changed

+199
-9
lines changed

bin/internal/engine.version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4c82c566edf394a5cfc237a266aea5bd37a6c172
1+
7c34dfafc9acece1a9438f206bfbb0a9bedba3bf

packages/flutter/lib/src/rendering/custom_paint.dart

+3
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,9 @@ class RenderCustomPaint extends RenderProxyBox {
865865
if (properties.onMoveCursorBackwardByCharacter != null) {
866866
config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
867867
}
868+
if (properties.onSetSelection != null) {
869+
config.onSetSelection = properties.onSetSelection;
870+
}
868871

869872
newChild.updateWith(
870873
config: config,

packages/flutter/lib/src/rendering/editable.dart

+7
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ class RenderEditable extends RenderBox {
356356
..isFocused = hasFocus
357357
..isTextField = true;
358358

359+
if (hasFocus)
360+
config.onSetSelection = _handleSetSelection;
361+
359362
if (_selection?.isValid == true) {
360363
config.textSelection = _selection;
361364
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
@@ -365,6 +368,10 @@ class RenderEditable extends RenderBox {
365368
}
366369
}
367370

371+
void _handleSetSelection(TextSelection selection) {
372+
onSelectionChanged(selection, this, SelectionChangedCause.keyboard);
373+
}
374+
368375
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
369376
final int extentOffset = _textPainter.getOffsetAfter(_selection.extentOffset);
370377
if (extentOffset == null)

packages/flutter/lib/src/rendering/proxy_box.dart

+27
Original file line numberDiff line numberDiff line change
@@ -3017,6 +3017,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
30173017
VoidCallback onDecrease,
30183018
MoveCursorHandler onMoveCursorForwardByCharacter,
30193019
MoveCursorHandler onMoveCursorBackwardByCharacter,
3020+
SetSelectionHandler onSetSelection,
30203021
}) : assert(container != null),
30213022
_container = container,
30223023
_explicitChildNodes = explicitChildNodes,
@@ -3040,6 +3041,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
30403041
_onDecrease = onDecrease,
30413042
_onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter,
30423043
_onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter,
3044+
_onSetSelection = onSetSelection,
30433045
super(child);
30443046

30453047
/// If 'container' is true, this [RenderObject] will introduce a new
@@ -3399,6 +3401,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
33993401
markNeedsSemanticsUpdate();
34003402
}
34013403

3404+
/// The handler for [SemanticsAction.setSelection].
3405+
///
3406+
/// This handler is invoked when the user either wants to change the currently
3407+
/// selected text in a text field or change the position of the cursor.
3408+
///
3409+
/// TalkBack users can trigger this handler by selecting "Move cursor to
3410+
/// beginning/end" or "Select all" from the local context menu.
3411+
SetSelectionHandler get onSetSelection => _onSetSelection;
3412+
SetSelectionHandler _onSetSelection;
3413+
set onSetSelection(SetSelectionHandler handler) {
3414+
if (_onSetSelection == handler)
3415+
return;
3416+
final bool hadValue = _onSetSelection != null;
3417+
_onSetSelection = handler;
3418+
if ((handler != null) != hadValue)
3419+
markNeedsSemanticsUpdate();
3420+
}
3421+
34023422
@override
34033423
void describeSemanticsConfiguration(SemanticsConfiguration config) {
34043424
super.describeSemanticsConfiguration(config);
@@ -3448,6 +3468,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
34483468
config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
34493469
if (onMoveCursorBackwardByCharacter != null)
34503470
config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
3471+
if (onSetSelection != null)
3472+
config.onSetSelection = _performSetSelection;
34513473
}
34523474

34533475
void _performTap() {
@@ -3499,6 +3521,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
34993521
if (onMoveCursorBackwardByCharacter != null)
35003522
onMoveCursorBackwardByCharacter(extendSelection);
35013523
}
3524+
3525+
void _performSetSelection(TextSelection selection) {
3526+
if (onSetSelection != null)
3527+
onSetSelection(selection);
3528+
}
35023529
}
35033530

35043531
/// Causes the semantics of all earlier render objects below the same semantic

packages/flutter/lib/src/semantics/semantics.dart

+36
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node);
3131
/// current selection or (if nothing is currently selected) start a selection.
3232
typedef void MoveCursorHandler(bool extendSelection);
3333

34+
/// Signature for the [SemanticsAction.setSelection] handlers to change the
35+
/// text selection (or re-position the cursor) to `selection`.
36+
typedef void SetSelectionHandler(TextSelection selection);
37+
3438
typedef void _SemanticsActionHandler(dynamic args);
3539

3640
/// A tag for a [SemanticsNode].
@@ -275,6 +279,7 @@ class SemanticsProperties extends DiagnosticableTree {
275279
this.onDecrease,
276280
this.onMoveCursorForwardByCharacter,
277281
this.onMoveCursorBackwardByCharacter,
282+
this.onSetSelection,
278283
});
279284

280285
/// If non-null, indicates that this subtree represents something that can be
@@ -485,6 +490,15 @@ class SemanticsProperties extends DiagnosticableTree {
485490
/// input focus is in a text field.
486491
final MoveCursorHandler onMoveCursorBackwardByCharacter;
487492

493+
/// The handler for [SemanticsAction.setSelection].
494+
///
495+
/// This handler is invoked when the user either wants to change the currently
496+
/// selected text in a text field or change the position of the cursor.
497+
///
498+
/// TalkBack users can trigger this handler by selecting "Move cursor to
499+
/// beginning/end" or "Select all" from the local context menu.
500+
final SetSelectionHandler onSetSelection;
501+
488502
@override
489503
void debugFillProperties(DiagnosticPropertiesBuilder description) {
490504
super.debugFillProperties(description);
@@ -1658,6 +1672,28 @@ class SemanticsConfiguration {
16581672
_onMoveCursorBackwardByCharacter = value;
16591673
}
16601674

1675+
/// The handler for [SemanticsAction.setSelection].
1676+
///
1677+
/// This handler is invoked when the user either wants to change the currently
1678+
/// selected text in a text field or change the position of the cursor.
1679+
///
1680+
/// TalkBack users can trigger this handler by selecting "Move cursor to
1681+
/// beginning/end" or "Select all" from the local context menu.
1682+
SetSelectionHandler get onSetSelection => _onSetSelection;
1683+
SetSelectionHandler _onSetSelection;
1684+
set onSetSelection(SetSelectionHandler value) {
1685+
assert(value != null);
1686+
_addAction(SemanticsAction.setSelection, (dynamic args) {
1687+
final Map<String, int> selection = args;
1688+
assert(selection != null && selection['base'] != null && selection['extent'] != null);
1689+
value(new TextSelection(
1690+
baseOffset: selection['base'],
1691+
extentOffset: selection['extent'],
1692+
));
1693+
});
1694+
_onSetSelection = value;
1695+
}
1696+
16611697
/// Returns the action handler registered for [action] or null if none was
16621698
/// registered.
16631699
///

packages/flutter/lib/src/widgets/basic.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -4854,6 +4854,7 @@ class Semantics extends SingleChildRenderObjectWidget {
48544854
VoidCallback onDecrease,
48554855
MoveCursorHandler onMoveCursorForwardByCharacter,
48564856
MoveCursorHandler onMoveCursorBackwardByCharacter,
4857+
SetSelectionHandler onSetSelection,
48574858
}) : this.fromProperties(
48584859
key: key,
48594860
child: child,
@@ -4880,6 +4881,7 @@ class Semantics extends SingleChildRenderObjectWidget {
48804881
onDecrease: onDecrease,
48814882
onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter,
48824883
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
4884+
onSetSelection: onSetSelection,
48834885
),
48844886
);
48854887

@@ -4948,6 +4950,7 @@ class Semantics extends SingleChildRenderObjectWidget {
49484950
onDecrease: properties.onDecrease,
49494951
onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter,
49504952
onMoveCursorBackwardByCharacter: properties.onMoveCursorBackwardByCharacter,
4953+
onSetSelection: properties.onSetSelection,
49514954
);
49524955
}
49534956

@@ -4986,7 +4989,8 @@ class Semantics extends SingleChildRenderObjectWidget {
49864989
..onIncrease = properties.onIncrease
49874990
..onDecrease = properties.onDecrease
49884991
..onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter
4989-
..onMoveCursorBackwardByCharacter = properties.onMoveCursorForwardByCharacter;
4992+
..onMoveCursorBackwardByCharacter = properties.onMoveCursorForwardByCharacter
4993+
..onSetSelection = properties.onSetSelection;
49904994
}
49914995

49924996
@override

packages/flutter/test/material/text_field_test.dart

+100-5
Original file line numberDiff line numberDiff line change
@@ -1761,7 +1761,7 @@ void main() {
17611761
child: new TextField(
17621762
key: key,
17631763
controller: controller,
1764-
)
1764+
),
17651765
),
17661766
);
17671767

@@ -1812,6 +1812,7 @@ void main() {
18121812
actions: <SemanticsAction>[
18131813
SemanticsAction.tap,
18141814
SemanticsAction.moveCursorBackwardByCharacter,
1815+
SemanticsAction.setSelection,
18151816
],
18161817
flags: <SemanticsFlag>[
18171818
SemanticsFlag.isTextField,
@@ -1835,6 +1836,7 @@ void main() {
18351836
SemanticsAction.tap,
18361837
SemanticsAction.moveCursorBackwardByCharacter,
18371838
SemanticsAction.moveCursorForwardByCharacter,
1839+
SemanticsAction.setSelection,
18381840
],
18391841
flags: <SemanticsFlag>[
18401842
SemanticsFlag.isTextField,
@@ -1858,6 +1860,7 @@ void main() {
18581860
actions: <SemanticsAction>[
18591861
SemanticsAction.tap,
18601862
SemanticsAction.moveCursorForwardByCharacter,
1863+
SemanticsAction.setSelection,
18611864
],
18621865
flags: <SemanticsFlag>[
18631866
SemanticsFlag.isTextField,
@@ -1878,10 +1881,10 @@ void main() {
18781881

18791882
await tester.pumpWidget(
18801883
overlay(
1881-
child: new TextField(
1882-
key: key,
1883-
controller: controller,
1884-
)
1884+
child: new TextField(
1885+
key: key,
1886+
controller: controller,
1887+
),
18851888
),
18861889
);
18871890

@@ -1915,6 +1918,7 @@ void main() {
19151918
actions: <SemanticsAction>[
19161919
SemanticsAction.tap,
19171920
SemanticsAction.moveCursorBackwardByCharacter,
1921+
SemanticsAction.setSelection,
19181922
],
19191923
flags: <SemanticsFlag>[
19201924
SemanticsFlag.isTextField,
@@ -1938,6 +1942,7 @@ void main() {
19381942
SemanticsAction.tap,
19391943
SemanticsAction.moveCursorBackwardByCharacter,
19401944
SemanticsAction.moveCursorForwardByCharacter,
1945+
SemanticsAction.setSelection,
19411946
],
19421947
flags: <SemanticsFlag>[
19431948
SemanticsFlag.isTextField,
@@ -1950,4 +1955,94 @@ void main() {
19501955
semantics.dispose();
19511956
});
19521957

1958+
testWidgets('TextField change selection with semantics', (WidgetTester tester) async {
1959+
final SemanticsTester semantics = new SemanticsTester(tester);
1960+
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
1961+
final TextEditingController controller = new TextEditingController()
1962+
..text = 'Hello';
1963+
final Key key = new UniqueKey();
1964+
1965+
await tester.pumpWidget(
1966+
overlay(
1967+
child: new TextField(
1968+
key: key,
1969+
controller: controller,
1970+
),
1971+
),
1972+
);
1973+
1974+
// Focus the text field
1975+
await tester.tap(find.byKey(key));
1976+
await tester.pump();
1977+
1978+
const int inputFieldId = 2;
1979+
1980+
expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream));
1981+
expect(semantics, hasSemantics(new TestSemantics.root(
1982+
children: <TestSemantics>[
1983+
new TestSemantics.rootChild(
1984+
id: inputFieldId,
1985+
value: 'Hello',
1986+
textSelection: const TextSelection.collapsed(offset: 5),
1987+
textDirection: TextDirection.ltr,
1988+
actions: <SemanticsAction>[
1989+
SemanticsAction.tap,
1990+
SemanticsAction.moveCursorBackwardByCharacter,
1991+
SemanticsAction.setSelection,
1992+
],
1993+
flags: <SemanticsFlag>[
1994+
SemanticsFlag.isTextField,
1995+
SemanticsFlag.isFocused,
1996+
],
1997+
),
1998+
],
1999+
), ignoreTransform: true, ignoreRect: true));
2000+
2001+
// move cursor back once
2002+
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
2003+
'base': 4,
2004+
'extent': 4,
2005+
});
2006+
await tester.pump();
2007+
expect(controller.selection, const TextSelection.collapsed(offset: 4));
2008+
2009+
// move cursor to front
2010+
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
2011+
'base': 0,
2012+
'extent': 0,
2013+
});
2014+
await tester.pump();
2015+
expect(controller.selection, const TextSelection.collapsed(offset: 0));
2016+
2017+
// select all
2018+
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
2019+
'base': 0,
2020+
'extent': 5,
2021+
});
2022+
await tester.pump();
2023+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
2024+
expect(semantics, hasSemantics(new TestSemantics.root(
2025+
children: <TestSemantics>[
2026+
new TestSemantics.rootChild(
2027+
id: inputFieldId,
2028+
value: 'Hello',
2029+
textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
2030+
textDirection: TextDirection.ltr,
2031+
actions: <SemanticsAction>[
2032+
SemanticsAction.tap,
2033+
SemanticsAction.moveCursorBackwardByCharacter,
2034+
SemanticsAction.setSelection,
2035+
],
2036+
flags: <SemanticsFlag>[
2037+
SemanticsFlag.isTextField,
2038+
SemanticsFlag.isFocused,
2039+
],
2040+
),
2041+
],
2042+
), ignoreTransform: true, ignoreRect: true));
2043+
2044+
semantics.dispose();
2045+
});
2046+
2047+
19532048
}

0 commit comments

Comments
 (0)