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)
......
...@@ -83,7 +83,7 @@ class SemanticsTag { ...@@ -83,7 +83,7 @@ class SemanticsTag {
/// these are presented in the radial context menu. /// these are presented in the radial context menu.
/// ///
/// Localization and text direction do not automatically apply to the provided /// Localization and text direction do not automatically apply to the provided
/// label. /// label or hint.
/// ///
/// Instances of this class should either be instantiated with const or /// Instances of this class should either be instantiated with const or
/// new instances cached in static fields. /// new instances cached in static fields.
...@@ -98,20 +98,45 @@ class CustomSemanticsAction { ...@@ -98,20 +98,45 @@ class CustomSemanticsAction {
/// The [label] must not be null or the empty string. /// The [label] must not be null or the empty string.
const CustomSemanticsAction({@required this.label}) const CustomSemanticsAction({@required this.label})
: assert(label != null), : assert(label != null),
assert(label != ''); assert(label != ''),
hint = null,
action = null;
/// The user readable name of this custom accessibility action. /// Creates a new [CustomSemanticsAction] that overrides a standard semantics
/// action.
///
/// The [hint] must not be null or the empty string.
const CustomSemanticsAction.overridingAction({@required this.hint, @required this.action})
: assert(hint != null),
assert(hint != ''),
assert(action != null),
label = null;
/// The user readable name of this custom semantics action.
final String label; final String label;
/// The hint description of this custom semantics action.
final String hint;
/// The standard semantics action this action replaces.
final SemanticsAction action;
@override @override
int get hashCode => label.hashCode; int get hashCode => ui.hashValues(label, hint, action);
@override @override
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
return false; return false;
final CustomSemanticsAction typedOther = other; final CustomSemanticsAction typedOther = other;
return typedOther.label == label; return typedOther.label == label
&& typedOther.hint == hint
&& typedOther.action == action;
}
@override
String toString() {
return 'CustomSemanticsAction(${_ids[this]}, label:$label, hint:$hint, action:$action)';
} }
// Logic to assign a unique id to each custom action without requiring // Logic to assign a unique id to each custom action without requiring
...@@ -121,7 +146,6 @@ class CustomSemanticsAction { ...@@ -121,7 +146,6 @@ class CustomSemanticsAction {
static final Map<CustomSemanticsAction, int> _ids = <CustomSemanticsAction, int>{}; static final Map<CustomSemanticsAction, int> _ids = <CustomSemanticsAction, int>{};
/// Get the identifier for a given `action`. /// Get the identifier for a given `action`.
@visibleForTesting
static int getIdentifier(CustomSemanticsAction action) { static int getIdentifier(CustomSemanticsAction action) {
int result = _ids[action]; int result = _ids[action];
if (result == null) { if (result == null) {
...@@ -133,7 +157,6 @@ class CustomSemanticsAction { ...@@ -133,7 +157,6 @@ class CustomSemanticsAction {
} }
/// Get the `action` for a given identifier. /// Get the `action` for a given identifier.
@visibleForTesting
static CustomSemanticsAction getAction(int id) { static CustomSemanticsAction getAction(int id) {
return _actions[id]; return _actions[id];
} }
...@@ -271,7 +294,8 @@ class SemanticsData extends Diagnosticable { ...@@ -271,7 +294,8 @@ class SemanticsData extends Diagnosticable {
/// parent). /// parent).
final Matrix4 transform; final Matrix4 transform;
/// The identifiers for the custom semantics action defined for this node. /// The identifiers for the custom semantics actions and standard action
/// overrides for this node.
/// ///
/// The list must be sorted in increasing order. /// The list must be sorted in increasing order.
/// ///
...@@ -407,6 +431,64 @@ class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> { ...@@ -407,6 +431,64 @@ class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
} }
} }
/// Provides hint values which override the default hints on supported
/// platforms.
///
/// On iOS, these values are always ignored.
@immutable
class SemanticsHintOverrides extends DiagnosticableTree {
/// Creates a semantics hint overrides.
const SemanticsHintOverrides({
this.onTapHint,
this.onLongPressHint,
}) : assert(onTapHint != ''),
assert(onLongPressHint != '');
/// The hint text for a tap action.
///
/// If null, the standard hint is used instead.
///
/// The hint should describe what happens when a tap occurs, not the
/// manner in which a tap is accomplished.
///
/// Bad: 'Double tap to show movies'.
/// Good: 'show movies'.
final String onTapHint;
/// The hint text for a long press action.
///
/// If null, the standard hint is used instead.
///
/// The hint should describe what happens when a long press occurs, not
/// the manner in which the long press is accomplished.
///
/// Bad: 'Double tap and hold to show tooltip'.
/// Good: 'show tooltip'.
final String onLongPressHint;
/// Whether there are any non-null hint values.
bool get isNotEmpty => onTapHint != null || onLongPressHint != null;
@override
int get hashCode => ui.hashValues(onTapHint, onLongPressHint);
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final SemanticsHintOverrides typedOther = other;
return typedOther.onTapHint == onTapHint
&& typedOther.onLongPressHint == onLongPressHint;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new StringProperty('onTapHint', onTapHint, defaultValue: null));
properties.add(new StringProperty('onLongPressHint', onLongPressHint, defaultValue: null));
}
}
/// Contains properties used by assistive technologies to make the application /// Contains properties used by assistive technologies to make the application
/// more accessible. /// more accessible.
/// ///
...@@ -436,6 +518,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -436,6 +518,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.increasedValue, this.increasedValue,
this.decreasedValue, this.decreasedValue,
this.hint, this.hint,
this.hintOverrides,
this.textDirection, this.textDirection,
this.sortKey, this.sortKey,
this.onTap, this.onTap,
...@@ -656,6 +739,16 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -656,6 +739,16 @@ class SemanticsProperties extends DiagnosticableTree {
/// in TalkBack and VoiceOver. /// in TalkBack and VoiceOver.
final String hint; final String hint;
/// Provides hint values which override the default hints on supported
/// platforms.
///
/// On Android, If no hint overrides are used then default [hint] will be
/// combined with the [label]. Otherwise, the [hint] will be ignored as long
/// as there as at least one non-null hint override.
///
/// On iOS, these are always ignored and the default [hint] is used instead.
final SemanticsHintOverrides hintOverrides;
/// The reading direction of the [label], [value], [hint], [increasedValue], /// The reading direction of the [label], [value], [hint], [increasedValue],
/// and [decreasedValue]. /// and [decreasedValue].
/// ///
...@@ -889,6 +982,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -889,6 +982,7 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(new StringProperty('hint', hint)); properties.add(new StringProperty('hint', hint));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); properties.add(new DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
properties.add(new DiagnosticsProperty<SemanticsHintOverrides>('hintOverrides', hintOverrides));
} }
@override @override
...@@ -1341,6 +1435,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1341,6 +1435,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
String get hint => _hint; String get hint => _hint;
String _hint = _kEmptyConfig.hint; String _hint = _kEmptyConfig.hint;
/// Provides hint values which override the default hints on supported
/// platforms.
SemanticsHintOverrides get hintOverrides => _hintOverrides;
SemanticsHintOverrides _hintOverrides;
/// The reading direction for [label], [value], [hint], [increasedValue], and /// The reading direction for [label], [value], [hint], [increasedValue], and
/// [decreasedValue]. /// [decreasedValue].
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
...@@ -1422,6 +1521,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1422,6 +1521,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_value = config.value; _value = config.value;
_increasedValue = config.increasedValue; _increasedValue = config.increasedValue;
_hint = config.hint; _hint = config.hint;
_hintOverrides = config.hintOverrides;
_flags = config._flags; _flags = config._flags;
_textDirection = config.textDirection; _textDirection = config.textDirection;
_sortKey = config.sortKey; _sortKey = config.sortKey;
...@@ -1468,6 +1568,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1468,6 +1568,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
final Set<int> customSemanticsActionIds = new Set<int>(); final Set<int> customSemanticsActionIds = new Set<int>();
for (CustomSemanticsAction action in _customSemanticsActions.keys) for (CustomSemanticsAction action in _customSemanticsActions.keys)
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
if (hintOverrides != null) {
if (hintOverrides.onTapHint != null) {
final CustomSemanticsAction action = new CustomSemanticsAction.overridingAction(
hint: hintOverrides.onTapHint,
action: SemanticsAction.tap,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
if (hintOverrides.onLongPressHint != null) {
final CustomSemanticsAction action = new CustomSemanticsAction.overridingAction(
hint: hintOverrides.onLongPressHint,
action: SemanticsAction.longPress,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
}
if (mergeAllDescendantsIntoThisNode) { if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) { _visitDescendants((SemanticsNode node) {
...@@ -1493,6 +1609,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1493,6 +1609,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
for (CustomSemanticsAction action in _customSemanticsActions.keys) for (CustomSemanticsAction action in _customSemanticsActions.keys)
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
} }
if (node.hintOverrides != null) {
if (node.hintOverrides.onTapHint != null) {
final CustomSemanticsAction action = new CustomSemanticsAction.overridingAction(
hint: node.hintOverrides.onTapHint,
action: SemanticsAction.tap,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
if (node.hintOverrides.onLongPressHint != null) {
final CustomSemanticsAction action = new CustomSemanticsAction.overridingAction(
hint: node.hintOverrides.onLongPressHint,
action: SemanticsAction.longPress,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
}
label = _concatStrings( label = _concatStrings(
thisString: label, thisString: label,
thisTextDirection: textDirection, thisTextDirection: textDirection,
...@@ -2139,8 +2271,10 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -2139,8 +2271,10 @@ class SemanticsOwner extends ChangeNotifier {
node._addToUpdate(builder, customSemanticsActionIds); node._addToUpdate(builder, customSemanticsActionIds);
} }
_dirtyNodes.clear(); _dirtyNodes.clear();
for (int actionId in customSemanticsActionIds) for (int actionId in customSemanticsActionIds) {
builder.updateCustomAction(id: actionId, label: CustomSemanticsAction.getAction(actionId).label); final CustomSemanticsAction action = CustomSemanticsAction.getAction(actionId);
builder.updateCustomAction(id: actionId, label: action.label, hint: action.hint, overrideId: action.action?.index ?? -1);
}
ui.window.updateSemantics(builder.build()); ui.window.updateSemantics(builder.build());
notifyListeners(); notifyListeners();
} }
...@@ -2818,6 +2952,17 @@ class SemanticsConfiguration { ...@@ -2818,6 +2952,17 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// Provides hint values which override the default hints on supported
/// platforms.
SemanticsHintOverrides get hintOverrides => _hintOverrides;
SemanticsHintOverrides _hintOverrides;
set hintOverrides(SemanticsHintOverrides value) {
if (value == null)
return;
_hintOverrides = value;
_hasBeenAnnotated = true;
}
/// Whether the semantics node is the root of a subtree for which values /// Whether the semantics node is the root of a subtree for which values
/// should be announced. /// should be announced.
/// ///
...@@ -3138,6 +3283,7 @@ class SemanticsConfiguration { ...@@ -3138,6 +3283,7 @@ class SemanticsConfiguration {
_scrollPosition ??= other._scrollPosition; _scrollPosition ??= other._scrollPosition;
_scrollExtentMax ??= other._scrollExtentMax; _scrollExtentMax ??= other._scrollExtentMax;
_scrollExtentMin ??= other._scrollExtentMin; _scrollExtentMin ??= other._scrollExtentMin;
_hintOverrides ??= other._hintOverrides;
textDirection ??= other.textDirection; textDirection ??= other.textDirection;
_sortKey ??= other._sortKey; _sortKey ??= other._sortKey;
...@@ -3178,6 +3324,7 @@ class SemanticsConfiguration { ...@@ -3178,6 +3324,7 @@ class SemanticsConfiguration {
.._value = _value .._value = _value
.._decreasedValue = _decreasedValue .._decreasedValue = _decreasedValue
.._hint = _hint .._hint = _hint
.._hintOverrides = _hintOverrides
.._flags = _flags .._flags = _flags
.._tagsForChildren = _tagsForChildren .._tagsForChildren = _tagsForChildren
.._textSelection = _textSelection .._textSelection = _textSelection
......
...@@ -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