Unverified Commit 948523b8 authored by Bartek Pacia's avatar Bartek Pacia Committed by GitHub

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`
parent 4252aa0f
......@@ -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,
......
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