Add arguments to SemanticsActions; implement extend selection for a11y (#13490)
**This PR contains a breaking API change:** `SemanticsConfiguration.addAction()` has been removed and replaces by action-specific setters (`onTap`, `onLongPress`, etc.) to take care of the fact that some action handlers (those, who take arguments) have different signatures.
This commit is contained in:
parent
1dd68d5deb
commit
d04c906e0b
@ -1 +1 @@
|
||||
2bdb21985c5eb9250a7b744b9b4d606c5ba30945
|
||||
e07eafae1d14b454c33b1ae68d14b7a5694c22b6
|
||||
|
@ -383,8 +383,8 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
|
||||
|
||||
config.isSemanticBoundary = isInteractive;
|
||||
if (isInteractive) {
|
||||
config.addAction(SemanticsAction.increase, _increaseAction);
|
||||
config.addAction(SemanticsAction.decrease, _decreaseAction);
|
||||
config.onIncrease = _increaseAction;
|
||||
config.onDecrease = _decreaseAction;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,7 +380,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
|
||||
config.isSemanticBoundary = isInteractive;
|
||||
if (isInteractive)
|
||||
config.addAction(SemanticsAction.tap, _handleTap);
|
||||
config.onTap = _handleTap;
|
||||
config.isChecked = _value;
|
||||
}
|
||||
|
||||
|
@ -713,8 +713,8 @@ class _RenderSlider extends RenderBox {
|
||||
|
||||
config.isSemanticBoundary = isInteractive;
|
||||
if (isInteractive) {
|
||||
config.addAction(SemanticsAction.increase, _increaseAction);
|
||||
config.addAction(SemanticsAction.decrease, _decreaseAction);
|
||||
config.onIncrease = _increaseAction;
|
||||
config.onDecrease = _decreaseAction;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,7 +288,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
||||
|
||||
config.isSemanticBoundary = isInteractive;
|
||||
if (isInteractive)
|
||||
config.addAction(SemanticsAction.tap, _handleTap);
|
||||
config.onTap = _handleTap;
|
||||
config.isChecked = _value;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui show window;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -187,8 +188,12 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSemanticsAction(int id, SemanticsAction action) {
|
||||
_pipelineOwner.semanticsOwner?.performAction(id, action);
|
||||
void _handleSemanticsAction(int id, SemanticsAction action, ByteData args) {
|
||||
_pipelineOwner.semanticsOwner?.performAction(
|
||||
id,
|
||||
action,
|
||||
args != null ? const StandardMessageCodec().decodeMessage(args) : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSemanticsOwnerCreated() {
|
||||
|
@ -823,34 +823,34 @@ class RenderCustomPaint extends RenderProxyBox {
|
||||
config.textDirection = properties.textDirection;
|
||||
}
|
||||
if (properties.onTap != null) {
|
||||
config.addAction(SemanticsAction.tap, properties.onTap);
|
||||
config.onTap = properties.onTap;
|
||||
}
|
||||
if (properties.onLongPress != null) {
|
||||
config.addAction(SemanticsAction.longPress, properties.onLongPress);
|
||||
config.onLongPress = properties.onLongPress;
|
||||
}
|
||||
if (properties.onScrollLeft != null) {
|
||||
config.addAction(SemanticsAction.scrollLeft, properties.onScrollLeft);
|
||||
config.onScrollLeft = properties.onScrollLeft;
|
||||
}
|
||||
if (properties.onScrollRight != null) {
|
||||
config.addAction(SemanticsAction.scrollRight, properties.onScrollRight);
|
||||
config.onScrollRight = properties.onScrollRight;
|
||||
}
|
||||
if (properties.onScrollUp != null) {
|
||||
config.addAction(SemanticsAction.scrollUp, properties.onScrollUp);
|
||||
config.onScrollUp = properties.onScrollUp;
|
||||
}
|
||||
if (properties.onScrollDown != null) {
|
||||
config.addAction(SemanticsAction.scrollDown, properties.onScrollDown);
|
||||
config.onScrollDown = properties.onScrollDown;
|
||||
}
|
||||
if (properties.onIncrease != null) {
|
||||
config.addAction(SemanticsAction.increase, properties.onIncrease);
|
||||
config.onIncrease = properties.onIncrease;
|
||||
}
|
||||
if (properties.onDecrease != null) {
|
||||
config.addAction(SemanticsAction.decrease, properties.onDecrease);
|
||||
config.onDecrease = properties.onDecrease;
|
||||
}
|
||||
if (properties.onMoveCursorForwardByCharacter != null) {
|
||||
config.addAction(SemanticsAction.moveCursorForwardByCharacter, properties.onMoveCursorForwardByCharacter);
|
||||
config.onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter;
|
||||
}
|
||||
if (properties.onMoveCursorBackwardByCharacter != null) {
|
||||
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, properties.onMoveCursorBackwardByCharacter);
|
||||
config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
|
||||
}
|
||||
|
||||
newChild.updateWith(
|
||||
|
@ -349,30 +349,33 @@ class RenderEditable extends RenderBox {
|
||||
..isTextField = true;
|
||||
|
||||
if (_selection?.isValid == true) {
|
||||
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null) {
|
||||
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, () {
|
||||
final int offset = _textPainter.getOffsetBefore(_selection.extentOffset);
|
||||
if (offset == null)
|
||||
return;
|
||||
onSelectionChanged(
|
||||
new TextSelection.collapsed(offset: offset), this, SelectionChangedCause.keyboard,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null) {
|
||||
config.addAction(SemanticsAction.moveCursorForwardByCharacter, () {
|
||||
final int offset = _textPainter.getOffsetAfter(_selection.extentOffset);
|
||||
if (offset == null)
|
||||
return;
|
||||
onSelectionChanged(
|
||||
new TextSelection.collapsed(offset: offset), this, SelectionChangedCause.keyboard,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
|
||||
config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
|
||||
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
|
||||
config.onMoveCursorForwardByCharacter = _handleMoveCursorForwardByCharacter;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
|
||||
final int extentOffset = _textPainter.getOffsetAfter(_selection.extentOffset);
|
||||
if (extentOffset == null)
|
||||
return;
|
||||
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
|
||||
onSelectionChanged(
|
||||
new TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMoveCursorBackwardByCharacter(bool extentSelection) {
|
||||
final int extentOffset = _textPainter.getOffsetBefore(_selection.extentOffset);
|
||||
if (extentOffset == null)
|
||||
return;
|
||||
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
|
||||
onSelectionChanged(
|
||||
new TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
super.attach(owner);
|
||||
|
@ -2140,7 +2140,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
/// void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
/// super.describeSemanticsConfiguration(config);
|
||||
/// config
|
||||
/// ..addAction(SemanticsAction.tap, _handleTap)
|
||||
/// ..onTap = _handleTap
|
||||
/// ..label = 'I am a button'
|
||||
/// ..isButton = true;
|
||||
/// }
|
||||
|
@ -2685,27 +2685,26 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
|
||||
config.explicitChildNodes = onHorizontalDragUpdate != null
|
||||
|| onVerticalDragUpdate != null;
|
||||
|
||||
final Map<SemanticsAction, VoidCallback> actions = <SemanticsAction, VoidCallback>{};
|
||||
if (onTap != null)
|
||||
actions[SemanticsAction.tap] = onTap;
|
||||
if (onLongPress != null)
|
||||
actions[SemanticsAction.longPress] = onLongPress;
|
||||
if (onTap != null && _isValidAction(SemanticsAction.tap))
|
||||
config.onTap = onTap;
|
||||
if (onLongPress != null && _isValidAction(SemanticsAction.longPress))
|
||||
config.onLongPress = onLongPress;
|
||||
if (onHorizontalDragUpdate != null) {
|
||||
actions[SemanticsAction.scrollRight] = _performSemanticScrollRight;
|
||||
actions[SemanticsAction.scrollLeft] = _performSemanticScrollLeft;
|
||||
if (_isValidAction(SemanticsAction.scrollRight))
|
||||
config.onScrollRight = _performSemanticScrollRight;
|
||||
if (_isValidAction(SemanticsAction.scrollLeft))
|
||||
config.onScrollLeft = _performSemanticScrollLeft;
|
||||
}
|
||||
if (onVerticalDragUpdate != null) {
|
||||
actions[SemanticsAction.scrollUp] = _performSemanticScrollUp;
|
||||
actions[SemanticsAction.scrollDown] = _performSemanticScrollDown;
|
||||
if (_isValidAction(SemanticsAction.scrollUp))
|
||||
config.onScrollUp = _performSemanticScrollUp;
|
||||
if (_isValidAction(SemanticsAction.scrollDown))
|
||||
config.onScrollDown = _performSemanticScrollDown;
|
||||
}
|
||||
}
|
||||
|
||||
final Iterable<SemanticsAction> actionsToAdd = validActions ?? actions.keys;
|
||||
|
||||
for (SemanticsAction action in actionsToAdd) {
|
||||
final VoidCallback handler = actions[action];
|
||||
if (handler != null)
|
||||
config.addAction(action, handler);
|
||||
}
|
||||
bool _isValidAction(SemanticsAction action) {
|
||||
return validActions == null || validActions.contains(action);
|
||||
}
|
||||
|
||||
SemanticsNode _innerNode;
|
||||
@ -2830,8 +2829,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
VoidCallback onScrollDown,
|
||||
VoidCallback onIncrease,
|
||||
VoidCallback onDecrease,
|
||||
VoidCallback onMoveCursorForwardByCharacter,
|
||||
VoidCallback onMoveCursorBackwardByCharacter,
|
||||
MoveCursorHandler onMoveCursorForwardByCharacter,
|
||||
MoveCursorHandler onMoveCursorBackwardByCharacter,
|
||||
}) : assert(container != null),
|
||||
_container = container,
|
||||
_explicitChildNodes = explicitChildNodes,
|
||||
@ -3173,9 +3172,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
///
|
||||
/// TalkBack users can trigger this by pressing the volume up key while the
|
||||
/// input focus is in a text field.
|
||||
VoidCallback get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
|
||||
VoidCallback _onMoveCursorForwardByCharacter;
|
||||
set onMoveCursorForwardByCharacter(VoidCallback handler) {
|
||||
MoveCursorHandler get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
|
||||
MoveCursorHandler _onMoveCursorForwardByCharacter;
|
||||
set onMoveCursorForwardByCharacter(MoveCursorHandler handler) {
|
||||
if (_onMoveCursorForwardByCharacter == handler)
|
||||
return;
|
||||
final bool hadValue = _onMoveCursorForwardByCharacter != null;
|
||||
@ -3191,9 +3190,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
///
|
||||
/// TalkBack users can trigger this by pressing the volume down key while the
|
||||
/// input focus is in a text field.
|
||||
VoidCallback get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
|
||||
VoidCallback _onMoveCursorBackwardByCharacter;
|
||||
set onMoveCursorBackwardByCharacter(VoidCallback handler) {
|
||||
MoveCursorHandler get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
|
||||
MoveCursorHandler _onMoveCursorBackwardByCharacter;
|
||||
set onMoveCursorBackwardByCharacter(MoveCursorHandler handler) {
|
||||
if (_onMoveCursorBackwardByCharacter == handler)
|
||||
return;
|
||||
final bool hadValue = _onMoveCursorBackwardByCharacter != null;
|
||||
@ -3230,25 +3229,25 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
// ones to ensure that changing a user provided handler from a non-null to
|
||||
// another non-null value doesn't require a semantics update.
|
||||
if (onTap != null)
|
||||
config.addAction(SemanticsAction.tap, _performTap);
|
||||
config.onTap = _performTap;
|
||||
if (onLongPress != null)
|
||||
config.addAction(SemanticsAction.longPress, _performLongPress);
|
||||
config.onLongPress = _performLongPress;
|
||||
if (onScrollLeft != null)
|
||||
config.addAction(SemanticsAction.scrollLeft, _performScrollLeft);
|
||||
config.onScrollLeft = _performScrollLeft;
|
||||
if (onScrollRight != null)
|
||||
config.addAction(SemanticsAction.scrollRight, _performScrollRight);
|
||||
config.onScrollRight = _performScrollRight;
|
||||
if (onScrollUp != null)
|
||||
config.addAction(SemanticsAction.scrollUp, _performScrollUp);
|
||||
config.onScrollUp = _performScrollUp;
|
||||
if (onScrollDown != null)
|
||||
config.addAction(SemanticsAction.scrollDown, _performScrollDown);
|
||||
config.onScrollDown = _performScrollDown;
|
||||
if (onIncrease != null)
|
||||
config.addAction(SemanticsAction.increase, _performIncrease);
|
||||
config.onIncrease = _performIncrease;
|
||||
if (onDecrease != null)
|
||||
config.addAction(SemanticsAction.decrease, _performDecrease);
|
||||
config.onDecrease = _performDecrease;
|
||||
if (onMoveCursorForwardByCharacter != null)
|
||||
config.addAction(SemanticsAction.moveCursorForwardByCharacter, _performMoveCursorForwardByCharacter);
|
||||
config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
|
||||
if (onMoveCursorBackwardByCharacter != null)
|
||||
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, _performMoveCursorBackwardByCharacter);
|
||||
config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
|
||||
}
|
||||
|
||||
void _performTap() {
|
||||
@ -3291,14 +3290,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
onDecrease();
|
||||
}
|
||||
|
||||
void _performMoveCursorForwardByCharacter() {
|
||||
void _performMoveCursorForwardByCharacter(bool extendSelection) {
|
||||
if (onMoveCursorForwardByCharacter != null)
|
||||
onMoveCursorForwardByCharacter();
|
||||
onMoveCursorForwardByCharacter(extendSelection);
|
||||
}
|
||||
|
||||
void _performMoveCursorBackwardByCharacter() {
|
||||
void _performMoveCursorBackwardByCharacter(bool extendSelection) {
|
||||
if (onMoveCursorBackwardByCharacter != null)
|
||||
onMoveCursorBackwardByCharacter();
|
||||
onMoveCursorBackwardByCharacter(extendSelection);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,14 @@ export 'semantics_event.dart';
|
||||
/// Used by [SemanticsNode.visitChildren].
|
||||
typedef bool SemanticsNodeVisitor(SemanticsNode node);
|
||||
|
||||
/// Signature for [SemanticsAction]s that move the cursor.
|
||||
///
|
||||
/// If `extendSelection` is set to true the cursor movement should extend the
|
||||
/// current selection or (if nothing is currently selected) start a selection.
|
||||
typedef void MoveCursorHandler(bool extendSelection);
|
||||
|
||||
typedef void _SemanticsActionHandler(dynamic args);
|
||||
|
||||
/// A tag for a [SemanticsNode].
|
||||
///
|
||||
/// Tags can be interpreted by the parent of a [SemanticsNode]
|
||||
@ -445,7 +453,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
///
|
||||
/// TalkBack users can trigger this by pressing the volume up key while the
|
||||
/// input focus is in a text field.
|
||||
final VoidCallback onMoveCursorForwardByCharacter;
|
||||
final MoveCursorHandler onMoveCursorForwardByCharacter;
|
||||
|
||||
/// The handler for [SemanticsAction.onMoveCursorBackwardByCharacter].
|
||||
///
|
||||
@ -454,7 +462,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
///
|
||||
/// TalkBack users can trigger this by pressing the volume down key while the
|
||||
/// input focus is in a text field.
|
||||
final VoidCallback onMoveCursorBackwardByCharacter;
|
||||
final MoveCursorHandler onMoveCursorBackwardByCharacter;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
@ -824,7 +832,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
|
||||
// TAGS, LABELS, ACTIONS
|
||||
|
||||
Map<SemanticsAction, VoidCallback> _actions = _kEmptyConfig._actions;
|
||||
Map<SemanticsAction, _SemanticsActionHandler> _actions = _kEmptyConfig._actions;
|
||||
|
||||
int _actionsAsBits = _kEmptyConfig._actionsAsBits;
|
||||
|
||||
@ -912,7 +920,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
_hint = config.hint;
|
||||
_flags = config._flags;
|
||||
_textDirection = config.textDirection;
|
||||
_actions = new Map<SemanticsAction, VoidCallback>.from(config._actions);
|
||||
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
|
||||
_actionsAsBits = config._actionsAsBits;
|
||||
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
|
||||
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
|
||||
@ -1213,7 +1221,7 @@ class SemanticsOwner extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
VoidCallback _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
|
||||
_SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
|
||||
SemanticsNode result = _nodes[id];
|
||||
if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) {
|
||||
result._visitDescendants((SemanticsNode node) {
|
||||
@ -1233,11 +1241,14 @@ class SemanticsOwner extends ChangeNotifier {
|
||||
///
|
||||
/// If the [SemanticsNode] has not indicated that it can perform the action,
|
||||
/// this function does nothing.
|
||||
void performAction(int id, SemanticsAction action) {
|
||||
///
|
||||
/// If the given `action` requires arguments they need to be passed in via
|
||||
/// the `args` parameter.
|
||||
void performAction(int id, SemanticsAction action, [dynamic args]) {
|
||||
assert(action != null);
|
||||
final VoidCallback handler = _getSemanticsActionHandlerForId(id, action);
|
||||
final _SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
|
||||
if (handler != null) {
|
||||
handler();
|
||||
handler(args);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1246,7 +1257,7 @@ class SemanticsOwner extends ChangeNotifier {
|
||||
_nodes[id]._showOnScreen();
|
||||
}
|
||||
|
||||
VoidCallback _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
|
||||
_SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
|
||||
if (node.transform != null) {
|
||||
final Matrix4 inverse = new Matrix4.identity();
|
||||
if (inverse.copyInverse(node.transform) == 0.0)
|
||||
@ -1268,7 +1279,7 @@ class SemanticsOwner extends ChangeNotifier {
|
||||
}
|
||||
if (node.hasChildren) {
|
||||
for (SemanticsNode child in node._children.reversed) {
|
||||
final VoidCallback handler = _getSemanticsActionHandlerForPosition(child, position, action);
|
||||
final _SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action);
|
||||
if (handler != null)
|
||||
return handler;
|
||||
}
|
||||
@ -1280,14 +1291,17 @@ class SemanticsOwner extends ChangeNotifier {
|
||||
///
|
||||
/// If the [SemanticsNode] has not indicated that it can perform the action,
|
||||
/// this function does nothing.
|
||||
void performActionAt(Offset position, SemanticsAction action) {
|
||||
///
|
||||
/// If the given `action` requires arguments they need to be passed in via
|
||||
/// the `args` parameter.
|
||||
void performActionAt(Offset position, SemanticsAction action, [dynamic args]) {
|
||||
assert(action != null);
|
||||
final SemanticsNode node = rootSemanticsNode;
|
||||
if (node == null)
|
||||
return;
|
||||
final VoidCallback handler = _getSemanticsActionHandlerForPosition(node, position, action);
|
||||
final _SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
|
||||
if (handler != null)
|
||||
handler();
|
||||
handler(args);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1366,7 +1380,7 @@ class SemanticsConfiguration {
|
||||
bool _isMergingDescendantsIntoOneNode = false;
|
||||
set isMergingDescendantsIntoOneNode(bool value) {
|
||||
assert(isSemanticBoundary);
|
||||
_isMergingDescendantsIntoOneNode = isMergingDescendantsIntoOneNode;
|
||||
_isMergingDescendantsIntoOneNode = value;
|
||||
}
|
||||
|
||||
// SEMANTIC ANNOTATIONS
|
||||
@ -1386,27 +1400,233 @@ class SemanticsConfiguration {
|
||||
/// See also:
|
||||
///
|
||||
/// * [addAction] to add an action.
|
||||
final Map<SemanticsAction, VoidCallback> _actions = <SemanticsAction, VoidCallback>{};
|
||||
final Map<SemanticsAction, _SemanticsActionHandler> _actions = <SemanticsAction, _SemanticsActionHandler>{};
|
||||
|
||||
int _actionsAsBits = 0;
|
||||
|
||||
/// Adds an `action` to the semantics tree.
|
||||
///
|
||||
/// Whenever the user performs `action` the provided `handler` is called.
|
||||
void addAction(SemanticsAction action, VoidCallback handler) {
|
||||
/// The provided `handler` is called to respond to the user triggered
|
||||
/// `action`.
|
||||
void _addAction(SemanticsAction action, _SemanticsActionHandler handler) {
|
||||
assert(handler != null);
|
||||
_actions[action] = handler;
|
||||
_actionsAsBits |= action.index;
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// Adds an `action` to the semantics tree, whose `handler` does not expect
|
||||
/// any arguments.
|
||||
///
|
||||
/// The provided `handler` is called to respond to the user triggered
|
||||
/// `action`.
|
||||
void _addArgumentlessAction(SemanticsAction action, VoidCallback handler) {
|
||||
assert(handler != null);
|
||||
_addAction(action, (dynamic args) {
|
||||
assert(args == null);
|
||||
handler();
|
||||
});
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.tap].
|
||||
///
|
||||
/// This is the semantic equivalent of a user briefly tapping the screen with
|
||||
/// the finger without moving it. For example, a button should implement this
|
||||
/// action.
|
||||
///
|
||||
/// VoiceOver users on iOS and TalkBack users on Android can trigger this
|
||||
/// action by double-tapping the screen while an element is focused.
|
||||
VoidCallback get onTap => _onTap;
|
||||
VoidCallback _onTap;
|
||||
set onTap(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.tap, value);
|
||||
_onTap = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.longPress].
|
||||
///
|
||||
/// This is the semantic equivalent of a user pressing and holding the screen
|
||||
/// with the finger for a few seconds without moving it.
|
||||
///
|
||||
/// VoiceOver users on iOS and TalkBack users on Android can trigger this
|
||||
/// action by double-tapping the screen without lifting the finger after the
|
||||
/// second tap.
|
||||
VoidCallback get onLongPress => _onLongPress;
|
||||
VoidCallback _onLongPress;
|
||||
set onLongPress(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.longPress, value);
|
||||
_onLongPress = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.scrollLeft].
|
||||
///
|
||||
/// This is the semantic equivalent of a user moving their finger across the
|
||||
/// screen from right to left. It should be recognized by controls that are
|
||||
/// horizontally scrollable.
|
||||
///
|
||||
/// VoiceOver users on iOS can trigger this action by swiping left with three
|
||||
/// fingers. TalkBack users on Android can trigger this action by swiping
|
||||
/// right and then left in one motion path. On Android, [onScrollUp] and
|
||||
/// [onScrollLeft] share the same gesture. Therefore, only on of them should
|
||||
/// be provided.
|
||||
VoidCallback get onScrollLeft => _onScrollLeft;
|
||||
VoidCallback _onScrollLeft;
|
||||
set onScrollLeft(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.scrollLeft, value);
|
||||
_onScrollLeft = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.scrollRight].
|
||||
///
|
||||
/// This is the semantic equivalent of a user moving their finger across the
|
||||
/// screen from left to right. It should be recognized by controls that are
|
||||
/// horizontally scrollable.
|
||||
///
|
||||
/// VoiceOver users on iOS can trigger this action by swiping right with three
|
||||
/// fingers. TalkBack users on Android can trigger this action by swiping
|
||||
/// left and then right in one motion path. On Android, [onScrollDown] and
|
||||
/// [onScrollRight] share the same gesture. Therefore, only on of them should
|
||||
/// be provided.
|
||||
VoidCallback get onScrollRight => _onScrollRight;
|
||||
VoidCallback _onScrollRight;
|
||||
set onScrollRight(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.scrollRight, value);
|
||||
_onScrollRight = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.scrollUp].
|
||||
///
|
||||
/// This is the semantic equivalent of a user moving their finger across the
|
||||
/// screen from bottom to top. It should be recognized by controls that are
|
||||
/// vertically scrollable.
|
||||
///
|
||||
/// VoiceOver users on iOS can trigger this action by swiping up with three
|
||||
/// fingers. TalkBack users on Android can trigger this action by swiping
|
||||
/// right and then left in one motion path. On Android, [onScrollUp] and
|
||||
/// [onScrollLeft] share the same gesture. Therefore, only on of them should
|
||||
/// be provided.
|
||||
VoidCallback get onScrollUp => _onScrollUp;
|
||||
VoidCallback _onScrollUp;
|
||||
set onScrollUp(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.scrollUp, value);
|
||||
_onScrollUp = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.scrollDown].
|
||||
///
|
||||
/// This is the semantic equivalent of a user moving their finger across the
|
||||
/// screen from top to bottom. It should be recognized by controls that are
|
||||
/// vertically scrollable.
|
||||
///
|
||||
/// VoiceOver users on iOS can trigger this action by swiping down with three
|
||||
/// fingers. TalkBack users on Android can trigger this action by swiping
|
||||
/// left and then right in one motion path. On Android, [onScrollDown] and
|
||||
/// [onScrollRight] share the same gesture. Therefore, only on of them should
|
||||
/// be provided.
|
||||
VoidCallback get onScrollDown => _onScrollDown;
|
||||
VoidCallback _onScrollDown;
|
||||
set onScrollDown(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.scrollDown, value);
|
||||
_onScrollDown = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.increase].
|
||||
///
|
||||
/// This is a request to increase the value represented by the widget. For
|
||||
/// example, this action might be recognized by a slider control.
|
||||
///
|
||||
/// If a [value] is set, [increasedValue] must also be provided and
|
||||
/// [onIncrease] must ensure that [value] will be set to [increasedValue].
|
||||
///
|
||||
/// VoiceOver users on iOS can trigger this action by swiping up with one
|
||||
/// finger. TalkBack users on Android can trigger this action by pressing the
|
||||
/// volume up button.
|
||||
VoidCallback get onIncrease => _onIncrease;
|
||||
VoidCallback _onIncrease;
|
||||
set onIncrease(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.increase, value);
|
||||
_onIncrease = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.decrease].
|
||||
///
|
||||
/// This is a request to decrease the value represented by the widget. For
|
||||
/// example, this action might be recognized by a slider control.
|
||||
///
|
||||
/// If a [value] is set, [decreasedValue] must also be provided and
|
||||
/// [onDecrease] must ensure that [value] will be set to [decreasedValue].
|
||||
///
|
||||
/// VoiceOver users on iOS can trigger this action by swiping down with one
|
||||
/// finger. TalkBack users on Android can trigger this action by pressing the
|
||||
/// volume down button.
|
||||
VoidCallback get onDecrease => _onDecrease;
|
||||
VoidCallback _onDecrease;
|
||||
set onDecrease(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.decrease, value);
|
||||
_onDecrease = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.showOnScreen].
|
||||
///
|
||||
/// A request to fully show the semantics node on screen. For example, this
|
||||
/// action might be send to a node in a scrollable list that is partially off
|
||||
/// screen to bring it on screen.
|
||||
///
|
||||
/// For elements in a scrollable list the framework provides a default
|
||||
/// implementation for this action and it is not advised to provide a
|
||||
/// custom one via this setter.
|
||||
VoidCallback get onShowOnScreen => _onShowOnScreen;
|
||||
VoidCallback _onShowOnScreen;
|
||||
set onShowOnScreen(VoidCallback value) {
|
||||
_addArgumentlessAction(SemanticsAction.showOnScreen, value);
|
||||
_onShowOnScreen = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.onMoveCursorForwardByCharacter].
|
||||
///
|
||||
/// This handler is invoked when the user wants to move the cursor in a
|
||||
/// text field forward by one character.
|
||||
///
|
||||
/// TalkBack users can trigger this by pressing the volume up key while the
|
||||
/// input focus is in a text field.
|
||||
MoveCursorHandler get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
|
||||
MoveCursorHandler _onMoveCursorForwardByCharacter;
|
||||
set onMoveCursorForwardByCharacter(MoveCursorHandler value) {
|
||||
assert(value != null);
|
||||
_addAction(SemanticsAction.moveCursorForwardByCharacter, (dynamic args) {
|
||||
final bool extentSelection = args;
|
||||
assert(extentSelection != null);
|
||||
value(extentSelection);
|
||||
});
|
||||
_onMoveCursorForwardByCharacter = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.onMoveCursorBackwardByCharacter].
|
||||
///
|
||||
/// This handler is invoked when the user wants to move the cursor in a
|
||||
/// text field backward by one character.
|
||||
///
|
||||
/// TalkBack users can trigger this by pressing the volume down key while the
|
||||
/// input focus is in a text field.
|
||||
MoveCursorHandler get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
|
||||
MoveCursorHandler _onMoveCursorBackwardByCharacter;
|
||||
set onMoveCursorBackwardByCharacter(MoveCursorHandler value) {
|
||||
assert(value != null);
|
||||
_addAction(SemanticsAction.moveCursorBackwardByCharacter, (dynamic args) {
|
||||
final bool extentSelection = args;
|
||||
assert(extentSelection != null);
|
||||
value(extentSelection);
|
||||
});
|
||||
_onMoveCursorBackwardByCharacter = value;
|
||||
}
|
||||
|
||||
/// Returns the action handler registered for [action] or null if none was
|
||||
/// registered.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [addAction] to add an action.
|
||||
VoidCallback getActionHandler(SemanticsAction action) => _actions[action];
|
||||
_SemanticsActionHandler getActionHandler(SemanticsAction action) => _actions[action];
|
||||
|
||||
/// Whether the semantic information provided by the owning [RenderObject] and
|
||||
/// all of its descendants should be treated as one logical entity.
|
||||
@ -1516,6 +1736,7 @@ class SemanticsConfiguration {
|
||||
}
|
||||
|
||||
/// Whether the owning [RenderObject] is selected (true) or not (false).
|
||||
bool get isSelected => _hasFlag(SemanticsFlags.isSelected);
|
||||
set isSelected(bool value) {
|
||||
_setFlag(SemanticsFlags.isSelected, value);
|
||||
}
|
||||
@ -1525,22 +1746,26 @@ class SemanticsConfiguration {
|
||||
///
|
||||
/// Do not set this to any value if the owning [RenderObject] doesn't have
|
||||
/// Booleans state that can be controlled by the user.
|
||||
bool get isChecked => _hasFlag(SemanticsFlags.hasCheckedState) && _hasFlag(SemanticsFlags.isChecked);
|
||||
set isChecked(bool value) {
|
||||
_setFlag(SemanticsFlags.hasCheckedState, true);
|
||||
_setFlag(SemanticsFlags.isChecked, value);
|
||||
}
|
||||
|
||||
/// Whether the owning [RenderObject] currently holds the user's focus.
|
||||
bool get isFocused => _hasFlag(SemanticsFlags.isFocused);
|
||||
set isFocused(bool value) {
|
||||
_setFlag(SemanticsFlags.isFocused, value);
|
||||
}
|
||||
|
||||
/// Whether the owning [RenderObject] is a button (true) or not (false).
|
||||
bool get isButton => _hasFlag(SemanticsFlags.isButton);
|
||||
set isButton(bool value) {
|
||||
_setFlag(SemanticsFlags.isButton, value);
|
||||
}
|
||||
|
||||
/// Whether the owning [RenderObject] is a text field.
|
||||
bool get isTextField => _hasFlag(SemanticsFlags.isTextField);
|
||||
set isTextField(bool value) {
|
||||
_setFlag(SemanticsFlags.isTextField, value);
|
||||
}
|
||||
@ -1589,6 +1814,8 @@ class SemanticsConfiguration {
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
bool _hasFlag(SemanticsFlags flag) => (_flags & flag.index) != 0;
|
||||
|
||||
// CONFIGURATION COMBINATION LOGIC
|
||||
|
||||
/// Whether this configuration is compatible with the provided `other`
|
||||
|
@ -265,10 +265,17 @@ TextEditingValue _selectionAwareTextManipulation(
|
||||
value.text.substring(selectionEndIndex)
|
||||
);
|
||||
manipulatedText = beforeSelection + inSelection + afterSelection;
|
||||
manipulatedSelection = value.selection.copyWith(
|
||||
baseOffset: beforeSelection.length,
|
||||
extentOffset: beforeSelection.length + inSelection.length,
|
||||
);
|
||||
if (value.selection.baseOffset > value.selection.extentOffset) {
|
||||
manipulatedSelection = value.selection.copyWith(
|
||||
baseOffset: beforeSelection.length + inSelection.length,
|
||||
extentOffset: beforeSelection.length,
|
||||
);
|
||||
} else {
|
||||
manipulatedSelection = value.selection.copyWith(
|
||||
baseOffset: beforeSelection.length,
|
||||
extentOffset: beforeSelection.length + inSelection.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
return new TextEditingValue(
|
||||
text: manipulatedText,
|
||||
|
@ -4726,8 +4726,8 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
VoidCallback onScrollDown,
|
||||
VoidCallback onIncrease,
|
||||
VoidCallback onDecrease,
|
||||
VoidCallback onMoveCursorForwardByCharacter,
|
||||
VoidCallback onMoveCursorBackwardByCharacter,
|
||||
MoveCursorHandler onMoveCursorForwardByCharacter,
|
||||
MoveCursorHandler onMoveCursorBackwardByCharacter,
|
||||
}) : this.fromProperties(
|
||||
key: key,
|
||||
child: child,
|
||||
|
@ -65,19 +65,19 @@ void main() {
|
||||
|
||||
TestRender middle;
|
||||
final TestRender root = new TestRender(
|
||||
action: SemanticsAction.tap,
|
||||
hasTapAction: true,
|
||||
isSemanticBoundary: true,
|
||||
child: new TestRender(
|
||||
action: SemanticsAction.longPress,
|
||||
hasLongPressAction: true,
|
||||
isSemanticBoundary: false,
|
||||
child: middle = new TestRender(
|
||||
action: SemanticsAction.scrollLeft,
|
||||
hasScrollLeftAction: true,
|
||||
isSemanticBoundary: false,
|
||||
child: new TestRender(
|
||||
action: SemanticsAction.scrollRight,
|
||||
hasScrollRightAction: true,
|
||||
isSemanticBoundary: false,
|
||||
child: new TestRender(
|
||||
action: SemanticsAction.scrollUp,
|
||||
hasScrollUpAction: true,
|
||||
isSemanticBoundary: true,
|
||||
)
|
||||
)
|
||||
@ -91,7 +91,9 @@ void main() {
|
||||
int expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollLeft.index | SemanticsAction.scrollRight.index;
|
||||
expect(root.debugSemantics.getSemanticsData().actions, expectedActions);
|
||||
|
||||
middle.action = SemanticsAction.scrollDown;
|
||||
middle
|
||||
..hasScrollLeftAction = false
|
||||
..hasScrollDownAction = true;
|
||||
middle.markNeedsSemanticsUpdate();
|
||||
|
||||
pumpFrame(phase: EnginePhase.flushSemantics);
|
||||
@ -204,9 +206,9 @@ void main() {
|
||||
|
||||
final SemanticsConfiguration config = new SemanticsConfiguration()
|
||||
..isMergingSemanticsOfDescendants = true
|
||||
..addAction(SemanticsAction.scrollUp, () { })
|
||||
..addAction(SemanticsAction.longPress, () { })
|
||||
..addAction(SemanticsAction.showOnScreen, () { })
|
||||
..onScrollUp = () { }
|
||||
..onLongPress = () { }
|
||||
..onShowOnScreen = () { }
|
||||
..isChecked = false
|
||||
..isSelected = true
|
||||
..isButton = true
|
||||
@ -237,22 +239,128 @@ void main() {
|
||||
'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])',
|
||||
);
|
||||
});
|
||||
|
||||
test('SemanticsConfiguration getter/setter', () {
|
||||
final SemanticsConfiguration config = new SemanticsConfiguration();
|
||||
|
||||
expect(config.isSemanticBoundary, isFalse);
|
||||
expect(config.isButton, isFalse);
|
||||
expect(config.isMergingSemanticsOfDescendants, isFalse);
|
||||
expect(config.isChecked, isFalse);
|
||||
expect(config.isSelected, isFalse);
|
||||
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse);
|
||||
expect(config.isFocused, isFalse);
|
||||
expect(config.isMergingDescendantsIntoOneNode, isFalse);
|
||||
expect(config.isTextField, isFalse);
|
||||
|
||||
expect(config.onShowOnScreen, isNull);
|
||||
expect(config.onScrollDown, isNull);
|
||||
expect(config.onScrollUp, isNull);
|
||||
expect(config.onScrollLeft, isNull);
|
||||
expect(config.onScrollRight, isNull);
|
||||
expect(config.onLongPress, isNull);
|
||||
expect(config.onDecrease, isNull);
|
||||
expect(config.onIncrease, isNull);
|
||||
expect(config.onMoveCursorForwardByCharacter, isNull);
|
||||
expect(config.onMoveCursorBackwardByCharacter, isNull);
|
||||
expect(config.onTap, isNull);
|
||||
|
||||
config.isSemanticBoundary = true;
|
||||
config.isButton = true;
|
||||
config.isMergingSemanticsOfDescendants = true;
|
||||
config.isChecked = true;
|
||||
config.isSelected = true;
|
||||
config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
|
||||
config.isFocused = true;
|
||||
config.isMergingDescendantsIntoOneNode = true;
|
||||
config.isTextField = true;
|
||||
|
||||
final VoidCallback onShowOnScreen = () { };
|
||||
final VoidCallback onScrollDown = () { };
|
||||
final VoidCallback onScrollUp = () { };
|
||||
final VoidCallback onScrollLeft = () { };
|
||||
final VoidCallback onScrollRight = () { };
|
||||
final VoidCallback onLongPress = () { };
|
||||
final VoidCallback onDecrease = () { };
|
||||
final VoidCallback onIncrease = () { };
|
||||
final MoveCursorHandler onMoveCursorForwardByCharacter = (bool _) { };
|
||||
final MoveCursorHandler onMoveCursorBackwardByCharacter = (bool _) { };
|
||||
final VoidCallback onTap = () { };
|
||||
|
||||
config.onShowOnScreen = onShowOnScreen;
|
||||
config.onScrollDown = onScrollDown;
|
||||
config.onScrollUp = onScrollUp;
|
||||
config.onScrollLeft = onScrollLeft;
|
||||
config.onScrollRight = onScrollRight;
|
||||
config.onLongPress = onLongPress;
|
||||
config.onDecrease = onDecrease;
|
||||
config.onIncrease = onIncrease;
|
||||
config.onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter;
|
||||
config.onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter;
|
||||
config.onTap = onTap;
|
||||
|
||||
expect(config.isSemanticBoundary, isTrue);
|
||||
expect(config.isButton, isTrue);
|
||||
expect(config.isMergingSemanticsOfDescendants, isTrue);
|
||||
expect(config.isChecked, isTrue);
|
||||
expect(config.isSelected, isTrue);
|
||||
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isTrue);
|
||||
expect(config.isFocused, isTrue);
|
||||
expect(config.isMergingDescendantsIntoOneNode, isTrue);
|
||||
expect(config.isTextField, isTrue);
|
||||
|
||||
expect(config.onShowOnScreen, same(onShowOnScreen));
|
||||
expect(config.onScrollDown, same(onScrollDown));
|
||||
expect(config.onScrollUp, same(onScrollUp));
|
||||
expect(config.onScrollLeft, same(onScrollLeft));
|
||||
expect(config.onScrollRight, same(onScrollRight));
|
||||
expect(config.onLongPress, same(onLongPress));
|
||||
expect(config.onDecrease, same(onDecrease));
|
||||
expect(config.onIncrease, same(onIncrease));
|
||||
expect(config.onMoveCursorForwardByCharacter, same(onMoveCursorForwardByCharacter));
|
||||
expect(config.onMoveCursorBackwardByCharacter, same(onMoveCursorBackwardByCharacter));
|
||||
expect(config.onTap, same(onTap));
|
||||
});
|
||||
}
|
||||
|
||||
class TestRender extends RenderProxyBox {
|
||||
|
||||
TestRender({ this.action, this.isSemanticBoundary, RenderObject child }) : super(child);
|
||||
TestRender({
|
||||
this.hasTapAction: false,
|
||||
this.hasLongPressAction: false,
|
||||
this.hasScrollLeftAction: false,
|
||||
this.hasScrollRightAction: false,
|
||||
this.hasScrollUpAction: false,
|
||||
this.hasScrollDownAction: false,
|
||||
this.isSemanticBoundary,
|
||||
RenderObject child
|
||||
}) : super(child);
|
||||
|
||||
final bool isSemanticBoundary;
|
||||
bool hasTapAction;
|
||||
bool hasLongPressAction;
|
||||
bool hasScrollLeftAction;
|
||||
bool hasScrollRightAction;
|
||||
bool hasScrollUpAction;
|
||||
bool hasScrollDownAction;
|
||||
bool isSemanticBoundary;
|
||||
|
||||
SemanticsAction action;
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
|
||||
config
|
||||
..isSemanticBoundary = isSemanticBoundary
|
||||
..addAction(action, () { });
|
||||
config.isSemanticBoundary = isSemanticBoundary;
|
||||
if (hasTapAction)
|
||||
config.onTap = () { };
|
||||
if (hasLongPressAction)
|
||||
config.onLongPress = () { };
|
||||
if (hasScrollLeftAction)
|
||||
config.onScrollLeft = () { };
|
||||
if (hasScrollRightAction)
|
||||
config.onScrollRight = () { };
|
||||
if (hasScrollUpAction)
|
||||
config.onScrollUp = () { };
|
||||
if (hasScrollDownAction)
|
||||
config.onScrollDown = () { };
|
||||
}
|
||||
}
|
||||
|
@ -414,6 +414,7 @@ void main() {
|
||||
|
||||
testWidgets('can move cursor with a11y means', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
const bool doNotExtendSelection = false;
|
||||
|
||||
controller.text = 'test';
|
||||
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
|
||||
@ -440,7 +441,7 @@ void main() {
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
expect(controller.selection.extentOffset, 4);
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter);
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 3);
|
||||
@ -454,11 +455,11 @@ void main() {
|
||||
],
|
||||
));
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter);
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter);
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter);
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
@ -472,6 +473,89 @@ void main() {
|
||||
],
|
||||
));
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 1);
|
||||
expect(controller.selection.extentOffset, 1);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('can extend selection with a11y means', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
const bool extendSelection = true;
|
||||
const bool doNotExtendSelection = false;
|
||||
|
||||
controller.text = 'test';
|
||||
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
|
||||
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
home: new EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
),
|
||||
));
|
||||
|
||||
expect(semantics, includesNodeWith(
|
||||
value: 'test',
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.moveCursorBackwardByCharacter,
|
||||
],
|
||||
));
|
||||
|
||||
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
|
||||
final int semanticsId = render.debugSemantics.id;
|
||||
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
expect(controller.selection.extentOffset, 4);
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
expect(controller.selection.extentOffset, 3);
|
||||
|
||||
expect(semantics, includesNodeWith(
|
||||
value: 'test',
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.moveCursorBackwardByCharacter,
|
||||
SemanticsAction.moveCursorForwardByCharacter,
|
||||
],
|
||||
));
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
expect(controller.selection.extentOffset, 0);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(semantics, includesNodeWith(
|
||||
value: 'test',
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.moveCursorForwardByCharacter,
|
||||
],
|
||||
));
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 1);
|
||||
expect(controller.selection.extentOffset, 1);
|
||||
|
||||
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, extendSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 1);
|
||||
expect(controller.selection.extentOffset, 2);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
@ -388,8 +388,8 @@ void main() {
|
||||
onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
|
||||
onIncrease: () => performedActions.add(SemanticsAction.increase),
|
||||
onDecrease: () => performedActions.add(SemanticsAction.decrease),
|
||||
onMoveCursorForwardByCharacter: () => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
|
||||
onMoveCursorBackwardByCharacter: () => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
|
||||
onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
|
||||
onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
|
||||
)
|
||||
);
|
||||
|
||||
@ -412,7 +412,14 @@ void main() {
|
||||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
|
||||
int expectedLength = 1;
|
||||
for (SemanticsAction action in allActions) {
|
||||
semanticsOwner.performAction(expectedId, action);
|
||||
switch (action) {
|
||||
case SemanticsAction.moveCursorBackwardByCharacter:
|
||||
case SemanticsAction.moveCursorForwardByCharacter:
|
||||
semanticsOwner.performAction(expectedId, action, true);
|
||||
break;
|
||||
default:
|
||||
semanticsOwner.performAction(expectedId, action);
|
||||
}
|
||||
expect(performedActions.length, expectedLength);
|
||||
expect(performedActions.last, action);
|
||||
expectedLength += 1;
|
||||
|
Loading…
x
Reference in New Issue
Block a user