Skip to content

Commit 21471aa

Browse files
authored
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 flutter/flutter#162124 fixes flutter/flutter#157207 fixes flutter/flutter#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
1 parent 04cbda2 commit 21471aa

File tree

11 files changed

+211
-38
lines changed

11 files changed

+211
-38
lines changed

engine/src/flutter/lib/ui/semantics.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ enum SemanticsRole {
362362

363363
/// The main display for a tab.
364364
tabPanel,
365+
366+
/// A pop up dialog.
367+
dialog,
368+
369+
/// An alert dialog.
370+
alertDialog,
365371
}
366372

367373
/// A Boolean value that can be associated with a semantics node.

engine/src/flutter/lib/web_ui/lib/semantics.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ class SemanticsFlag {
256256
}
257257

258258
// Mirrors engine/src/flutter/lib/ui/semantics.dart
259-
enum SemanticsRole { none, tab, tabBar, tabPanel }
259+
enum SemanticsRole { none, tab, tabBar, tabPanel, dialog, alertDialog }
260260

261261
// When adding a new StringAttributeType, the classes in these file must be
262262
// updated as well.

engine/src/flutter/lib/web_ui/lib/src/engine/semantics/route.dart

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,8 @@ import '../dom.dart';
66
import '../semantics.dart';
77
import '../util.dart';
88

