Unverified Commit 55c7e6e3 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Support customizing standard accessibility action hints on Android. (#19665)

parent 651c5ab3
...@@ -3207,6 +3207,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3207,6 +3207,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
String increasedValue, String increasedValue,
String decreasedValue, String decreasedValue,
String hint, String hint,
SemanticsHintOverrides hintOverrides,
TextDirection textDirection, TextDirection textDirection,
SemanticsSortKey sortKey, SemanticsSortKey sortKey,
VoidCallback onTap, VoidCallback onTap,
...@@ -3252,6 +3253,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3252,6 +3253,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_increasedValue = increasedValue, _increasedValue = increasedValue,
_decreasedValue = decreasedValue, _decreasedValue = decreasedValue,
_hint = hint, _hint = hint,
_hintOverrides = hintOverrides,
_textDirection = textDirection, _textDirection = textDirection,
_sortKey = sortKey, _sortKey = sortKey,
_onTap = onTap, _onTap = onTap,
...@@ -3548,6 +3550,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3548,6 +3550,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.hintOverride] to the given value.
SemanticsHintOverrides get hintOverrides => _hintOverrides;
SemanticsHintOverrides _hintOverrides;
set hintOverrides(SemanticsHintOverrides value) {
if (_hintOverrides == value)
return;
_hintOverrides = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value. /// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value.
/// ///
/// This must not be null if [label], [hint], [value], [increasedValue], or /// This must not be null if [label], [hint], [value], [increasedValue], or
...@@ -3989,6 +4001,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3989,6 +4001,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.decreasedValue = decreasedValue; config.decreasedValue = decreasedValue;
if (hint != null) if (hint != null)
config.hint = hint; config.hint = hint;
if (hintOverrides != null && hintOverrides.isNotEmpty)
config.hintOverrides = hintOverrides;
if (scopesRoute != null) if (scopesRoute != null)
config.scopesRoute = scopesRoute; config.scopesRoute = scopesRoute;
if (namesRoute != null) if (namesRoute != null)
......
...@@ -5098,6 +5098,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5098,6 +5098,8 @@ class Semantics extends SingleChildRenderObjectWidget {
String increasedValue, String increasedValue,
String decreasedValue, String decreasedValue,
String hint, String hint,
String onTapHint,
String onLongPressHint,
TextDirection textDirection, TextDirection textDirection,
SemanticsSortKey sortKey, SemanticsSortKey sortKey,
VoidCallback onTap, VoidCallback onTap,
...@@ -5165,6 +5167,11 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5165,6 +5167,11 @@ class Semantics extends SingleChildRenderObjectWidget {
onDismiss: onDismiss, onDismiss: onDismiss,
onSetSelection: onSetSelection, onSetSelection: onSetSelection,
customSemanticsActions: customSemanticsActions, customSemanticsActions: customSemanticsActions,
hintOverrides: onTapHint != null || onLongPressHint != null ?
new SemanticsHintOverrides(
onTapHint: onTapHint,
onLongPressHint: onLongPressHint,
) : null,
), ),
); );
...@@ -5248,6 +5255,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5248,6 +5255,7 @@ class Semantics extends SingleChildRenderObjectWidget {
increasedValue: properties.increasedValue, increasedValue: properties.increasedValue,
decreasedValue: properties.decreasedValue, decreasedValue: properties.decreasedValue,
hint: properties.hint, hint: properties.hint,
hintOverrides: properties.hintOverrides,
textDirection: _getTextDirection(context), textDirection: _getTextDirection(context),
sortKey: properties.sortKey, sortKey: properties.sortKey,
onTap: properties.onTap, onTap: properties.onTap,
...@@ -5308,6 +5316,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5308,6 +5316,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..increasedValue = properties.increasedValue ..increasedValue = properties.increasedValue
..decreasedValue = properties.decreasedValue ..decreasedValue = properties.decreasedValue
..hint = properties.hint ..hint = properties.hint
..hintOverrides = properties.hintOverrides
..namesRoute = properties.namesRoute ..namesRoute = properties.namesRoute
..textDirection = _getTextDirection(context) ..textDirection = _getTextDirection(context)
..sortKey = properties.sortKey ..sortKey = properties.sortKey
......
...@@ -634,6 +634,51 @@ void main() { ...@@ -634,6 +634,51 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpWidget(new Semantics(
container: true,
onTap: () {},
onTapHint: 'test',
));
expect(tester.getSemanticsData(find.byType(Semantics)), matchesSemanticsData(
hasTapAction: true,
onTapHint: 'test'
));
await tester.pumpWidget(new Semantics(
container: true,
onLongPress: () {},
onLongPressHint: 'foo',
));
expect(tester.getSemanticsData(find.byType(Semantics)), matchesSemanticsData(
hasLongPressAction: true,
onLongPressHint: 'foo'
));
semantics.dispose();
});
testWidgets('CustomSemanticsActions can be added to a Semantics widget', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpWidget(new Semantics(
container: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () {},
const CustomSemanticsAction(label: 'bar'): () {}
},
));
expect(tester.getSemanticsData(find.byType(Semantics)), matchesSemanticsData(
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'bar'),
const CustomSemanticsAction(label: 'foo'),
],
));
semantics.dispose();
});
testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async { testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
......
...@@ -345,8 +345,11 @@ Matcher matchesSemanticsData({ ...@@ -345,8 +345,11 @@ Matcher matchesSemanticsData({
bool hasPasteAction = false, bool hasPasteAction = false,
bool hasDidGainAccessibilityFocusAction = false, bool hasDidGainAccessibilityFocusAction = false,
bool hasDidLoseAccessibilityFocusAction = false, bool hasDidLoseAccessibilityFocusAction = false,
bool hasCustomAction = false,
bool hasDismissAction = false, bool hasDismissAction = false,
// Custom actions and overrides
String onTapHint,
String onLongPressHint,
List<CustomSemanticsAction> customActions,
}) { }) {
final List<SemanticsFlag> flags = <SemanticsFlag>[]; final List<SemanticsFlag> flags = <SemanticsFlag>[];
if (hasCheckedState) if (hasCheckedState)
...@@ -421,7 +424,7 @@ Matcher matchesSemanticsData({ ...@@ -421,7 +424,7 @@ Matcher matchesSemanticsData({
actions.add(SemanticsAction.didGainAccessibilityFocus); actions.add(SemanticsAction.didGainAccessibilityFocus);
if (hasDidLoseAccessibilityFocusAction) if (hasDidLoseAccessibilityFocusAction)
actions.add(SemanticsAction.didLoseAccessibilityFocus); actions.add(SemanticsAction.didLoseAccessibilityFocus);
if (hasCustomAction) if (customActions != null && customActions.isNotEmpty)
actions.add(SemanticsAction.customAction); actions.add(SemanticsAction.customAction);
if (hasDismissAction) if (hasDismissAction)
actions.add(SemanticsAction.dismiss); actions.add(SemanticsAction.dismiss);
...@@ -429,6 +432,12 @@ Matcher matchesSemanticsData({ ...@@ -429,6 +432,12 @@ Matcher matchesSemanticsData({
actions.add(SemanticsAction.moveCursorForwardByWord); actions.add(SemanticsAction.moveCursorForwardByWord);
if (hasMoveCursorBackwardByWordAction) if (hasMoveCursorBackwardByWordAction)
actions.add(SemanticsAction.moveCursorBackwardByWord); actions.add(SemanticsAction.moveCursorBackwardByWord);
SemanticsHintOverrides hintOverrides;
if (onTapHint != null || onLongPressHint != null)
hintOverrides = new SemanticsHintOverrides(
onTapHint: onTapHint,
onLongPressHint: onLongPressHint,
);
return new _MatchesSemanticsData( return new _MatchesSemanticsData(
label: label, label: label,
...@@ -438,6 +447,8 @@ Matcher matchesSemanticsData({ ...@@ -438,6 +447,8 @@ Matcher matchesSemanticsData({
flags: flags, flags: flags,
textDirection: textDirection, textDirection: textDirection,
rect: rect, rect: rect,
customActions: customActions,
hintOverrides: hintOverrides,
); );
} }
...@@ -1467,12 +1478,16 @@ class _MatchesSemanticsData extends Matcher { ...@@ -1467,12 +1478,16 @@ class _MatchesSemanticsData extends Matcher {
this.actions, this.actions,
this.textDirection, this.textDirection,
this.rect, this.rect,
this.customActions,
this.hintOverrides,
}); });
final String label; final String label;
final String value; final String value;
final String hint; final String hint;
final SemanticsHintOverrides hintOverrides;
final List<SemanticsAction> actions; final List<SemanticsAction> actions;
final List<CustomSemanticsAction> customActions;
final List<SemanticsFlag> flags; final List<SemanticsFlag> flags;
final TextDirection textDirection; final TextDirection textDirection;
final Rect rect; final Rect rect;
...@@ -1494,6 +1509,10 @@ class _MatchesSemanticsData extends Matcher { ...@@ -1494,6 +1509,10 @@ class _MatchesSemanticsData extends Matcher {
description.add('with textDirection: $textDirection '); description.add('with textDirection: $textDirection ');
if (rect != null) if (rect != null)
description.add('with rect: $rect'); description.add('with rect: $rect');
if (customActions != null)
description.add('with custom actions: $customActions');
if (hintOverrides != null)
description.add('with custom hints: $hintOverrides');
return description; return description;
} }
...@@ -1511,7 +1530,7 @@ class _MatchesSemanticsData extends Matcher { ...@@ -1511,7 +1530,7 @@ class _MatchesSemanticsData extends Matcher {
return failWithDescription(matchState, 'value was: ${data.value}'); return failWithDescription(matchState, 'value was: ${data.value}');
if (textDirection != null && textDirection != data.textDirection) if (textDirection != null && textDirection != data.textDirection)
return failWithDescription(matchState, 'textDirection was: $textDirection'); return failWithDescription(matchState, 'textDirection was: $textDirection');
if (rect != null && rect == data.rect) { if (rect != null && rect != data.rect) {
return failWithDescription(matchState, 'rect was: $rect'); return failWithDescription(matchState, 'rect was: $rect');
} }
if (actions != null) { if (actions != null) {
...@@ -1527,6 +1546,27 @@ class _MatchesSemanticsData extends Matcher { ...@@ -1527,6 +1546,27 @@ class _MatchesSemanticsData extends Matcher {
return failWithDescription(matchState, 'actions were: $actionSummary'); return failWithDescription(matchState, 'actions were: $actionSummary');
} }
} }
if (customActions != null || hintOverrides != null) {
final List<CustomSemanticsAction> providedCustomActions = data.customSemanticsActionIds.map((int id) {
return CustomSemanticsAction.getAction(id);
}).toList();
final List<CustomSemanticsAction> expectedCustomActions = new List<CustomSemanticsAction>.from(customActions ?? const <int>[]);
if (hintOverrides?.onTapHint != null)
expectedCustomActions.add(new CustomSemanticsAction.overridingAction(hint: hintOverrides.onTapHint, action: SemanticsAction.tap));
if (hintOverrides?.onLongPressHint != null)
expectedCustomActions.add(new CustomSemanticsAction.overridingAction(hint: hintOverrides.onLongPressHint, action: SemanticsAction.longPress));
if (expectedCustomActions.length != providedCustomActions.length)
return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
int sortActions(CustomSemanticsAction left, CustomSemanticsAction right) {
return CustomSemanticsAction.getIdentifier(left) - CustomSemanticsAction.getIdentifier(right);
}
expectedCustomActions.sort(sortActions);
providedCustomActions.sort(sortActions);
for (int i = 0; i < expectedCustomActions.length; i++) {
if (expectedCustomActions[i] != providedCustomActions[i])
return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
}
}
if (flags != null) { if (flags != null) {
int flagBits = 0; int flagBits = 0;
for (SemanticsFlag flag in flags) for (SemanticsFlag flag in flags)
......
...@@ -394,10 +394,17 @@ void main() { ...@@ -394,10 +394,17 @@ void main() {
header: true, header: true,
button: true, button: true,
onTap: () {}, onTap: () {},
onLongPress: () {},
label: 'foo', label: 'foo',
hint: 'bar', hint: 'bar',
value: 'baz', value: 'baz',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
onTapHint: 'scan',
onLongPressHint: 'fill',
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () {},
const CustomSemanticsAction(label: 'bar'): () {},
},
)); ));
expect(tester.getSemanticsData(find.byKey(key)), expect(tester.getSemanticsData(find.byKey(key)),
...@@ -407,17 +414,68 @@ void main() { ...@@ -407,17 +414,68 @@ void main() {
value: 'baz', value: 'baz',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
hasTapAction: true, hasTapAction: true,
hasLongPressAction: true,
isButton: true, isButton: true,
isHeader: true, isHeader: true,
namesRoute: true, namesRoute: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar')
],
), ),
); );
// Doesn't match custom actions
expect(tester.getSemanticsData(find.byKey(key)),
isNot(matchesSemanticsData(
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scan',
onLongPressHint: 'fill',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'barz')
],
)),
);
// Doesn't match wrong hints
expect(tester.getSemanticsData(find.byKey(key)),
isNot(matchesSemanticsData(
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
hasTapAction: true,
hasLongPressAction: true,
isButton: true,
isHeader: true,
namesRoute: true,
onTapHint: 'scans',
onLongPressHint: 'fills',
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'foo'),
const CustomSemanticsAction(label: 'bar')
],
)),
);
handle.dispose(); handle.dispose();
}); });
testWidgets('Can match all semantics flags and actions', (WidgetTester tester) async { testWidgets('Can match all semantics flags and actions', (WidgetTester tester) async {
int actions = 0; int actions = 0;
int flags = 0; int flags = 0;
const CustomSemanticsAction action = const CustomSemanticsAction(label: 'test');
for (int index in SemanticsAction.values.keys) for (int index in SemanticsAction.values.keys)
actions |= index; actions |= index;
for (int index in SemanticsFlag.values.keys) for (int index in SemanticsFlag.values.keys)
...@@ -436,6 +494,7 @@ void main() { ...@@ -436,6 +494,7 @@ void main() {
scrollPosition: null, scrollPosition: null,
scrollExtentMax: null, scrollExtentMax: null,
scrollExtentMin: null, scrollExtentMin: null,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
); );
expect(data, matchesSemanticsData( expect(data, matchesSemanticsData(
...@@ -478,8 +537,8 @@ void main() { ...@@ -478,8 +537,8 @@ void main() {
hasPasteAction: true, hasPasteAction: true,
hasDidGainAccessibilityFocusAction: true, hasDidGainAccessibilityFocusAction: true,
hasDidLoseAccessibilityFocusAction: true, hasDidLoseAccessibilityFocusAction: true,
hasCustomAction: true,
hasDismissAction: true, hasDismissAction: true,
customActions: <CustomSemanticsAction>[action],
)); ));
}); });
}); });
......
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