Add accessibility identifier to SemanticsProperties
(#138331)
This PR adds `String? identifier` to `Semantics` and `SemanticsProperties`. The `identifier` will be exposed on Android as `resource-id` and on iOS as `accessibilityIdentifier`. Mainly targeted at #17988 Initial Engine PR with Android support: https://github.com/flutter/engine/pull/47961 iOS Engine PR: https://github.com/flutter/engine/pull/48858 ### Migration This change breaks the SemanticsUpdateBuilder API which is on the Framework<-->Engine border. For more details see [engine PR](https://github.com/flutter/engine/pull/47961). Steps: part 1: [engine] add `SemanticsUpdateBuilderNew` https://github.com/flutter/engine/pull/47961 **part 2: [flutter] use `SemanticsUpdateBuilderNew`** <-- we are here part 3: [engine] update `SemanticsUpdateBuilder` to be the same as `SemanticsUpdateBuilderNew`* part 4: [flutter] use (now updated) `SemanticsUpdateBuilder` again. part 5: [engine] remove `SemanticsBuilderNew`
This commit is contained in:
parent
4252aa0fff
commit
948523b80c
@ -4356,6 +4356,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
if (_properties.image != null) {
|
||||
config.isImage = _properties.image!;
|
||||
}
|
||||
if (_properties.identifier != null) {
|
||||
config.identifier = _properties.identifier!;
|
||||
}
|
||||
if (_attributedLabel != null) {
|
||||
config.attributedLabel = _attributedLabel!;
|
||||
}
|
||||
|
@ -2,14 +2,16 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;
|
||||
// ignore: deprecated_member_use
|
||||
import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilderNew;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'debug.dart';
|
||||
|
||||
export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;
|
||||
// ignore: deprecated_member_use
|
||||
export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilderNew;
|
||||
|
||||
/// The glue between the semantics layer and the Flutter engine.
|
||||
mixin SemanticsBinding on BindingBase {
|
||||
@ -160,8 +162,10 @@ mixin SemanticsBinding on BindingBase {
|
||||
///
|
||||
/// This method is used by the [SemanticsOwner] to create builder for all its
|
||||
/// semantics updates.
|
||||
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
|
||||
return ui.SemanticsUpdateBuilder();
|
||||
// ignore: deprecated_member_use
|
||||
ui.SemanticsUpdateBuilderNew createSemanticsUpdateBuilder() {
|
||||
// ignore: deprecated_member_use
|
||||
return ui.SemanticsUpdateBuilderNew();
|
||||
}
|
||||
|
||||
/// The platform is requesting that animations be disabled or simplified.
|
||||
|
@ -3,7 +3,8 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilder, StringAttribute, TextDirection;
|
||||
// ignore: deprecated_member_use
|
||||
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilderNew, StringAttribute, TextDirection;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -426,6 +427,7 @@ class SemanticsData with Diagnosticable {
|
||||
SemanticsData({
|
||||
required this.flags,
|
||||
required this.actions,
|
||||
required this.identifier,
|
||||
required this.attributedLabel,
|
||||
required this.attributedValue,
|
||||
required this.attributedIncreasedValue,
|
||||
@ -461,6 +463,9 @@ class SemanticsData with Diagnosticable {
|
||||
/// A bit field of [SemanticsAction]s that apply to this node.
|
||||
final int actions;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.identifier}
|
||||
final String identifier;
|
||||
|
||||
/// A textual description for the current label of the node.
|
||||
///
|
||||
/// The reading direction is given by [textDirection].
|
||||
@ -696,6 +701,7 @@ class SemanticsData with Diagnosticable {
|
||||
flag.name,
|
||||
];
|
||||
properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null));
|
||||
properties.add(StringProperty('identifier', identifier, defaultValue: ''));
|
||||
properties.add(AttributedStringProperty('label', attributedLabel));
|
||||
properties.add(AttributedStringProperty('value', attributedValue));
|
||||
properties.add(AttributedStringProperty('increasedValue', attributedIncreasedValue));
|
||||
@ -721,6 +727,7 @@ class SemanticsData with Diagnosticable {
|
||||
return other is SemanticsData
|
||||
&& other.flags == flags
|
||||
&& other.actions == actions
|
||||
&& other.identifier == identifier
|
||||
&& other.attributedLabel == attributedLabel
|
||||
&& other.attributedValue == attributedValue
|
||||
&& other.attributedIncreasedValue == attributedIncreasedValue
|
||||
@ -749,6 +756,7 @@ class SemanticsData with Diagnosticable {
|
||||
int get hashCode => Object.hash(
|
||||
flags,
|
||||
actions,
|
||||
identifier,
|
||||
attributedLabel,
|
||||
attributedValue,
|
||||
attributedIncreasedValue,
|
||||
@ -765,8 +773,8 @@ class SemanticsData with Diagnosticable {
|
||||
scrollExtentMax,
|
||||
scrollExtentMin,
|
||||
platformViewId,
|
||||
maxValueLength,
|
||||
Object.hash(
|
||||
maxValueLength,
|
||||
currentValueLength,
|
||||
transform,
|
||||
elevation,
|
||||
@ -901,6 +909,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
this.liveRegion,
|
||||
this.maxValueLength,
|
||||
this.currentValueLength,
|
||||
this.identifier,
|
||||
this.label,
|
||||
this.attributedLabel,
|
||||
this.value,
|
||||
@ -1165,6 +1174,21 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// [maxValueLength] is set.
|
||||
final int? currentValueLength;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsProperties.identifier}
|
||||
/// Provides an identifier for the semantics node in native accessibility hierarchy.
|
||||
///
|
||||
/// This value is not exposed to the users of the app.
|
||||
///
|
||||
/// It's usually used for UI testing with tools that work by querying the
|
||||
/// native accessibility, like UIAutomator, XCUITest, or Appium.
|
||||
///
|
||||
/// On Android, this is used for `AccessibilityNodeInfo.setViewIdResourceName`.
|
||||
/// It'll be appear in accessibility hierarchy as `resource-id`.
|
||||
///
|
||||
/// On iOS, this will set `UIAccessibilityElement.accessibilityIdentifier`.
|
||||
/// {@endtemplate}
|
||||
final String? identifier;
|
||||
|
||||
/// Provides a textual description of the widget.
|
||||
///
|
||||
/// If a label is provided, there must either by an ambient [Directionality]
|
||||
@ -1632,6 +1656,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
properties.add(DiagnosticsProperty<bool>('mixed', mixed, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<bool>('expanded', expanded, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
|
||||
properties.add(StringProperty('identifier', identifier));
|
||||
properties.add(StringProperty('label', label, defaultValue: null));
|
||||
properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null));
|
||||
properties.add(StringProperty('value', value, defaultValue: null));
|
||||
@ -2210,6 +2235,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
/// Whether this node currently has a given [SemanticsFlag].
|
||||
bool hasFlag(SemanticsFlag flag) => _flags & flag.index != 0;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.identifier}
|
||||
String get identifier => _identifier;
|
||||
String _identifier = _kEmptyConfig.identifier;
|
||||
|
||||
/// A textual description of this node.
|
||||
///
|
||||
/// The reading direction is given by [textDirection].
|
||||
@ -2514,6 +2543,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
|
||||
final bool mergeAllDescendantsIntoThisNodeValueChanged = _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
|
||||
|
||||
_identifier = config.identifier;
|
||||
_attributedLabel = config.attributedLabel;
|
||||
_attributedValue = config.attributedValue;
|
||||
_attributedIncreasedValue = config.attributedIncreasedValue;
|
||||
@ -2569,6 +2599,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
// Can't use _effectiveActionsAsBits here. The filtering of action bits
|
||||
// must be done after the merging the its descendants.
|
||||
int actions = _actionsAsBits;
|
||||
String identifier = _identifier;
|
||||
AttributedString attributedLabel = _attributedLabel;
|
||||
AttributedString attributedValue = _attributedValue;
|
||||
AttributedString attributedIncreasedValue = _attributedIncreasedValue;
|
||||
@ -2625,6 +2656,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
platformViewId ??= node._platformViewId;
|
||||
maxValueLength ??= node._maxValueLength;
|
||||
currentValueLength ??= node._currentValueLength;
|
||||
if (identifier == '') {
|
||||
identifier = node._identifier;
|
||||
}
|
||||
if (attributedValue.string == '') {
|
||||
attributedValue = node._attributedValue;
|
||||
}
|
||||
@ -2682,6 +2716,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
return SemanticsData(
|
||||
flags: flags,
|
||||
actions: _areUserActionsBlocked ? actions & _kUnblockedUserActions : actions,
|
||||
identifier: identifier,
|
||||
attributedLabel: attributedLabel,
|
||||
attributedValue: attributedValue,
|
||||
attributedIncreasedValue: attributedIncreasedValue,
|
||||
@ -2715,7 +2750,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0);
|
||||
static final Float64List _kIdentityTransform = _initIdentityTransform();
|
||||
|
||||
void _addToUpdate(SemanticsUpdateBuilder builder, Set<int> customSemanticsActionIdsUpdate) {
|
||||
// ignore: deprecated_member_use
|
||||
void _addToUpdate(SemanticsUpdateBuilderNew builder, Set<int> customSemanticsActionIdsUpdate) {
|
||||
assert(_dirty);
|
||||
final SemanticsData data = getSemanticsData();
|
||||
final Int32List childrenInTraversalOrder;
|
||||
@ -2750,6 +2786,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
flags: data.flags,
|
||||
actions: data.actions,
|
||||
rect: data.rect,
|
||||
identifier: data.identifier,
|
||||
label: data.attributedLabel.string,
|
||||
labelAttributes: data.attributedLabel.attributes,
|
||||
value: data.attributedValue.string,
|
||||
@ -2904,6 +2941,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
properties.add(IterableProperty<String>('flags', flags, ifEmpty: null));
|
||||
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
|
||||
properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
|
||||
properties.add(StringProperty('identifier', _identifier, defaultValue: ''));
|
||||
properties.add(AttributedStringProperty('label', _attributedLabel));
|
||||
properties.add(AttributedStringProperty('value', _attributedValue));
|
||||
properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue));
|
||||
@ -3406,7 +3444,8 @@ class SemanticsOwner extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
|
||||
final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
|
||||
// ignore: deprecated_member_use
|
||||
final SemanticsUpdateBuilderNew builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
|
||||
for (final SemanticsNode node in visitedNodes) {
|
||||
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
|
||||
// The _serialize() method marks the node as not dirty, and
|
||||
@ -4201,6 +4240,14 @@ class SemanticsConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.identifier}
|
||||
String get identifier => _identifier;
|
||||
String _identifier = '';
|
||||
set identifier(String identifier) {
|
||||
_identifier = identifier;
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// A textual description of the owning [RenderObject].
|
||||
///
|
||||
/// Setting this attribute will override the [attributedLabel].
|
||||
@ -4898,6 +4945,9 @@ class SemanticsConfiguration {
|
||||
|
||||
textDirection ??= child.textDirection;
|
||||
_sortKey ??= child._sortKey;
|
||||
if (_identifier == '') {
|
||||
_identifier = child._identifier;
|
||||
}
|
||||
_attributedLabel = _concatAttributedString(
|
||||
thisAttributedString: _attributedLabel,
|
||||
thisTextDirection: textDirection,
|
||||
@ -4938,6 +4988,7 @@ class SemanticsConfiguration {
|
||||
.._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants
|
||||
.._textDirection = _textDirection
|
||||
.._sortKey = _sortKey
|
||||
.._identifier = _identifier
|
||||
.._attributedLabel = _attributedLabel
|
||||
.._attributedIncreasedValue = _attributedIncreasedValue
|
||||
.._attributedValue = _attributedValue
|
||||
|
@ -7123,6 +7123,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
bool? expanded,
|
||||
int? maxValueLength,
|
||||
int? currentValueLength,
|
||||
String? identifier,
|
||||
String? label,
|
||||
AttributedString? attributedLabel,
|
||||
String? value,
|
||||
@ -7191,6 +7192,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
liveRegion: liveRegion,
|
||||
maxValueLength: maxValueLength,
|
||||
currentValueLength: currentValueLength,
|
||||
identifier: identifier,
|
||||
label: label,
|
||||
attributedLabel: attributedLabel,
|
||||
value: value,
|
||||
|
@ -682,6 +682,7 @@ void main() {
|
||||
' flags: []\n'
|
||||
' invisible\n'
|
||||
' isHidden: false\n'
|
||||
' identifier: ""\n'
|
||||
' label: ""\n'
|
||||
' value: ""\n'
|
||||
' increasedValue: ""\n'
|
||||
@ -805,6 +806,7 @@ void main() {
|
||||
' flags: []\n'
|
||||
' invisible\n'
|
||||
' isHidden: false\n'
|
||||
' identifier: ""\n'
|
||||
' label: ""\n'
|
||||
' value: ""\n'
|
||||
' increasedValue: ""\n'
|
||||
|
@ -157,6 +157,7 @@ void main() {
|
||||
'Semantics('
|
||||
'container: false, '
|
||||
'properties: SemanticsProperties, '
|
||||
'identifier: null, '// ignore: missing_whitespace_between_adjacent_strings
|
||||
'attributedLabel: "label" [SpellOutStringAttribute(TextRange(start: 0, end: 5))], '
|
||||
'attributedValue: "value" [LocaleStringAttribute(TextRange(start: 0, end: 5), en-MX)], '
|
||||
'attributedHint: "hint" [SpellOutStringAttribute(TextRange(start: 1, end: 2))], '
|
||||
@ -171,13 +172,16 @@ void main() {
|
||||
|
||||
class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding {
|
||||
@override
|
||||
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
|
||||
// ignore: deprecated_member_use
|
||||
ui.SemanticsUpdateBuilderNew createSemanticsUpdateBuilder() {
|
||||
return SemanticsUpdateBuilderSpy();
|
||||
}
|
||||
}
|
||||
|
||||
class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilder {
|
||||
final SemanticsUpdateBuilder _builder = ui.SemanticsUpdateBuilder();
|
||||
// ignore: deprecated_member_use
|
||||
class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilderNew {
|
||||
// ignore: deprecated_member_use
|
||||
final SemanticsUpdateBuilderNew _builder = ui.SemanticsUpdateBuilderNew();
|
||||
|
||||
static Map<int, SemanticsNodeUpdateObservation> observations = <int, SemanticsNodeUpdateObservation>{};
|
||||
|
||||
@ -199,6 +203,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
|
||||
required double elevation,
|
||||
required double thickness,
|
||||
required Rect rect,
|
||||
required String identifier,
|
||||
required String label,
|
||||
List<StringAttribute>? labelAttributes,
|
||||
required String value,
|
||||
|
@ -624,6 +624,7 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
|
||||
/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics.
|
||||
/// * [containsSemantics], a similar matcher without default values for flags or actions.
|
||||
Matcher matchesSemantics({
|
||||
String? identifier,
|
||||
String? label,
|
||||
AttributedString? attributedLabel,
|
||||
String? hint,
|
||||
@ -701,6 +702,7 @@ Matcher matchesSemantics({
|
||||
List<Matcher>? children,
|
||||
}) {
|
||||
return _MatchesSemanticsData(
|
||||
identifier: identifier,
|
||||
label: label,
|
||||
attributedLabel: attributedLabel,
|
||||
hint: hint,
|
||||
@ -808,6 +810,7 @@ Matcher matchesSemantics({
|
||||
/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics.
|
||||
/// * [matchesSemantics], a similar matcher with default values for flags and actions.
|
||||
Matcher containsSemantics({
|
||||
String? identifier,
|
||||
String? label,
|
||||
AttributedString? attributedLabel,
|
||||
String? hint,
|
||||
@ -885,6 +888,7 @@ Matcher containsSemantics({
|
||||
List<Matcher>? children,
|
||||
}) {
|
||||
return _MatchesSemanticsData(
|
||||
identifier: identifier,
|
||||
label: label,
|
||||
attributedLabel: attributedLabel,
|
||||
hint: hint,
|
||||
@ -2207,6 +2211,7 @@ class _MatchesReferenceImage extends AsyncMatcher {
|
||||
|
||||
class _MatchesSemanticsData extends Matcher {
|
||||
_MatchesSemanticsData({
|
||||
required this.identifier,
|
||||
required this.label,
|
||||
required this.attributedLabel,
|
||||
required this.hint,
|
||||
@ -2344,6 +2349,7 @@ class _MatchesSemanticsData extends Matcher {
|
||||
onLongPressHint: onLongPressHint,
|
||||
);
|
||||
|
||||
final String? identifier;
|
||||
final String? label;
|
||||
final AttributedString? attributedLabel;
|
||||
final String? hint;
|
||||
|
@ -663,6 +663,7 @@ void main() {
|
||||
final SemanticsData data = SemanticsData(
|
||||
flags: flags,
|
||||
actions: actions,
|
||||
identifier: 'i',
|
||||
attributedLabel: AttributedString('a'),
|
||||
attributedIncreasedValue: AttributedString('b'),
|
||||
attributedValue: AttributedString('c'),
|
||||
@ -790,6 +791,7 @@ void main() {
|
||||
link: true,
|
||||
onTap: () { },
|
||||
onLongPress: () { },
|
||||
identifier: 'ident',
|
||||
label: 'foo',
|
||||
hint: 'bar',
|
||||
value: 'baz',
|
||||
@ -947,6 +949,7 @@ void main() {
|
||||
final SemanticsData data = SemanticsData(
|
||||
flags: flags,
|
||||
actions: actions,
|
||||
identifier: 'i',
|
||||
attributedLabel: AttributedString('a'),
|
||||
attributedIncreasedValue: AttributedString('b'),
|
||||
attributedValue: AttributedString('c'),
|
||||
@ -1039,6 +1042,7 @@ void main() {
|
||||
final SemanticsData data = SemanticsData(
|
||||
flags: 0,
|
||||
actions: 0,
|
||||
identifier: 'i',
|
||||
attributedLabel: AttributedString('a'),
|
||||
attributedIncreasedValue: AttributedString('b'),
|
||||
attributedValue: AttributedString('c'),
|
||||
@ -1137,6 +1141,7 @@ void main() {
|
||||
final SemanticsData emptyData = SemanticsData(
|
||||
flags: 0,
|
||||
actions: 0,
|
||||
identifier: 'i',
|
||||
attributedLabel: AttributedString('a'),
|
||||
attributedIncreasedValue: AttributedString('b'),
|
||||
attributedValue: AttributedString('c'),
|
||||
@ -1163,6 +1168,7 @@ void main() {
|
||||
final SemanticsData fullData = SemanticsData(
|
||||
flags: allFlags,
|
||||
actions: allActions,
|
||||
identifier: 'i',
|
||||
attributedLabel: AttributedString('a'),
|
||||
attributedIncreasedValue: AttributedString('b'),
|
||||
attributedValue: AttributedString('c'),
|
||||
@ -1252,6 +1258,7 @@ void main() {
|
||||
final SemanticsData data = SemanticsData(
|
||||
flags: 0,
|
||||
actions: SemanticsAction.customAction.index,
|
||||
identifier: 'i',
|
||||
attributedLabel: AttributedString('a'),
|
||||
attributedIncreasedValue: AttributedString('b'),
|
||||
attributedValue: AttributedString('c'),
|
||||
|
@ -313,7 +313,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('updateSemantics is passed through to backing FlutterView', (WidgetTester tester) async {
|
||||
final SemanticsUpdate expectedUpdate = SemanticsUpdateBuilder().build();
|
||||
// ignore: deprecated_member_use
|
||||
final SemanticsUpdate expectedUpdate = SemanticsUpdateBuilderNew().build();
|
||||
final _FakeFlutterView backingView = _FakeFlutterView();
|
||||
final TestFlutterView view = TestFlutterView(
|
||||
view: backingView,
|
||||
|
Loading…
x
Reference in New Issue
Block a user