Unverified Commit 671c1101 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Change text selection (or cursor position) via a11y (#14275)

Roll engine to 7c34dfafc9acece1a9438f206bfbb0a9bedba3bf
parent c23509e9
4c82c566edf394a5cfc237a266aea5bd37a6c172
7c34dfafc9acece1a9438f206bfbb0a9bedba3bf
......@@ -865,6 +865,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.onMoveCursorBackwardByCharacter != null) {
config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
}
if (properties.onSetSelection != null) {
config.onSetSelection = properties.onSetSelection;
}
newChild.updateWith(
config: config,
......
......@@ -356,6 +356,9 @@ class RenderEditable extends RenderBox {
..isFocused = hasFocus
..isTextField = true;
if (hasFocus)
config.onSetSelection = _handleSetSelection;
if (_selection?.isValid == true) {
config.textSelection = _selection;
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
......@@ -365,6 +368,10 @@ class RenderEditable extends RenderBox {
}
}
void _handleSetSelection(TextSelection selection) {
onSelectionChanged(selection, this, SelectionChangedCause.keyboard);
}
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
final int extentOffset = _textPainter.getOffsetAfter(_selection.extentOffset);
if (extentOffset == null)
......
......@@ -3017,6 +3017,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
VoidCallback onDecrease,
MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection,
}) : assert(container != null),
_container = container,
_explicitChildNodes = explicitChildNodes,
......@@ -3040,6 +3041,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_onDecrease = onDecrease,
_onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter,
_onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter,
_onSetSelection = onSetSelection,
super(child);
/// If 'container' is true, this [RenderObject] will introduce a new
......@@ -3399,6 +3401,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
SetSelectionHandler get onSetSelection => _onSetSelection;
SetSelectionHandler _onSetSelection;
set onSetSelection(SetSelectionHandler handler) {
if (_onSetSelection == handler)
return;
final bool hadValue = _onSetSelection != null;
_onSetSelection = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......@@ -3448,6 +3468,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
if (onMoveCursorBackwardByCharacter != null)
config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
if (onSetSelection != null)
config.onSetSelection = _performSetSelection;
}
void _performTap() {
......@@ -3499,6 +3521,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (onMoveCursorBackwardByCharacter != null)
onMoveCursorBackwardByCharacter(extendSelection);
}
void _performSetSelection(TextSelection selection) {
if (onSetSelection != null)
onSetSelection(selection);
}
}
/// Causes the semantics of all earlier render objects below the same semantic
......
......@@ -31,6 +31,10 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node);
/// current selection or (if nothing is currently selected) start a selection.
typedef void MoveCursorHandler(bool extendSelection);
/// Signature for the [SemanticsAction.setSelection] handlers to change the
/// text selection (or re-position the cursor) to `selection`.
typedef void SetSelectionHandler(TextSelection selection);
typedef void _SemanticsActionHandler(dynamic args);
/// A tag for a [SemanticsNode].
......@@ -275,6 +279,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.onDecrease,
this.onMoveCursorForwardByCharacter,
this.onMoveCursorBackwardByCharacter,
this.onSetSelection,
});
/// If non-null, indicates that this subtree represents something that can be
......@@ -485,6 +490,15 @@ class SemanticsProperties extends DiagnosticableTree {
/// input focus is in a text field.
final MoveCursorHandler onMoveCursorBackwardByCharacter;
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
final SetSelectionHandler onSetSelection;
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
......@@ -1658,6 +1672,28 @@ class SemanticsConfiguration {
_onMoveCursorBackwardByCharacter = value;
}
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
SetSelectionHandler get onSetSelection => _onSetSelection;
SetSelectionHandler _onSetSelection;
set onSetSelection(SetSelectionHandler value) {
assert(value != null);
_addAction(SemanticsAction.setSelection, (dynamic args) {
final Map<String, int> selection = args;
assert(selection != null && selection['base'] != null && selection['extent'] != null);
value(new TextSelection(
baseOffset: selection['base'],
extentOffset: selection['extent'],
));
});
_onSetSelection = value;
}
/// Returns the action handler registered for [action] or null if none was
/// registered.
///
......
......@@ -4854,6 +4854,7 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback onDecrease,
MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection,
}) : this.fromProperties(
key: key,
child: child,
......@@ -4880,6 +4881,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onDecrease: onDecrease,
onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter,
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
onSetSelection: onSetSelection,
),
);
......@@ -4948,6 +4950,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onDecrease: properties.onDecrease,
onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter,
onMoveCursorBackwardByCharacter: properties.onMoveCursorBackwardByCharacter,
onSetSelection: properties.onSetSelection,
);
}
......@@ -4986,7 +4989,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..onIncrease = properties.onIncrease
..onDecrease = properties.onDecrease
..onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter
..onMoveCursorBackwardByCharacter = properties.onMoveCursorForwardByCharacter;
..onMoveCursorBackwardByCharacter = properties.onMoveCursorForwardByCharacter
..onSetSelection = properties.onSetSelection;
}
@override
......
......@@ -1761,7 +1761,7 @@ void main() {
child: new TextField(
key: key,
controller: controller,
)
),
),
);
......@@ -1812,6 +1812,7 @@ void main() {
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1835,6 +1836,7 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1858,6 +1860,7 @@ void main() {
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1878,10 +1881,10 @@ void main() {
await tester.pumpWidget(
overlay(
child: new TextField(
key: key,
controller: controller,
)
child: new TextField(
key: key,
controller: controller,
),
),
);
......@@ -1915,6 +1918,7 @@ void main() {
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1938,6 +1942,7 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1950,4 +1955,94 @@ void main() {
semantics.dispose();
});
testWidgets('TextField change selection with semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
final TextEditingController controller = new TextEditingController()
..text = 'Hello';
final Key key = new UniqueKey();
await tester.pumpWidget(
overlay(
child: new TextField(
key: key,
controller: controller,
),
),
);
// Focus the text field
await tester.tap(find.byKey(key));
await tester.pump();
const int inputFieldId = 2;
expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream));
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: inputFieldId,
value: 'Hello',
textSelection: const TextSelection.collapsed(offset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
// move cursor back once
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
'base': 4,
'extent': 4,
});
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(offset: 4));
// move cursor to front
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
'base': 0,
'extent': 0,
});
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(offset: 0));
// select all
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
'base': 0,
'extent': 5,
});
await tester.pump();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: inputFieldId,
value: 'Hello',
textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
}
......@@ -383,6 +383,7 @@ void main() {
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
));
......@@ -395,6 +396,7 @@ void main() {
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
));
......@@ -406,6 +408,7 @@ void main() {
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
));
......@@ -452,6 +455,7 @@ void main() {
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
));
......@@ -470,6 +474,7 @@ void main() {
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
));
......@@ -523,6 +528,7 @@ void main() {
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
));
......@@ -541,6 +547,7 @@ void main() {
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
));
......
......@@ -11,6 +11,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
SemanticsTester semantics = new SemanticsTester(tester);
......@@ -390,13 +394,14 @@ void main() {
onDecrease: () => performedActions.add(SemanticsAction.decrease),
onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
)
);
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.showOnScreen); // showOnScreen is non user-exposed.
final int expectedId = 32;
const int expectedId = 2;
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
......@@ -417,6 +422,12 @@ void main() {
case SemanticsAction.moveCursorForwardByCharacter:
semanticsOwner.performAction(expectedId, action, true);
break;
case SemanticsAction.setSelection:
semanticsOwner.performAction(expectedId, action, <String, int>{
'base': 4,
'extent': 5,
});
break;
default:
semanticsOwner.performAction(expectedId, action);
}
......@@ -446,7 +457,7 @@ void main() {
),
);
final int expectedId = 35;
const int expectedId = 2;
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
......
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