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 {
String increasedValue,
String decreasedValue,
String hint,
SemanticsHintOverrides hintOverrides,
TextDirection textDirection,
SemanticsSortKey sortKey,
VoidCallback onTap,
......@@ -3252,6 +3253,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_increasedValue = increasedValue,
_decreasedValue = decreasedValue,
_hint = hint,
_hintOverrides = hintOverrides,
_textDirection = textDirection,
_sortKey = sortKey,
_onTap = onTap,
......@@ -3548,6 +3550,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
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.
///
/// This must not be null if [label], [hint], [value], [increasedValue], or
......@@ -3989,6 +4001,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.decreasedValue = decreasedValue;
if (hint != null)
config.hint = hint;
if (hintOverrides != null && hintOverrides.isNotEmpty)
config.hintOverrides = hintOverrides;
if (scopesRoute != null)
config.scopesRoute = scopesRoute;
if (namesRoute != null)
......
......@@ -5098,6 +5098,8 @@ class Semantics extends SingleChildRenderObjectWidget {
String increasedValue,
String decreasedValue,
String hint,
String onTapHint,
String onLongPressHint,
TextDirection textDirection,
SemanticsSortKey sortKey,
VoidCallback onTap,
......@@ -5165,6 +5167,11 @@ class Semantics extends SingleChildRenderObjectWidget {
onDismiss: onDismiss,
onSetSelection: onSetSelection,
customSemanticsActions: customSemanticsActions,
hintOverrides: onTapHint != null || onLongPressHint != null ?
new SemanticsHintOverrides(
onTapHint: onTapHint,
onLongPressHint: onLongPressHint,
) : null,
),
);
......@@ -5248,6 +5255,7 @@ class Semantics extends SingleChildRenderObjectWidget {
increasedValue: properties.increasedValue,
decreasedValue: properties.decreasedValue,
hint: properties.hint,
hintOverrides: properties.hintOverrides,
textDirection: _getTextDirection(context),
sortKey: properties.sortKey,
onTap: properties.onTap,
......@@ -5308,6 +5316,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..increasedValue = properties.increasedValue
..decreasedValue = properties.decreasedValue
..hint = properties.hint
..hintOverrides = properties.hintOverrides
..namesRoute = properties.namesRoute
..textDirection = _getTextDirection(context)
..sortKey = properties.sortKey
......
......@@ -634,6 +634,51 @@ void main() {
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 {
final SemanticsTester semantics = new SemanticsTester(tester);
......
......@@ -345,8 +345,11 @@ Matcher matchesSemanticsData({
bool hasPasteAction = false,
bool hasDidGainAccessibilityFocusAction = false,
bool hasDidLoseAccessibilityFocusAction = false,
bool hasCustomAction = false,
bool hasDismissAction = false,
// Custom actions and overrides
String onTapHint,
String onLongPressHint,
List<CustomSemanticsAction> customActions,
}) {
final List<SemanticsFlag> flags = <SemanticsFlag>[];
if (hasCheckedState)
......@@ -421,7 +424,7 @@ Matcher matchesSemanticsData({
actions.add(SemanticsAction.didGainAccessibilityFocus);
if (hasDidLoseAccessibilityFocusAction)
actions.add(SemanticsAction.didLoseAccessibilityFocus);
if (hasCustomAction)
if (customActions != null && customActions.isNotEmpty)
actions.add(SemanticsAction.customAction);
if (hasDismissAction)
actions.add(SemanticsAction.dismiss);
......@@ -429,6 +432,12 @@ Matcher matchesSemanticsData({
actions.add(SemanticsAction.moveCursorForwardByWord);
if (hasMoveCursorBackwardByWordAction)
actions.add(SemanticsAction.moveCursorBackwardByWord);
SemanticsHintOverrides hintOverrides;
if (onTapHint != null || onLongPressHint != null)
hintOverrides = new SemanticsHintOverrides(
onTapHint: onTapHint,
onLongPressHint: onLongPressHint,
);
return new _MatchesSemanticsData(
label: label,
......@@ -438,6 +447,8 @@ Matcher matchesSemanticsData({
flags: flags,
textDirection: textDirection,
rect: rect,
customActions: customActions,
hintOverrides: hintOverrides,
);
}
......@@ -1467,12 +1478,16 @@ class _MatchesSemanticsData extends Matcher {
this.actions,
this.textDirection,
this.rect,
this.customActions,
this.hintOverrides,
});
final String label;
final String value;
final String hint;
final SemanticsHintOverrides hintOverrides;
final List<SemanticsAction> actions;
final List<CustomSemanticsAction> customActions;
final List<SemanticsFlag> flags;
final TextDirection textDirection;
final Rect rect;
......@@ -1494,6 +1509,10 @@ class _MatchesSemanticsData extends Matcher {
description.add('with textDirection: $textDirection ');
if (rect != null)
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;
}
......@@ -1511,7 +1530,7 @@ class _MatchesSemanticsData extends Matcher {
return failWithDescription(matchState, 'value was: ${data.value}');
if (textDirection != null && textDirection != data.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');
}
if (actions != null) {
......@@ -1527,6 +1546,27 @@ class _MatchesSemanticsData extends Matcher {
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) {
int flagBits = 0;
for (SemanticsFlag flag in flags)
......
......@@ -394,10 +394,17 @@ void main() {
header: true,
button: true,
onTap: () {},
onLongPress: () {},
label: 'foo',
hint: 'bar',
value: 'baz',
textDirection: TextDirection.rtl,
onTapHint: 'scan',
onLongPressHint: 'fill',
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () {},
const CustomSemanticsAction(label: 'bar'): () {},
},
));
expect(tester.getSemanticsData(find.byKey(key)),
......@@ -407,17 +414,68 @@ void main() {
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: '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();
});
testWidgets('Can match all semantics flags and actions', (WidgetTester tester) async {
int actions = 0;
int flags = 0;
const CustomSemanticsAction action = const CustomSemanticsAction(label: 'test');
for (int index in SemanticsAction.values.keys)
actions |= index;
for (int index in SemanticsFlag.values.keys)
......@@ -436,6 +494,7 @@ void main() {
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
);
expect(data, matchesSemanticsData(
......@@ -478,8 +537,8 @@ void main() {
hasPasteAction: true,
hasDidGainAccessibilityFocusAction: true,
hasDidLoseAccessibilityFocusAction: true,
hasCustomAction: 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