Reland role merge (#165330)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->
the previous pr causes some issue with mobile platform.
The change is in second commit
## Pre-launch Checklist
- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index 6ca090b..ff670d4 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -5575,6 +5575,23 @@
bool _hasFlag(SemanticsFlag flag) => (_flags & flag.index) != 0;
+ bool get _hasExplicitRole {
+ if (_role != SemanticsRole.none) {
+ return true;
+ }
+ if (_hasFlag(SemanticsFlag.isTextField) ||
+ // In non web platforms, the header is a trait.
+ (_hasFlag(SemanticsFlag.isHeader) && kIsWeb) ||
+ _hasFlag(SemanticsFlag.isSlider) ||
+ _hasFlag(SemanticsFlag.isLink) ||
+ _hasFlag(SemanticsFlag.scopesRoute) ||
+ _hasFlag(SemanticsFlag.isImage) ||
+ _hasFlag(SemanticsFlag.isKeyboardKey)) {
+ return true;
+ }
+ return false;
+ }
+
// CONFIGURATION COMBINATION LOGIC
/// Whether this configuration is compatible with the provided `other`
@@ -5604,6 +5621,9 @@
if (_attributedValue.string.isNotEmpty && other._attributedValue.string.isNotEmpty) {
return false;
}
+ if (_hasExplicitRole && other._hasExplicitRole) {
+ return false;
+ }
return true;
}
diff --git a/packages/flutter/test/material/search_test.dart b/packages/flutter/test/material/search_test.dart
index aad4713..8ee6da8 100644
--- a/packages/flutter/test/material/search_test.dart
+++ b/packages/flutter/test/material/search_test.dart
@@ -651,6 +651,61 @@
final bool isCupertino =
debugDefaultTargetPlatformOverride == TargetPlatform.iOS ||
debugDefaultTargetPlatformOverride == TargetPlatform.macOS;
+ final TestSemantics textField =
+ kIsWeb
+ ? TestSemantics(
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isHeader,
+ if (!isCupertino) SemanticsFlag.namesRoute,
+ ],
+ children: <TestSemantics>[
+ TestSemantics(
+ id: 9,
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.hasEnabledState,
+ SemanticsFlag.isEnabled,
+ SemanticsFlag.isFocused,
+ ],
+ actions: <SemanticsAction>[
+ if (isDesktop) SemanticsAction.didGainAccessibilityFocus,
+ if (isDesktop) SemanticsAction.didLoseAccessibilityFocus,
+ SemanticsAction.tap,
+ SemanticsAction.focus,
+ SemanticsAction.setSelection,
+ SemanticsAction.setText,
+ SemanticsAction.paste,
+ ],
+ label: 'Search',
+ textDirection: TextDirection.ltr,
+ textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
+ ),
+ ],
+ )
+ : TestSemantics(
+ id: 9,
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.hasEnabledState,
+ SemanticsFlag.isEnabled,
+ SemanticsFlag.isFocused,
+ SemanticsFlag.isHeader,
+ if (!isCupertino) SemanticsFlag.namesRoute,
+ ],
+ actions: <SemanticsAction>[
+ if (isDesktop) SemanticsAction.didGainAccessibilityFocus,
+ if (isDesktop) SemanticsAction.didLoseAccessibilityFocus,
+ SemanticsAction.tap,
+ SemanticsAction.focus,
+ SemanticsAction.setSelection,
+ SemanticsAction.setText,
+ SemanticsAction.paste,
+ ],
+ label: 'Search',
+ textDirection: TextDirection.ltr,
+ textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
+ );
+
return TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
@@ -688,29 +743,7 @@
tooltip: 'Back',
textDirection: TextDirection.ltr,
),
- TestSemantics(
- id: 9,
- flags: <SemanticsFlag>[
- SemanticsFlag.isTextField,
- SemanticsFlag.hasEnabledState,
- SemanticsFlag.isEnabled,
- SemanticsFlag.isFocused,
- SemanticsFlag.isHeader,
- if (!isCupertino) SemanticsFlag.namesRoute,
- ],
- actions: <SemanticsAction>[
- if (isDesktop) SemanticsAction.didGainAccessibilityFocus,
- if (isDesktop) SemanticsAction.didLoseAccessibilityFocus,
- SemanticsAction.tap,
- SemanticsAction.focus,
- SemanticsAction.setSelection,
- SemanticsAction.setText,
- SemanticsAction.paste,
- ],
- label: 'Search',
- textDirection: TextDirection.ltr,
- textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
- ),
+ textField,
TestSemantics(
id: 10,
label: 'Bottom',
@@ -818,6 +851,60 @@
final bool isCupertino =
debugDefaultTargetPlatformOverride == TargetPlatform.iOS ||
debugDefaultTargetPlatformOverride == TargetPlatform.macOS;
+ final TestSemantics textField =
+ kIsWeb
+ ? TestSemantics(
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isHeader,
+ if (!isCupertino) SemanticsFlag.namesRoute,
+ ],
+ children: <TestSemantics>[
+ TestSemantics(
+ id: 11,
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.hasEnabledState,
+ SemanticsFlag.isEnabled,
+ SemanticsFlag.isFocused,
+ ],
+ actions: <SemanticsAction>[
+ if (isDesktop) SemanticsAction.didGainAccessibilityFocus,
+ if (isDesktop) SemanticsAction.didLoseAccessibilityFocus,
+ SemanticsAction.tap,
+ SemanticsAction.focus,
+ SemanticsAction.setSelection,
+ SemanticsAction.setText,
+ SemanticsAction.paste,
+ ],
+ label: 'Search',
+ textDirection: TextDirection.ltr,
+ textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
+ ),
+ ],
+ )
+ : TestSemantics(
+ id: 11,
+ flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.hasEnabledState,
+ SemanticsFlag.isEnabled,
+ SemanticsFlag.isFocused,
+ SemanticsFlag.isHeader,
+ if (!isCupertino) SemanticsFlag.namesRoute,
+ ],
+ actions: <SemanticsAction>[
+ if (isDesktop) SemanticsAction.didGainAccessibilityFocus,
+ if (isDesktop) SemanticsAction.didLoseAccessibilityFocus,
+ SemanticsAction.tap,
+ SemanticsAction.focus,
+ SemanticsAction.setSelection,
+ SemanticsAction.setText,
+ SemanticsAction.paste,
+ ],
+ label: 'Search',
+ textDirection: TextDirection.ltr,
+ textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
+ );
return TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
@@ -852,29 +939,7 @@
tooltip: 'Back',
textDirection: TextDirection.ltr,
),
- TestSemantics(
- id: 11,
- flags: <SemanticsFlag>[
- SemanticsFlag.isTextField,
- SemanticsFlag.hasEnabledState,
- SemanticsFlag.isEnabled,
- SemanticsFlag.isFocused,
- SemanticsFlag.isHeader,
- if (!isCupertino) SemanticsFlag.namesRoute,
- ],
- actions: <SemanticsAction>[
- if (isDesktop) SemanticsAction.didGainAccessibilityFocus,
- if (isDesktop) SemanticsAction.didLoseAccessibilityFocus,
- SemanticsAction.tap,
- SemanticsAction.focus,
- SemanticsAction.setSelection,
- SemanticsAction.setText,
- SemanticsAction.paste,
- ],
- label: 'Search',
- textDirection: TextDirection.ltr,
- textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
- ),
+ textField,
TestSemantics(id: 14, label: 'Bottom', textDirection: TextDirection.ltr),
],
),
diff --git a/packages/flutter/test/widgets/basic_test.dart b/packages/flutter/test/widgets/basic_test.dart
index 5310051..7af66de 100644
--- a/packages/flutter/test/widgets/basic_test.dart
+++ b/packages/flutter/test/widgets/basic_test.dart
@@ -11,6 +11,7 @@
import 'dart:ui' as ui;
import 'dart:ui';
+import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -380,6 +381,208 @@
expect(attributedHint.attributes[0].range, const TextRange(start: 1, end: 2));
});
+ testWidgets('Semantics does not merge role', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(
+ key: key2,
+ role: SemanticsRole.alertDialog,
+ child: const Placeholder(),
+ ),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.role, SemanticsRole.alertDialog);
+ });
+
+ testWidgets('Semantics does not merge role - text field', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, textField: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isTextField), isTrue);
+ });
+
+ testWidgets('Semantics does not merge role - link', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, link: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isLink), isTrue);
+ });
+
+ testWidgets('Semantics does not merge role - scopes route', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(
+ key: key2,
+ scopesRoute: true,
+ explicitChildNodes: true,
+ child: const Placeholder(),
+ ),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.scopesRoute), isTrue);
+ });
+
+ testWidgets('Semantics does not merge role - header on web', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, header: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ if (kIsWeb) {
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isHeader), isTrue);
+ } else {
+ expect(node1 == node2, isTrue);
+ }
+ });
+
+ testWidgets('Semantics does not merge role - image', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, image: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isImage), isTrue);
+ });
+
+ testWidgets('Semantics does not merge role - slider', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, slider: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isSlider), isTrue);
+ });
+
+ testWidgets('Semantics does not merge role - keyboard key', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, keyboardKey: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isKeyboardKey), isTrue);
+ });
+
+ testWidgets('Semantics does not merge role - scopes route', (WidgetTester tester) async {
+ final UniqueKey key1 = UniqueKey();
+ final UniqueKey key2 = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Semantics(
+ key: key1,
+ role: SemanticsRole.dialog,
+ child: Semantics(key: key2, slider: true, child: const Placeholder()),
+ ),
+ ),
+ ),
+ );
+ final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
+ final SemanticsNode node2 = tester.getSemantics(find.byKey(key2));
+ expect(node1 != node2, isTrue);
+ expect(node1.role, SemanticsRole.dialog);
+ expect(node2.hasFlag(SemanticsFlag.isSlider), isTrue);
+ });
+
testWidgets('Semantics can set controls visibility of nodes', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(