Commit e3327af2 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add option for custom actions to Semantics widget (#12695)

* Add option for custom actions to Semantics widget

* review comments

* review comment
parent 311b57be
......@@ -3179,6 +3179,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
String value,
String hint,
TextDirection textDirection,
VoidCallback onTap,
VoidCallback onLongPress,
VoidCallback onScrollLeft,
VoidCallback onScrollRight,
VoidCallback onScrollUp,
VoidCallback onScrollDown,
VoidCallback onIncrease,
VoidCallback onDecrease,
}) : assert(container != null),
_container = container,
_explicitChildNodes = explicitChildNodes,
......@@ -3189,6 +3197,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_value = value,
_hint = hint,
_textDirection = textDirection,
_onTap = onTap,
_onLongPress = onLongPress,
_onScrollLeft = onScrollLeft,
_onScrollRight = onScrollRight,
_onScrollUp = onScrollUp,
_onScrollDown = onScrollDown,
_onIncrease = onIncrease,
_onDecrease = onDecrease,
super(child);
/// If 'container' is true, this [RenderObject] will introduce a new
......@@ -3317,6 +3333,170 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// 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 handler) {
if (_onTap == handler)
return;
final bool hadValue = _onTap != null;
_onTap = handler;
if ((handler != null) == hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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 handler) {
if (_onLongPress == handler)
return;
final bool hadValue = _onLongPress != null;
_onLongPress = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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 handler) {
if (_onScrollLeft == handler)
return;
final bool hadValue = _onScrollLeft != null;
_onScrollLeft = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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 handler) {
if (_onScrollRight == handler)
return;
final bool hadValue = _onScrollRight != null;
_onScrollRight = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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 handler) {
if (_onScrollUp == handler)
return;
final bool hadValue = _onScrollUp != null;
_onScrollUp = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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 handler) {
if (_onScrollDown == handler)
return;
final bool hadValue = _onScrollDown != null;
_onScrollDown = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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.
///
/// 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 handler) {
if (_onIncrease == handler)
return;
final bool hadValue = _onIncrease != null;
_onIncrease = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
/// 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.
///
/// 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 handler) {
if (_onDecrease == handler)
return;
final bool hadValue = _onDecrease != null;
_onDecrease = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate(onlyLocalUpdates: true);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.isSemanticBoundary = container;
......@@ -3326,6 +3506,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isChecked = checked;
if (selected != null)
config.isSelected = selected;
if (button != null)
config.isButton = button;
if (label != null)
config.label = label;
if (value != null)
......@@ -3334,8 +3516,65 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.hint = hint;
if (textDirection != null)
config.textDirection = textDirection;
if (button != null)
config.isButton = button;
// Registering _perform* as action handlers instead of the user provided
// 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);
if (onLongPress != null)
config.addAction(SemanticsAction.longPress, _performLongPress);
if (onScrollLeft != null)
config.addAction(SemanticsAction.scrollLeft, _performScrollLeft);
if (onScrollRight != null)
config.addAction(SemanticsAction.scrollRight, _performScrollRight);
if (onScrollUp != null)
config.addAction(SemanticsAction.scrollUp, _performScrollUp);
if (onScrollDown != null)
config.addAction(SemanticsAction.scrollDown, _performScrollDown);
if (onIncrease != null)
config.addAction(SemanticsAction.increase, _performIncrease);
if (onDecrease != null)
config.addAction(SemanticsAction.decrease, _performDecrease);
}
void _performTap() {
if (onTap != null)
onTap();
}
void _performLongPress() {
if (onLongPress != null)
onLongPress();
}
void _performScrollLeft() {
if (onScrollLeft != null)
onScrollLeft();
}
void _performScrollRight() {
if (onScrollRight != null)
onScrollRight();
}
void _performScrollUp() {
if (onScrollUp != null)
onScrollUp();
}
void _performScrollDown() {
if (onScrollDown != null)
onScrollDown();
}
void _performIncrease() {
if (onIncrease != null)
onIncrease();
}
void _performDecrease() {
if (onDecrease != null)
onDecrease();
}
}
......
......@@ -4456,6 +4456,14 @@ class Semantics extends SingleChildRenderObjectWidget {
this.value,
this.hint,
this.textDirection,
this.onTap,
this.onLongPress,
this.onScrollLeft,
this.onScrollRight,
this.onScrollUp,
this.onScrollDown,
this.onIncrease,
this.onDecrease,
}) : assert(container != null),
super(key: key, child: child);
......@@ -4540,6 +4548,98 @@ class Semantics extends SingleChildRenderObjectWidget {
return textDirection ?? (label != null || value != null || hint != null ? Directionality.of(context) : null);
}
/// 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.
final VoidCallback onTap;
/// 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.
final VoidCallback onLongPress;
/// 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.
final VoidCallback onScrollLeft;
/// 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.
final VoidCallback onScrollRight;
/// 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.
final VoidCallback onScrollUp;
/// 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.
final VoidCallback onScrollDown;
/// 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.
///
/// 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.
final VoidCallback onIncrease;
/// 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.
///
/// 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.
final VoidCallback onDecrease;
@override
RenderSemanticsAnnotations createRenderObject(BuildContext context) {
return new RenderSemanticsAnnotations(
......@@ -4552,6 +4652,14 @@ class Semantics extends SingleChildRenderObjectWidget {
value: value,
hint: hint,
textDirection: _getTextDirection(context),
onTap: onTap,
onLongPress: onLongPress,
onScrollLeft: onScrollLeft,
onScrollRight: onScrollRight,
onScrollUp: onScrollUp,
onScrollDown: onScrollDown,
onIncrease: onIncrease,
onDecrease: onDecrease,
);
}
......@@ -4565,7 +4673,15 @@ class Semantics extends SingleChildRenderObjectWidget {
..label = label
..value = value
..hint = hint
..textDirection = _getTextDirection(context);
..textDirection = _getTextDirection(context)
..onTap = onTap
..onLongPress = onLongPress
..onScrollLeft = onScrollLeft
..onScrollRight = onScrollRight
..onScrollUp = onScrollUp
..onScrollDown = onScrollDown
..onIncrease = onIncrease
..onDecrease = onDecrease;
}
@override
......
......@@ -370,4 +370,147 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics widget supports all actions', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final List<SemanticsAction> performedActions = <SemanticsAction>[];
await tester.pumpWidget(
new Semantics(
container: true,
onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
onScrollRight: () => performedActions.add(SemanticsAction.scrollRight),
onScrollUp: () => performedActions.add(SemanticsAction.scrollUp),
onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
onIncrease: () => performedActions.add(SemanticsAction.increase),
onDecrease: () => performedActions.add(SemanticsAction.decrease),
)
);
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.showOnScreen); // showOnScreen is non user-exposed.
final int expectedId = 32;
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: allActions.fold(0, (int previous, SemanticsAction action) => previous | action.index)
),
],
);
expect(semantics, hasSemantics(expectedSemantics));
// Do the actions work?
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
int expectedLength = 1;
for (SemanticsAction action in allActions) {
semanticsOwner.performAction(expectedId, action);
expect(performedActions.length, expectedLength);
expect(performedActions.last, action);
expectedLength += 1;
}
semantics.dispose();
});
testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
int semanticsUpdateCount = 0;
tester.binding.pipelineOwner.ensureSemantics(
listener: () {
semanticsUpdateCount += 1;
}
);
final List<String> performedActions = <String>[];
await tester.pumpWidget(
new Semantics(
container: true,
onTap: () => performedActions.add('first'),
),
);
final int expectedId = 35;
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: SemanticsAction.tap.index,
),
],
);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
expect(semantics, hasSemantics(expectedSemantics));
semanticsOwner.performAction(expectedId, SemanticsAction.tap);
expect(semanticsUpdateCount, 1);
expect(performedActions, <String>['first']);
semanticsUpdateCount = 0;
performedActions.clear();
// Updating existing handler should not trigger semantics update
await tester.pumpWidget(
new Semantics(
container: true,
onTap: () => performedActions.add('second'),
),
);
expect(semantics, hasSemantics(expectedSemantics));
semanticsOwner.performAction(expectedId, SemanticsAction.tap);
expect(semanticsUpdateCount, 0);
expect(performedActions, <String>['second']);
semanticsUpdateCount = 0;
performedActions.clear();
// Adding a handler works
await tester.pumpWidget(
new Semantics(
container: true,
onTap: () => performedActions.add('second'),
onLongPress: () => performedActions.add('longPress'),
),
);
final TestSemantics expectedSemanticsWithLongPress = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: SemanticsAction.tap.index | SemanticsAction.longPress.index,
),
],
);
expect(semantics, hasSemantics(expectedSemanticsWithLongPress));
semanticsOwner.performAction(expectedId, SemanticsAction.longPress);
expect(semanticsUpdateCount, 1);
expect(performedActions, <String>['longPress']);
semanticsUpdateCount = 0;
performedActions.clear();
// Removing a handler works
await tester.pumpWidget(
new Semantics(
container: true,
onTap: () => performedActions.add('second'),
),
);
expect(semantics, hasSemantics(expectedSemantics));
expect(semanticsUpdateCount, 1);
semantics.dispose();
});
}
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