Adds dialog and alertdialog role (#162692)
<!-- 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 --> fixes https://github.com/flutter/flutter/issues/162124 fixes https://github.com/flutter/flutter/issues/157207 fixes https://github.com/flutter/flutter/issues/157204 ## 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
This commit is contained in:
parent
04cbda2b1a
commit
21471aa236
@ -362,6 +362,12 @@ enum SemanticsRole {
|
||||
|
||||
/// The main display for a tab.
|
||||
tabPanel,
|
||||
|
||||
/// A pop up dialog.
|
||||
dialog,
|
||||
|
||||
/// An alert dialog.
|
||||
alertDialog,
|
||||
}
|
||||
|
||||
/// A Boolean value that can be associated with a semantics node.
|
||||
|
@ -256,7 +256,7 @@ class SemanticsFlag {
|
||||
}
|
||||
|
||||
// Mirrors engine/src/flutter/lib/ui/semantics.dart
|
||||
enum SemanticsRole { none, tab, tabBar, tabPanel }
|
||||
enum SemanticsRole { none, tab, tabBar, tabPanel, dialog, alertDialog }
|
||||
|
||||
// When adding a new StringAttributeType, the classes in these file must be
|
||||
// updated as well.
|
||||
|
@ -6,17 +6,8 @@ import '../dom.dart';
|
||||
import '../semantics.dart';
|
||||
import '../util.dart';
|
||||
|
||||
/// Denotes that all descendant nodes are inside a route.
|
||||
///
|
||||
/// Routes can include dialogs, pop-up menus, sub-screens, and more.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RouteName], which provides a description for this route in the absense
|
||||
/// of an explicit route label set on the route itself.
|
||||
class SemanticRoute extends SemanticRole {
|
||||
SemanticRoute(SemanticsObject semanticsObject)
|
||||
: super.blank(EngineSemanticsRole.route, semanticsObject) {
|
||||
class SemanticRouteBase extends SemanticRole {
|
||||
SemanticRouteBase(super.kind, super.object) : super.blank() {
|
||||
// The following behaviors can coexist with the route. Generic `RouteName`
|
||||
// and `LabelAndValue` are not used by this role because when the route
|
||||
// names its own route an `aria-label` is used instead of
|
||||
@ -42,13 +33,6 @@ class SemanticRoute extends SemanticRole {
|
||||
// Case 2: nothing requested explicit focus. Focus on the first descendant.
|
||||
_setDefaultFocus();
|
||||
});
|
||||
|
||||
// Lacking any more specific information, ARIA role "dialog" is the
|
||||
// closest thing to Flutter's route. This can be revisited if better
|
||||
// options become available, especially if the framework volunteers more
|
||||
// specific information about the route. Other attributes in the vicinity
|
||||
// of routes include: "alertdialog", `aria-modal`, "menu", "tooltip".
|
||||
setAriaRole('dialog');
|
||||
}
|
||||
|
||||
void _setDefaultFocus() {
|
||||
@ -109,6 +93,63 @@ class SemanticRoute extends SemanticRole {
|
||||
}
|
||||
}
|
||||
|
||||
/// Denotes that all descendant nodes are inside a route.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RouteName], which provides a description for this route in the absence
|
||||
/// of an explicit route label set on the route itself.
|
||||
class SemanticRoute extends SemanticRouteBase {
|
||||
SemanticRoute(SemanticsObject object) : super(EngineSemanticsRole.route, object) {
|
||||
// Lacking any more specific information, ARIA role "dialog" is the
|
||||
// closest thing to Flutter's route. This can be revisited if better
|
||||
// options become available, especially if the framework volunteers more
|
||||
// specific information about the route. Other attributes in the vicinity
|
||||
// of routes include: "alertdialog", `aria-modal`, "menu", "tooltip".
|
||||
setAriaRole('dialog');
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates the container as a pop dialog.
|
||||
///
|
||||
/// Uses aria dialog role to convey this semantic information to the element.
|
||||
///
|
||||
/// Setting this role will also set aria-modal to true, which helps screen
|
||||
/// reader better understand this section of screen.
|
||||
///
|
||||
/// Screen-readers take advantage of "aria-label" to describe the visual.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RouteName], which provides a description for this route in the absence
|
||||
/// of an explicit route label set on the route itself.
|
||||
class SemanticDialog extends SemanticRouteBase {
|
||||
SemanticDialog(SemanticsObject object) : super(EngineSemanticsRole.dialog, object) {
|
||||
setAriaRole('dialog');
|
||||
setAttribute('aria-modal', true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates the container as an alert dialog.
|
||||
///
|
||||
/// Uses aria alertdialog role to convey this semantic information to the element.
|
||||
///
|
||||
/// Setting this role will also set aria-modal to true, which helps screen
|
||||
/// reader better understand this section of screen.
|
||||
///
|
||||
/// Screen-readers takes advantage of "aria-label" to describe the visual.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RouteName], which provides a description for this route in the absence
|
||||
/// of an explicit route label set on the route itself.
|
||||
class SemanticAlertDialog extends SemanticRouteBase {
|
||||
SemanticAlertDialog(SemanticsObject object) : super(EngineSemanticsRole.alertDialog, object) {
|
||||
setAriaRole('alertdialog');
|
||||
setAttribute('aria-modal', true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Supplies a description for the nearest ancestor [SemanticRoute].
|
||||
///
|
||||
/// This role is assigned to nodes that have `namesRoute` set but not
|
||||
@ -121,7 +162,7 @@ class SemanticRoute extends SemanticRole {
|
||||
class RouteName extends SemanticBehavior {
|
||||
RouteName(super.semanticsObject, super.owner);
|
||||
|
||||
SemanticRoute? _route;
|
||||
SemanticRouteBase? _route;
|
||||
|
||||
@override
|
||||
void update() {
|
||||
@ -139,7 +180,7 @@ class RouteName extends SemanticBehavior {
|
||||
}
|
||||
|
||||
if (semanticsObject.isLabelDirty) {
|
||||
final SemanticRoute? route = _route;
|
||||
final SemanticRouteBase? route = _route;
|
||||
if (route != null) {
|
||||
// Already attached to a route, just update the description.
|
||||
route.describeBy(this);
|
||||
@ -158,11 +199,11 @@ class RouteName extends SemanticBehavior {
|
||||
|
||||
void _lookUpNearestAncestorRoute() {
|
||||
SemanticsObject? parent = semanticsObject.parent;
|
||||
while (parent != null && parent.semanticRole?.kind != EngineSemanticsRole.route) {
|
||||
while (parent != null && (parent.semanticRole is! SemanticRouteBase)) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent != null && parent.semanticRole?.kind == EngineSemanticsRole.route) {
|
||||
_route = parent.semanticRole! as SemanticRoute;
|
||||
if (parent != null) {
|
||||
_route = parent.semanticRole! as SemanticRouteBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -416,6 +416,12 @@ enum EngineSemanticsRole {
|
||||
/// A main content for a tab.
|
||||
tabPanel,
|
||||
|
||||
/// A popup dialog.
|
||||
dialog,
|
||||
|
||||
/// An alert dialog.
|
||||
alertDialog,
|
||||
|
||||
/// A role used when a more specific role cannot be assigend to
|
||||
/// a [SemanticsObject].
|
||||
///
|
||||
@ -1745,6 +1751,10 @@ class SemanticsObject {
|
||||
return EngineSemanticsRole.tabPanel;
|
||||
case ui.SemanticsRole.tabBar:
|
||||
return EngineSemanticsRole.tabList;
|
||||
case ui.SemanticsRole.dialog:
|
||||
return EngineSemanticsRole.dialog;
|
||||
case ui.SemanticsRole.alertDialog:
|
||||
return EngineSemanticsRole.alertDialog;
|
||||
case ui.SemanticsRole.none:
|
||||
// fallback to checking semantics properties.
|
||||
}
|
||||
@ -1794,6 +1804,8 @@ class SemanticsObject {
|
||||
EngineSemanticsRole.tab => SemanticTab(this),
|
||||
EngineSemanticsRole.tabList => SemanticTabList(this),
|
||||
EngineSemanticsRole.tabPanel => SemanticTabPanel(this),
|
||||
EngineSemanticsRole.dialog => SemanticDialog(this),
|
||||
EngineSemanticsRole.alertDialog => SemanticAlertDialog(this),
|
||||
EngineSemanticsRole.generic => GenericRole(this),
|
||||
};
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ void runSemanticsTests() {
|
||||
});
|
||||
group('route', () {
|
||||
_testRoute();
|
||||
_testDialogs();
|
||||
});
|
||||
group('focusable', () {
|
||||
_testFocusable();
|
||||
@ -3352,6 +3353,99 @@ void _testRoute() {
|
||||
});
|
||||
}
|
||||
|
||||
void _testDialogs() {
|
||||
test('nodes with dialog role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.dialog,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.dialog);
|
||||
expect(object.element.getAttribute('role'), 'dialog');
|
||||
});
|
||||
|
||||
test('nodes with alertdialog role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.alertDialog,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.alertDialog);
|
||||
expect(object.element.getAttribute('role'), 'alertdialog');
|
||||
});
|
||||
|
||||
test('dialog can be described by a descendant', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
void pumpSemantics({required String label}) {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.dialog,
|
||||
transform: Matrix4.identity().toFloat64(),
|
||||
children: <SemanticsNodeUpdate>[
|
||||
tester.updateNode(
|
||||
id: 1,
|
||||
children: <SemanticsNodeUpdate>[
|
||||
tester.updateNode(id: 2, namesRoute: true, label: label),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree(owner(), '''
|
||||
<sem role="dialog" aria-describedby="flt-semantic-node-2">
|
||||
<sem-c>
|
||||
<sem>
|
||||
<sem-c>
|
||||
<sem><span>$label</span></sem>
|
||||
</sem-c>
|
||||
</sem>
|
||||
</sem-c>
|
||||
</sem>
|
||||
''');
|
||||
}
|
||||
|
||||
pumpSemantics(label: 'Route label');
|
||||
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.dialog);
|
||||
expect(owner().debugSemanticsTree![2]!.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(
|
||||
owner().debugSemanticsTree![2]!.semanticRole?.debugSemanticBehaviorTypes,
|
||||
contains(RouteName),
|
||||
);
|
||||
|
||||
pumpSemantics(label: 'Updated route label');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
typedef CapturedAction = (int nodeId, ui.SemanticsAction action, Object? args);
|
||||
|
||||
void _testFocusable() {
|
||||
|
@ -9,7 +9,7 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show ImageFilter, lerpDouble;
|
||||
import 'dart:ui' show ImageFilter, SemanticsRole, lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -463,6 +463,7 @@ class _CupertinoAlertDialogState extends State<CupertinoAlertDialog> {
|
||||
child: CupertinoPopupSurface(
|
||||
isSurfacePainted: false,
|
||||
child: Semantics(
|
||||
role: SemanticsRole.alertDialog,
|
||||
namesRoute: true,
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
@ -1332,6 +1333,7 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
||||
namesRoute: true,
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
role: SemanticsRole.dialog,
|
||||
label: 'Alert',
|
||||
child: CupertinoUserInterfaceLevel(
|
||||
data: CupertinoUserInterfaceLevelData.elevated,
|
||||
|
@ -10,7 +10,7 @@
|
||||
/// @docImport 'text_button.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' show clampDouble, lerpDouble;
|
||||
import 'dart:ui' show SemanticsRole, clampDouble, lerpDouble;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
@ -67,6 +67,7 @@ class Dialog extends StatelessWidget {
|
||||
this.shape,
|
||||
this.alignment,
|
||||
this.child,
|
||||
this.semanticsRole = SemanticsRole.dialog,
|
||||
}) : assert(elevation == null || elevation >= 0.0),
|
||||
_fullscreen = false;
|
||||
|
||||
@ -79,6 +80,7 @@ class Dialog extends StatelessWidget {
|
||||
this.insetAnimationDuration = Duration.zero,
|
||||
this.insetAnimationCurve = Curves.decelerate,
|
||||
this.child,
|
||||
this.semanticsRole = SemanticsRole.dialog,
|
||||
}) : elevation = 0,
|
||||
shadowColor = null,
|
||||
surfaceTintColor = null,
|
||||
@ -229,6 +231,11 @@ class Dialog extends StatelessWidget {
|
||||
/// This value is used to determine if this is a fullscreen dialog.
|
||||
final bool _fullscreen;
|
||||
|
||||
/// The role this dialog represent in assist technologies.
|
||||
///
|
||||
/// Defaults to [SemanticsRole.dialog].
|
||||
final SemanticsRole semanticsRole;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
@ -268,17 +275,20 @@ class Dialog extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedPadding(
|
||||
padding: effectivePadding,
|
||||
duration: insetAnimationDuration,
|
||||
curve: insetAnimationCurve,
|
||||
child: MediaQuery.removeViewInsets(
|
||||
removeLeft: true,
|
||||
removeTop: true,
|
||||
removeRight: true,
|
||||
removeBottom: true,
|
||||
context: context,
|
||||
child: dialogChild,
|
||||
return Semantics(
|
||||
role: semanticsRole,
|
||||
child: AnimatedPadding(
|
||||
padding: effectivePadding,
|
||||
duration: insetAnimationDuration,
|
||||
curve: insetAnimationCurve,
|
||||
child: MediaQuery.removeViewInsets(
|
||||
removeLeft: true,
|
||||
removeTop: true,
|
||||
removeRight: true,
|
||||
removeBottom: true,
|
||||
context: context,
|
||||
child: dialogChild,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -918,6 +928,7 @@ class AlertDialog extends StatelessWidget {
|
||||
clipBehavior: clipBehavior,
|
||||
shape: shape,
|
||||
alignment: alignment,
|
||||
semanticsRole: SemanticsRole.alertDialog,
|
||||
child: dialogChild,
|
||||
);
|
||||
}
|
||||
|
@ -104,6 +104,8 @@ final int _kUnblockedUserActions =
|
||||
/// A static class to conduct semantics role checks.
|
||||
sealed class _DebugSemanticsRoleChecks {
|
||||
static FlutterError? _checkSemanticsData(SemanticsNode node) => switch (node.role) {
|
||||
SemanticsRole.alertDialog => _noCheckRequired,
|
||||
SemanticsRole.dialog => _noCheckRequired,
|
||||
SemanticsRole.none => _noCheckRequired,
|
||||
SemanticsRole.tab => _semanticsTab,
|
||||
SemanticsRole.tabBar => _semanticsTabBar,
|
||||
|
@ -7,9 +7,10 @@
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -1760,6 +1761,7 @@ void main() {
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
||||
label: 'Alert',
|
||||
role: SemanticsRole.dialog,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
|
@ -711,6 +711,7 @@ void main() {
|
||||
SemanticsFlag.scopesRoute,
|
||||
SemanticsFlag.namesRoute,
|
||||
],
|
||||
role: SemanticsRole.alertDialog,
|
||||
label: 'Alert',
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
|
@ -1616,6 +1616,7 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
role: SemanticsRole.alertDialog,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(id: 5, label: 'title', textDirection: TextDirection.ltr),
|
||||
// The content semantics does not merge into the semantics
|
||||
@ -1803,6 +1804,7 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
role: SemanticsRole.dialog,
|
||||
children: <TestSemantics>[
|
||||
// Title semantics does not merge into the semantics
|
||||
// node 4.
|
||||
|
Loading…
x
Reference in New Issue
Block a user