Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[web] rename dialog to route to match the framework #54228

Merged
merged 3 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -43012,7 +43012,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../fl
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../../flutter/LICENSE
Expand All @@ -43021,6 +43020,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dar
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -45899,7 +45899,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
Expand All @@ -45908,6 +45907,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export 'engine/scene_painting.dart';
export 'engine/scene_view.dart';
export 'engine/semantics/accessibility.dart';
export 'engine/semantics/checkable.dart';
export 'engine/semantics/dialog.dart';
export 'engine/semantics/focusable.dart';
export 'engine/semantics/heading.dart';
export 'engine/semantics/image.dart';
Expand All @@ -154,6 +153,7 @@ export 'engine/semantics/label_and_value.dart';
export 'engine/semantics/link.dart';
export 'engine/semantics/live_region.dart';
export 'engine/semantics/platform_view.dart';
export 'engine/semantics/route.dart';
export 'engine/semantics/scrollable.dart';
export 'engine/semantics/semantics.dart';
export 'engine/semantics/semantics_helper.dart';
Expand Down
8 changes: 4 additions & 4 deletions lib/web_ui/lib/src/engine/semantics/focusable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,14 @@ class AccessibilityFocusManager {
// as it is subject to non-local effects. Let's say the framework decides
// that a semantics node is currently not focused. That would lead to
// changeFocus(false) to be called. However, what if this node is inside
// a dialog, and nothing else in the dialog is focused. The Flutter
// a route, and nothing else in the route is focused? The Flutter
// framework expects that the screen reader will focus on the first (in
// traversal order) focusable element inside the dialog and send a
// traversal order) focusable element inside the route and send a
// SemanticsAction.focus action. Screen readers on the web do not do
// that, and so the web engine has to implement this behavior directly. So
// the dialog will look for a focusable element and request focus on it,
// the route will look for a focusable element and request focus on it,
// but now there may be a race between this method unsetting the focus and
// the dialog requesting focus on the same element.
// the route requesting focus on the same element.
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import '../dom.dart';
import '../semantics.dart';
import '../util.dart';

/// Provides accessibility for routes, including dialogs and pop-up menus.
class SemanticDialog extends SemanticRole {
SemanticDialog(SemanticsObject semanticsObject) : super.blank(SemanticRoleKind.dialog, semanticsObject) {
// The following behaviors can coexist with dialog. Generic `RouteName`
// and `LabelAndValue` are not used by this role because when the dialog
// names its own route an `aria-label` is used instead of `aria-describedby`.
/// 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(SemanticRoleKind.route, semanticsObject) {
// 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
// `aria-describedby`.
addFocusManagement();
addLiveRegion();

// When a route/dialog shows up it is expected that the screen reader will
// focus on something inside it. There could be two possibilities:
// When a route is pushed it is expected that the screen reader will focus
// on something inside it. There could be two possibilities:
//
// 1. The framework explicitly marked a node inside the dialog as focused
// 1. The framework explicitly marked a node inside the route as focused
// via the `isFocusable` and `isFocused` flags. In this case, the node
// will request focus directly and there's nothing to do on top of that.
// 2. No node inside the route takes focus explicitly. In this case, the
Expand Down Expand Up @@ -53,103 +61,114 @@ class SemanticDialog extends SemanticRole {
void update() {
super.update();

// If semantic object corresponding to the dialog also provides the label
// for itself it is applied as `aria-label`. See also [describeBy].
// If semantic object corresponding to the route also provides the label for
// itself it is applied as `aria-label`. See also [describeBy].
if (semanticsObject.namesRoute) {
final String? label = semanticsObject.label;
assert(() {
if (label == null || label.trim().isEmpty) {
printWarning(
'Semantic node ${semanticsObject.id} had both scopesRoute and '
'namesRoute set, indicating a self-labelled dialog, but it is '
'missing the label. A dialog should be labelled either by setting '
'namesRoute set, indicating a self-labelled route, but it is '
'missing the label. A route should be labelled either by setting '
'namesRoute on itself and providing a label, or by containing a '
'child node with namesRoute that can describe it with its content.'
);
}
return true;
}());

setAttribute('aria-label', label ?? '');
setAriaRole('dialog');
_assignRole();
}
}

/// Sets the description of this dialog based on a [RouteName] descendant
/// node, unless the dialog provides its own label.
/// Sets the description of this route based on a [RouteName] descendant
/// node, unless the route provides its own label.
void describeBy(RouteName routeName) {
if (semanticsObject.namesRoute) {
// The dialog provides its own label, which takes precedence.
// The route provides its own label, which takes precedence.
return;
}

setAriaRole('dialog');
_assignRole();
setAttribute(
'aria-describedby',
routeName.semanticsObject.element.id,
);
}

void _assignRole() {
// 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');
}

@override
bool focusAsRouteDefault() {
// Dialogs are the ones that look inside themselves to find elements to
// focus on. It doesn't make sense to focus on the dialog itself.
// Routes are the ones that look inside themselves to find elements to
// focus on. It doesn't make sense to focus on the route itself.
return false;
}
}

/// Supplies a description for the nearest ancestor [SemanticDialog].
/// Supplies a description for the nearest ancestor [SemanticRoute].
///
/// This role is assigned to nodes that have `namesRoute` set but not
/// `scopesRoute`. When both flags are set the node only gets the [SemanticDialog] role.
/// `scopesRoute`. When both flags are set the node only gets the
/// [SemanticRoute] role.
///
/// If the ancestor dialog is missing, this role has no effect. It is up to the
/// framework, widget, and app authors to make sure a route name is scoped under
/// a route.
/// If the ancestor route is missing, this role has no effect. It is up to the
/// framework, widget, and app authors to make sure a route name is scoped
/// under a route.
class RouteName extends SemanticBehavior {
RouteName(super.semanticsObject, super.owner);

SemanticDialog? _dialog;
SemanticRoute? _route;

@override
void update() {
// NOTE(yjbanov): this does not handle the case when the node structure
// changes such that this RouteName is no longer attached to the same
// dialog. While this is technically expressible using the semantics API,
// after discussing this case with customers I decided that this case is not
// changes such that this RouteName is no longer attached to the same route.
// While this is technically expressible using the semantics API, after
// discussing this case with customers I decided that this case is not
// interesting enough to support. A tree restructure like this is likely to
// confuse screen readers, and it would add complexity to the engine's
// semantics code. Since reparenting can be done with no update to either
// the Dialog or RouteName we'd have to scan intermediate nodes for
// structural changes.
// the SemanticRoute or RouteName we'd have to scan intermediate nodes
// for structural changes.
if (!semanticsObject.namesRoute) {
return;
}

if (semanticsObject.isLabelDirty) {
final SemanticDialog? dialog = _dialog;
if (dialog != null) {
// Already attached to a dialog, just update the description.
dialog.describeBy(this);
final SemanticRoute? route = _route;
if (route != null) {
// Already attached to a route, just update the description.
route.describeBy(this);
} else {
// Setting the label for the first time. Wait for the DOM tree to be
// established, then find the nearest dialog and update its label.
// established, then find the nearest route and update its label.
semanticsObject.owner.addOneTimePostUpdateCallback(() {
if (!isDisposed) {
_lookUpNearestAncestorDialog();
_dialog?.describeBy(this);
_lookUpNearestAncestorRoute();
_route?.describeBy(this);
}
});
}
}
}

void _lookUpNearestAncestorDialog() {
void _lookUpNearestAncestorRoute() {
SemanticsObject? parent = semanticsObject.parent;
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.dialog) {
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.route) {
parent = parent.parent;
}
if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.dialog) {
_dialog = parent.semanticRole! as SemanticDialog;
if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.route) {
_route = parent.semanticRole! as SemanticRoute;
}
}
}
18 changes: 9 additions & 9 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import '../vector_math.dart';
import '../window.dart';
import 'accessibility.dart';
import 'checkable.dart';
import 'dialog.dart';
import 'focusable.dart';
import 'heading.dart';
import 'image.dart';
Expand All @@ -29,6 +28,7 @@ import 'label_and_value.dart';
import 'link.dart';
import 'live_region.dart';
import 'platform_view.dart';
import 'route.dart';
import 'scrollable.dart';
import 'semantics_helper.dart';
import 'tappable.dart';
Expand Down Expand Up @@ -379,19 +379,19 @@ enum SemanticRoleKind {
/// There are 3 possible situations:
///
/// * The node also has the `namesRoute` bit set. This means that the node's
/// `label` describes the dialog, which can be expressed by adding the
/// `label` describes the route, which can be expressed by adding the
/// `aria-label` attribute.
/// * A descendant node has the `namesRoute` bit set. This means that the
/// child's content describes the dialog. The child may simply be labelled,
/// or it may be a subtree of nodes that describe the dialog together. The
/// child's content describes the route. The child may simply be labelled,
/// or it may be a subtree of nodes that describe the route together. The
/// nearest HTML equivalent is `aria-describedby`. The child acquires the
/// [routeName] role, which manages the relevant ARIA attributes.
/// * There is no `namesRoute` bit anywhere in the sub-tree rooted at the
/// current node. In this case it's likely not a dialog at all, and the node
/// current node. In this case it's likely not a route at all, and the node
/// should not get a label or the "dialog" role. It's just a group of
/// children. For example, a modal barrier has `scopesRoute` set but marking
/// it as a dialog would be wrong.
dialog,
/// it as a route would be wrong.
route,

/// The node's role is to host a platform view.
platformView,
Expand Down Expand Up @@ -1653,7 +1653,7 @@ class SemanticsObject {
} else if (isScrollContainer) {
return SemanticRoleKind.scrollable;
} else if (scopesRoute) {
return SemanticRoleKind.dialog;
return SemanticRoleKind.route;
} else if (isLink) {
return SemanticRoleKind.link;
} else {
Expand All @@ -1668,7 +1668,7 @@ class SemanticsObject {
SemanticRoleKind.incrementable => SemanticIncrementable(this),
SemanticRoleKind.button => SemanticButton(this),
SemanticRoleKind.checkable => SemanticCheckable(this),
SemanticRoleKind.dialog => SemanticDialog(this),
SemanticRoleKind.route => SemanticRoute(this),
SemanticRoleKind.image => SemanticImage(this),
SemanticRoleKind.platformView => SemanticPlatformView(this),
SemanticRoleKind.link => SemanticLink(this),
Expand Down
Loading