9-
/// Denotes that all descendant nodes are inside a route.
10-
///
11-
/// Routes can include dialogs, pop-up menus, sub-screens, and more.
12-
///
13-
/// See also:
14-
///
15-
/// * [RouteName], which provides a description for this route in the absense
16-
/// of an explicit route label set on the route itself.
17-
class SemanticRoute extends SemanticRole {
18-
SemanticRoute(SemanticsObject semanticsObject)
19-
: super.blank(EngineSemanticsRole.route, semanticsObject) {
9+
class SemanticRouteBase extends SemanticRole {
10+
SemanticRouteBase(super.kind, super.object) : super.blank() {
2011
// The following behaviors can coexist with the route. Generic `RouteName`
2112
// and `LabelAndValue` are not used by this role because when the route
2213
// names its own route an `aria-label` is used instead of
@@ -42,13 +33,6 @@ class SemanticRoute extends SemanticRole {
4233
// Case 2: nothing requested explicit focus. Focus on the first descendant.
4334
_setDefaultFocus();
4435
});
45-
46-
// Lacking any more specific information, ARIA role "dialog" is the
47-
// closest thing to Flutter's route. This can be revisited if better
48-
// options become available, especially if the framework volunteers more
49-
// specific information about the route. Other attributes in the vicinity
50-
// of routes include: "alertdialog", `aria-modal`, "menu", "tooltip".
51-
setAriaRole('dialog');
5236
}
5337

5438
void _setDefaultFocus() {
@@ -109,6 +93,63 @@ class SemanticRoute extends SemanticRole {
10993
}
11094
}
11195

96+
/// Denotes that all descendant nodes are inside a route.
97+
///
98+
/// See also:
99+
///
100+
/// * [RouteName], which provides a description for this route in the absence
101+
/// of an explicit route label set on the route itself.
102+
class SemanticRoute extends SemanticRouteBase {
103+
SemanticRoute(SemanticsObject object) : super(EngineSemanticsRole.route, object) {
104+
// Lacking any more specific information, ARIA role "dialog" is the
105+
// closest thing to Flutter's route. This can be revisited if better
106+
// options become available, especially if the framework volunteers more
107+
// specific information about the route. Other attributes in the vicinity
108+
// of routes include: "alertdialog", `aria-modal`, "menu", "tooltip".
109+
setAriaRole('dialog');
110+
}
111+
}
112+
113+
/// Indicates the container as a pop dialog.
114+
///
115+
/// Uses aria dialog role to convey this semantic information to the element.
116+
///
117+
/// Setting this role will also set aria-modal to true, which helps screen
118+
/// reader better understand this section of screen.
119+
///
120+
/// Screen-readers take advantage of "aria-label" to describe the visual.
121+
///
122+
/// See also:
123+
///
124+
/// * [RouteName], which provides a description for this route in the absence
125+
/// of an explicit route label set on the route itself.
126+
class SemanticDialog extends SemanticRouteBase {
127+
SemanticDialog(SemanticsObject object) : super(EngineSemanticsRole.dialog, object) {
128+
setAriaRole('dialog');
129+
setAttribute('aria-modal', true);
130+
}
131+
}
132+
133+
/// Indicates the container as an alert dialog.
134+
///
135+
/// Uses aria alertdialog role to convey this semantic information to the element.
136+
///
137+
/// Setting this role will also set aria-modal to true, which helps screen
138+
/// reader better understand this section of screen.
139+
///
140+
/// Screen-readers takes advantage of "aria-label" to describe the visual.
141+
///
142+
/// See also:
143+
///
144+
/// * [RouteName], which provides a description for this route in the absence
145+
/// of an explicit route label set on the route itself.
146+
class SemanticAlertDialog extends SemanticRouteBase {
147+
SemanticAlertDialog(SemanticsObject object) : super(EngineSemanticsRole.alertDialog, object) {
148+
setAriaRole('alertdialog');
149+
setAttribute('aria-modal', true);
150+
}
151+
}
152+
112153
/// Supplies a description for the nearest ancestor [SemanticRoute].
113154
///
114155
/// This role is assigned to nodes that have `namesRoute` set but not
@@ -121,7 +162,7 @@ class SemanticRoute extends SemanticRole {
121162
class RouteName extends SemanticBehavior {
122163
RouteName(super.semanticsObject, super.owner);
123164

124-
SemanticRoute? _route;
165+
SemanticRouteBase? _route;
125166

126167
@override
127168
void update() {
@@ -139,7 +180,7 @@ class RouteName extends SemanticBehavior {
139180
}
140181

141182
if (semanticsObject.isLabelDirty) {
142-
final SemanticRoute? route = _route;
183+
final SemanticRouteBase? route = _route;
143184
if (route != null) {
144185
// Already attached to a route, just update the description.
145186
route.describeBy(this);
@@ -158,11 +199,11 @@ class RouteName extends SemanticBehavior {
158199

159200
void _lookUpNearestAncestorRoute() {
160201
SemanticsObject? parent = semanticsObject.parent;
161-
while (parent != null && parent.semanticRole?.kind != EngineSemanticsRole.route) {
202+
while (parent != null && (parent.semanticRole is! SemanticRouteBase)) {
162203
parent = parent.parent;
163204
}
164-
if (parent != null && parent.semanticRole?.kind == EngineSemanticsRole.route) {
165-
_route = parent.semanticRole! as SemanticRoute;
205+
if (parent != null) {
206+
_route = parent.semanticRole! as SemanticRouteBase;
166207
}
167208
}
168209
}

engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,12 @@ enum EngineSemanticsRole {
416416
/// A main content for a tab.
417417
tabPanel,
418418

419+
/// A popup dialog.
420+
dialog,
421+
422+
/// An alert dialog.
423+
alertDialog,
424+
419425
/// A role used when a more specific role cannot be assigend to
420426
/// a [SemanticsObject].
421427
///
@@ -1745,6 +1751,10 @@ class SemanticsObject {
17451751
return EngineSemanticsRole.tabPanel;
17461752
case ui.SemanticsRole.tabBar:
17471753
return EngineSemanticsRole.tabList;
1754+
case ui.SemanticsRole.dialog:
1755+
return EngineSemanticsRole.dialog;
1756+
case ui.SemanticsRole.alertDialog:
1757+
return EngineSemanticsRole.alertDialog;
17481758
case ui.SemanticsRole.none:
17491759
// fallback to checking semantics properties.
17501760
}
@@ -1794,6 +1804,8 @@ class SemanticsObject {
17941804
EngineSemanticsRole.tab => SemanticTab(this),
17951805
EngineSemanticsRole.tabList => SemanticTabList(this),
17961806
EngineSemanticsRole.tabPanel => SemanticTabPanel(this),
1807+
EngineSemanticsRole.dialog => SemanticDialog(this),
1808+
EngineSemanticsRole.alertDialog => SemanticAlertDialog(this),
17971809
EngineSemanticsRole.generic => GenericRole(this),
17981810
};
17991811
}

engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ void runSemanticsTests() {
112112
});
113113
group('route', () {
114114
_testRoute();
115+
_testDialogs();
115116
});
116117
group('focusable', () {
117118
_testFocusable();
@@ -3352,6 +3353,99 @@ void _testRoute() {
33523353
});
33533354
}
33543355

3356+
void _testDialogs() {
3357+
test('nodes with dialog role', () {
3358+
semantics()
3359+
..debugOverrideTimestampFunction(() => _testTime)
3360+
..semanticsEnabled = true;
3361+
3362+
SemanticsObject pumpSemantics() {
3363+
final SemanticsTester tester = SemanticsTester(owner());
3364+
tester.updateNode(
3365+
id: 0,
3366+
role: ui.SemanticsRole.dialog,
3367+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
3368+
);
3369+
tester.apply();
3370+
return tester.getSemanticsObject(0);
3371+
}
3372+
3373+
final SemanticsObject object = pumpSemantics();
3374+
expect(object.semanticRole?.kind, EngineSemanticsRole.dialog);
3375+
expect(object.element.getAttribute('role'), 'dialog');
3376+
});
3377+
3378+
test('nodes with alertdialog role', () {
3379+
semantics()
3380+
..debugOverrideTimestampFunction(() => _testTime)
3381+
..semanticsEnabled = true;
3382+
3383+
SemanticsObject pumpSemantics() {
3384+
final SemanticsTester tester = SemanticsTester(owner());
3385+
tester.updateNode(
3386+
id: 0,
3387+
role: ui.SemanticsRole.alertDialog,
3388+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
3389+
);
3390+
tester.apply();
3391+
return tester.getSemanticsObject(0);
3392+
}
3393+
3394+
final SemanticsObject object = pumpSemantics();
3395+
expect(object.semanticRole?.kind, EngineSemanticsRole.alertDialog);
3396+
expect(object.element.getAttribute('role'), 'alertdialog');
3397+
});
3398+
3399+
test('dialog can be described by a descendant', () {
3400+
semantics()
3401+
..debugOverrideTimestampFunction(() => _testTime)
3402+
..semanticsEnabled = true;
3403+
3404+
void pumpSemantics({required String label}) {
3405+
final SemanticsTester tester = SemanticsTester(owner());
3406+
tester.updateNode(
3407+
id: 0,
3408+
role: ui.SemanticsRole.dialog,
3409+
transform: Matrix4.identity().toFloat64(),
3410+
children: <SemanticsNodeUpdate>[
3411+
tester.updateNode(
3412+
id: 1,
3413+
children: <SemanticsNodeUpdate>[
3414+
tester.updateNode(id: 2, namesRoute: true, label: label),
3415+
],
3416+
),
3417+
],
3418+
);
3419+
tester.apply();
3420+
3421+
expectSemanticsTree(owner(), '''
3422+
<sem role="dialog" aria-describedby="flt-semantic-node-2">
3423+
<sem-c>
3424+
<sem>
3425+
<sem-c>
3426+
<sem><span>$label</span></sem>
3427+
</sem-c>
3428+
</sem>
3429+
</sem-c>
3430+
</sem>
3431+
''');
3432+
}
3433+
3434+
pumpSemantics(label: 'Route label');
3435+
3436+
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.dialog);
3437+
expect(owner().debugSemanticsTree![2]!.semanticRole?.kind, EngineSemanticsRole.generic);
3438+
expect(
3439+
owner().debugSemanticsTree![2]!.semanticRole?.debugSemanticBehaviorTypes,
3440+
contains(RouteName),
3441+
);
3442+
3443+
pumpSemantics(label: 'Updated route label');
3444+
3445+
semantics().semanticsEnabled = false;
3446+
});
3447+
}
3448+
33553449
typedef CapturedAction = (int nodeId, ui.SemanticsAction action, Object? args);
33563450

33573451
void _testFocusable() {

packages/flutter/lib/src/cupertino/dialog.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
library;
1010

1111
import 'dart:math' as math;
12-
import 'dart:ui' show ImageFilter, lerpDouble;
12+
import 'dart:ui' show ImageFilter, SemanticsRole, lerpDouble;
1313

1414
import 'package:flutter/foundation.dart';
1515
import 'package:flutter/gestures.dart';
@@ -463,6 +463,7 @@ class _CupertinoAlertDialogState extends State<CupertinoAlertDialog> {
463463
child: CupertinoPopupSurface(
464464
isSurfacePainted: false,
465465
child: Semantics(
466+
role: SemanticsRole.alertDialog,
466467
namesRoute: true,
467468
scopesRoute: true,
468469
explicitChildNodes: true,
@@ -1332,6 +1333,7 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
13321333
namesRoute: true,
13331334
scopesRoute: true,
13341335
explicitChildNodes: true,
1336+
role: SemanticsRole.dialog,
13351337
label: 'Alert',
13361338
child: CupertinoUserInterfaceLevel(
13371339
data: CupertinoUserInterfaceLevelData.elevated,

packages/flutter/lib/src/material/dialog.dart

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/// @docImport 'text_button.dart';
1111
library;
1212

13-
import 'dart:ui' show clampDouble, lerpDouble;
13+
import 'dart:ui' show SemanticsRole, clampDouble, lerpDouble;
1414

1515
import 'package:flutter/cupertino.dart';
1616

@@ -67,6 +67,7 @@ class Dialog extends StatelessWidget {
6767
this.shape,
6868
this.alignment,
6969
this.child,
70+
this.semanticsRole = SemanticsRole.dialog,
7071
}) : assert(elevation == null || elevation >= 0.0),
7172
_fullscreen = false;
7273

@@ -79,6 +80,7 @@ class Dialog extends StatelessWidget {
7980
this.insetAnimationDuration = Duration.zero,
8081
this.insetAnimationCurve = Curves.decelerate,
8182
this.child,
83+
this.semanticsRole = SemanticsRole.dialog,
8284
}) : elevation = 0,
8385
shadowColor = null,
8486
surfaceTintColor = null,
@@ -229,6 +231,11 @@ class Dialog extends StatelessWidget {
229231
/// This value is used to determine if this is a fullscreen dialog.
230232
final bool _fullscreen;
231233

234+
/// The role this dialog represent in assist technologies.
235+
///
236+
/// Defaults to [SemanticsRole.dialog].
237+
final SemanticsRole semanticsRole;
238+
232239
@override
233240
Widget build(BuildContext context) {
234241
final ThemeData theme = Theme.of(context);
@@ -268,17 +275,20 @@ class Dialog extends StatelessWidget {
268275
);
269276
}
270277

271-
return AnimatedPadding(
272-
padding: effectivePadding,
273-
duration: insetAnimationDuration,
274-
curve: insetAnimationCurve,
275-
child: MediaQuery.removeViewInsets(
276-
removeLeft: true,
277-
removeTop: true,
278-
removeRight: true,
279-
removeBottom: true,
280-
context: context,
281-
child: dialogChild,
278+
return Semantics(
279+
role: semanticsRole,
280+
child: AnimatedPadding(
281+
padding: effectivePadding,
282+
duration: insetAnimationDuration,
283+
curve: insetAnimationCurve,
284+
child: MediaQuery.removeViewInsets(
285+
removeLeft: true,
286+
removeTop: true,
287+
removeRight: true,
288+
removeBottom: true,
289+
context: context,
290+
child: dialogChild,
291+
),
282292
),
283293
);
284294
}
@@ -918,6 +928,7 @@ class AlertDialog extends StatelessWidget {
918928
clipBehavior: clipBehavior,
919929
shape: shape,
920930
alignment: alignment,
931+
semanticsRole: SemanticsRole.alertDialog,
921932
child: dialogChild,
922933
);
923934
}

0 commit comments

Comments
 (0)