Skip to content

Commit 7473782

Browse files
authored
[web] better class names for semantics (flutter#54070)
A few non-functional clean-ups in semantics: * Rename `RoleManager` to `SemanticBehavior`. * Rename `PrimaryRoleManager` to `SemanticRole`. * Remove the `Role` enum. Move the enum docs into the respective classes. ## Why? Previous naming was confusing. It's not clear what the difference is between a "role manager" and a "primary role manager". The word "manager" is a meaningless addition; the `Semantic*` prefix is much more meaningful. The `Role` enum was only used for tests, but tests can just use `SemanticRole.runtimeType`. ## New state of the world After this PR the semantics system has "objects" (class `SemanticsObject`), "roles" (class `SemanticRole`), and "behaviors" (class `SemanticBehavior`). - A semantic _object_ is an object attached to the framework-side `SemanticNode`. It lives as long as the semantic node does, and provides basic functionality that's common across all nodes. - A semantic object has exactly one semantic _role_. This role is determined from the flags set on the semantic node. Flags can change, causing a semantic object to change its role, which is why these are two separate classes. If an object had just one permanent role, we could combine these classes into one (maybe one day we'll do it, as changing roles dynamically is weird, but that needs major changes in the framework). - A semantic role may have zero or more semantic _behaviors_. A behavior supplies a piece of functionality, such as focusability, clickability/tappability, live regions, etc. A behavior can be shared by multiple roles. For example, both `Button` and `Checkable` roles use the `Tappable` behavior. This is why there's a many-to-many relationship between roles and behaviors. Or in entity relationship terms: ```mermaid --- title: Semantic object relationships --- erDiagram SemanticsNode ||--|| SemanticsObject : managed-by SemanticsObject ||--o{ SemanticRole : has-a SemanticRole }o--o{ SemanticBehavior : has ```
1 parent f4f0bf3 commit 7473782

18 files changed

+308
-334
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ _CheckableKind _checkableKindFromSemanticsFlag(
4949
/// See also [ui.SemanticsFlag.hasCheckedState], [ui.SemanticsFlag.isChecked],
5050
/// [ui.SemanticsFlag.isInMutuallyExclusiveGroup], [ui.SemanticsFlag.isToggled],
5151
/// [ui.SemanticsFlag.hasToggledState]
52-
class Checkable extends PrimaryRoleManager {
53-
Checkable(SemanticsObject semanticsObject)
52+
class SemanticCheckable extends SemanticRole {
53+
SemanticCheckable(SemanticsObject semanticsObject)
5454
: _kind = _checkableKindFromSemanticsFlag(semanticsObject),
5555
super.withBasics(
56-
PrimaryRole.checkable,
56+
SemanticRoleKind.checkable,
5757
semanticsObject,
5858
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
5959
) {

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

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import '../dom.dart';
66
import '../semantics.dart';
77
import '../util.dart';
88

9-
/// Provides accessibility for dialogs.
10-
///
11-
/// See also [Role.dialog].
12-
class Dialog extends PrimaryRoleManager {
13-
Dialog(SemanticsObject semanticsObject) : super.blank(PrimaryRole.dialog, semanticsObject) {
14-
// The following secondary roles can coexist with dialog. Generic `RouteName`
9+
/// Provides accessibility for routes, including dialogs and pop-up menus.
10+
class SemanticDialog extends SemanticRole {
11+
SemanticDialog(SemanticsObject semanticsObject) : super.blank(SemanticRoleKind.dialog, semanticsObject) {
12+
// The following behaviors can coexist with dialog. Generic `RouteName`
1513
// and `LabelAndValue` are not used by this role because when the dialog
1614
// names its own route an `aria-label` is used instead of `aria-describedby`.
1715
addFocusManagement();
@@ -39,14 +37,14 @@ class Dialog extends PrimaryRoleManager {
3937

4038
void _setDefaultFocus() {
4139
semanticsObject.visitDepthFirstInTraversalOrder((SemanticsObject node) {
42-
final PrimaryRoleManager? roleManager = node.primaryRole;
43-
if (roleManager == null) {
40+
final SemanticRole? role = node.semanticRole;
41+
if (role == null) {
4442
return true;
4543
}
4644

4745
// If the node does not take focus (e.g. focusing on it does not make
4846
// sense at all). Despair not. Keep looking.
49-
final bool didTakeFocus = roleManager.focusAsRouteDefault();
47+
final bool didTakeFocus = role.focusAsRouteDefault();
5048
return !didTakeFocus;
5149
});
5250
}
@@ -99,14 +97,18 @@ class Dialog extends PrimaryRoleManager {
9997
}
10098
}
10199

102-
/// Supplies a description for the nearest ancestor [Dialog].
103-
class RouteName extends RoleManager {
104-
RouteName(
105-
SemanticsObject semanticsObject,
106-
PrimaryRoleManager owner,
107-
) : super(Role.routeName, semanticsObject, owner);
100+
/// Supplies a description for the nearest ancestor [SemanticDialog].
101+
///
102+
/// This role is assigned to nodes that have `namesRoute` set but not
103+
/// `scopesRoute`. When both flags are set the node only gets the [SemanticDialog] role.
104+
///
105+
/// If the ancestor dialog is missing, this role has no effect. It is up to the
106+
/// framework, widget, and app authors to make sure a route name is scoped under
107+
/// a route.
108+
class RouteName extends SemanticBehavior {
109+
RouteName(super.semanticsObject, super.owner);
108110

109-
Dialog? _dialog;
111+
SemanticDialog? _dialog;
110112

111113
@override
112114
void update() {
@@ -124,7 +126,7 @@ class RouteName extends RoleManager {
124126
}
125127

126128
if (semanticsObject.isLabelDirty) {
127-
final Dialog? dialog = _dialog;
129+
final SemanticDialog? dialog = _dialog;
128130
if (dialog != null) {
129131
// Already attached to a dialog, just update the description.
130132
dialog.describeBy(this);
@@ -143,11 +145,11 @@ class RouteName extends RoleManager {
143145

144146
void _lookUpNearestAncestorDialog() {
145147
SemanticsObject? parent = semanticsObject.parent;
146-
while (parent != null && parent.primaryRole?.role != PrimaryRole.dialog) {
148+
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.dialog) {
147149
parent = parent.parent;
148150
}
149-
if (parent != null && parent.primaryRole?.role == PrimaryRole.dialog) {
150-
_dialog = parent.primaryRole! as Dialog;
151+
if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.dialog) {
152+
_dialog = parent.semanticRole! as SemanticDialog;
151153
}
152154
}
153155
}

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import 'semantics.dart';
1212
/// Supplies generic accessibility focus features to semantics nodes that have
1313
/// [ui.SemanticsFlag.isFocusable] set.
1414
///
15-
/// Assumes that the element being focused on is [SemanticsObject.element]. Role
16-
/// managers with special needs can implement custom focus management and
17-
/// exclude this role manager.
15+
/// Assumes that the element being focused on is [SemanticsObject.element].
16+
/// Semantic roles with special needs can implement custom focus management and
17+
/// exclude this behavior.
1818
///
1919
/// `"tab-index=0"` is used because `<flt-semantics>` is not intrinsically
2020
/// focusable. Examples of intrinsically focusable elements include:
@@ -27,10 +27,9 @@ import 'semantics.dart';
2727
/// See also:
2828
///
2929
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets
30-
class Focusable extends RoleManager {
31-
Focusable(SemanticsObject semanticsObject, PrimaryRoleManager owner)
32-
: _focusManager = AccessibilityFocusManager(semanticsObject.owner),
33-
super(Role.focusable, semanticsObject, owner);
30+
class Focusable extends SemanticBehavior {
31+
Focusable(super.semanticsObject, super.owner)
32+
: _focusManager = AccessibilityFocusManager(semanticsObject.owner);
3433

3534
final AccessibilityFocusManager _focusManager;
3635

@@ -44,9 +43,9 @@ class Focusable extends RoleManager {
4443
/// programmatically, simulating the screen reader choosing a default element
4544
/// to focus on.
4645
///
47-
/// Returns `true` if the role manager took the focus. Returns `false` if
48-
/// this role manager did not take the focus. The return value can be used to
49-
/// decide whether to stop searching for a node that should take focus.
46+
/// Returns `true` if the node took the focus. Returns `false` if the node did
47+
/// not take the focus. The return value can be used to decide whether to stop
48+
/// searching for a node that should take focus.
5049
bool focusAsRouteDefault() {
5150
_focusManager._lastEvent = AccessibilityFocusManagerEvent.requestedFocus;
5251
owner.element.focusWithoutScroll();
@@ -106,10 +105,10 @@ enum AccessibilityFocusManagerEvent {
106105
///
107106
/// Unlike [Focusable], which implements focus features on [SemanticsObject]s
108107
/// whose [SemanticsObject.element] is directly focusable, this class can help
109-
/// implementing focus features on custom elements. For example, [Incrementable]
110-
/// uses a custom `<input>` tag internally while its root-level element is not
111-
/// focusable. However, it can still use this class to manage the focus of the
112-
/// internal element.
108+
/// implementing focus features on custom elements. For example,
109+
/// [SemanticIncrementable] uses a custom `<input>` tag internally while its
110+
/// root-level element is not focusable. However, it can still use this class to
111+
/// manage the focus of the internal element.
113112
class AccessibilityFocusManager {
114113
/// Creates a focus manager tied to a specific [EngineSemanticsOwner].
115114
AccessibilityFocusManager(this._owner);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import 'semantics.dart';
88

99
/// Renders semantics objects as headings with the corresponding
1010
/// level (h1 ... h6).
11-
class Heading extends PrimaryRoleManager {
12-
Heading(SemanticsObject semanticsObject)
13-
: super.blank(PrimaryRole.heading, semanticsObject) {
11+
class SemanticHeading extends SemanticRole {
12+
SemanticHeading(SemanticsObject semanticsObject)
13+
: super.blank(SemanticRoleKind.heading, semanticsObject) {
1414
addFocusManagement();
1515
addLiveRegion();
1616
addRouteName();

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import 'semantics.dart';
1010
/// Uses aria img role to convey this semantic information to the element.
1111
///
1212
/// Screen-readers takes advantage of "aria-label" to describe the visual.
13-
class ImageRoleManager extends PrimaryRoleManager {
14-
ImageRoleManager(SemanticsObject semanticsObject)
15-
: super.blank(PrimaryRole.image, semanticsObject) {
16-
// The following secondary roles can coexist with images. `LabelAndValue` is
17-
// not used because this role manager uses special auxiliary elements to
13+
class SemanticImage extends SemanticRole {
14+
SemanticImage(SemanticsObject semanticsObject)
15+
: super.blank(SemanticRoleKind.image, semanticsObject) {
16+
// The following behaviors can coexist with images. `LabelAndValue` is
17+
// not used because this behavior uses special auxiliary elements to
1818
// supply ARIA labels.
1919
// TODO(yjbanov): reevaluate usage of aux elements, https://github.com/flutter/flutter/issues/129317
2020
addFocusManagement();

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import 'semantics.dart';
1919
/// The input element is disabled whenever the gesture mode switches to pointer
2020
/// events. This is to prevent the browser from taking over drag gestures. Drag
2121
/// gestures must be interpreted by the Flutter framework.
22-
class Incrementable extends PrimaryRoleManager {
23-
Incrementable(SemanticsObject semanticsObject)
22+
class SemanticIncrementable extends SemanticRole {
23+
SemanticIncrementable(SemanticsObject semanticsObject)
2424
: _focusManager = AccessibilityFocusManager(semanticsObject.owner),
25-
super.blank(PrimaryRole.incrementable, semanticsObject) {
25+
super.blank(SemanticRoleKind.incrementable, semanticsObject) {
2626
// The following generic roles can coexist with incrementables. Generic focus
2727
// management is not used by this role because the root DOM element is not
2828
// the one being focused on, but the internal `<input>` element.

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ enum LabelRepresentation {
4848
sizedSpan;
4949

5050
/// Creates the behavior for this label representation.
51-
LabelRepresentationBehavior createBehavior(PrimaryRoleManager owner) {
51+
LabelRepresentationBehavior createBehavior(SemanticRole owner) {
5252
return switch (this) {
5353
ariaLabel => AriaLabelRepresentation._(owner),
5454
domText => DomTextRepresentation._(owner),
@@ -63,8 +63,8 @@ abstract final class LabelRepresentationBehavior {
6363

6464
final LabelRepresentation kind;
6565

66-
/// The role manager that this label representation is attached to.
67-
final PrimaryRoleManager owner;
66+
/// The role that this label representation is attached to.
67+
final SemanticRole owner;
6868

6969
/// Convenience getter for the corresponding semantics object.
7070
SemanticsObject get semanticsObject => owner.semanticsObject;
@@ -109,7 +109,7 @@ abstract final class LabelRepresentationBehavior {
109109
///
110110
/// <flt-semantics aria-label="Hello, World!"></flt-semantics>
111111
final class AriaLabelRepresentation extends LabelRepresentationBehavior {
112-
AriaLabelRepresentation._(PrimaryRoleManager owner) : super(LabelRepresentation.ariaLabel, owner);
112+
AriaLabelRepresentation._(SemanticRole owner) : super(LabelRepresentation.ariaLabel, owner);
113113

114114
String? _previousLabel;
115115

@@ -143,7 +143,7 @@ final class AriaLabelRepresentation extends LabelRepresentationBehavior {
143143
/// no ARIA role set, or the role does not size the element, then the
144144
/// [SizedSpanRepresentation] representation can be used.
145145
final class DomTextRepresentation extends LabelRepresentationBehavior {
146-
DomTextRepresentation._(PrimaryRoleManager owner) : super(LabelRepresentation.domText, owner);
146+
DomTextRepresentation._(SemanticRole owner) : super(LabelRepresentation.domText, owner);
147147

148148
DomText? _domText;
149149
String? _previousLabel;
@@ -233,7 +233,7 @@ typedef _Measurement = ({
233233
/// * Use an existing non-text role, e.g. "heading". Sizes correctly, but breaks
234234
/// the message (reads "heading").
235235
final class SizedSpanRepresentation extends LabelRepresentationBehavior {
236-
SizedSpanRepresentation._(PrimaryRoleManager owner) : super(LabelRepresentation.sizedSpan, owner) {
236+
SizedSpanRepresentation._(SemanticRole owner) : super(LabelRepresentation.sizedSpan, owner) {
237237
_domText.style
238238
// `inline-block` is needed for two reasons:
239239
// - It supports measuring the true size of the text. Pure `block` would
@@ -433,14 +433,13 @@ final class SizedSpanRepresentation extends LabelRepresentationBehavior {
433433
DomElement get focusTarget => _domText;
434434
}
435435

436-
/// Renders [SemanticsObject.label] and/or [SemanticsObject.value] to the semantics DOM.
436+
/// Renders the label for a [SemanticsObject] that can be scanned by screen
437+
/// readers, web crawlers, and other automated agents.
437438
///
438-
/// The value is not always rendered. Some semantics nodes correspond to
439-
/// interactive controls. In such case the value is reported via that element's
440-
/// `value` attribute rather than rendering it separately.
441-
class LabelAndValue extends RoleManager {
442-
LabelAndValue(SemanticsObject semanticsObject, PrimaryRoleManager owner, { required this.preferredRepresentation })
443-
: super(Role.labelAndValue, semanticsObject, owner);
439+
/// See [computeDomSemanticsLabel] for the exact logic that constructs the label
440+
/// of a semantic node.
441+
class LabelAndValue extends SemanticBehavior {
442+
LabelAndValue(super.semanticsObject, super.owner, { required this.preferredRepresentation });
444443

445444
/// The preferred representation of the label in the DOM.
446445
///
@@ -471,7 +470,7 @@ class LabelAndValue extends RoleManager {
471470
/// If the node has children always use an `aria-label`. Using extra child
472471
/// nodes to represent the label will cause layout shifts and confuse the
473472
/// screen reader. If the are no children, use the representation preferred
474-
/// by the primary role manager.
473+
/// by the role.
475474
LabelRepresentationBehavior _getEffectiveRepresentation() {
476475
final LabelRepresentation effectiveRepresentation = semanticsObject.hasChildren
477476
? LabelRepresentation.ariaLabel
@@ -491,7 +490,7 @@ class LabelAndValue extends RoleManager {
491490
/// combination is present.
492491
String? _computeLabel() {
493492
// If the node is incrementable the value is reported to the browser via
494-
// the respective role manager. We do not need to also render it again here.
493+
// the respective role. We do not need to also render it again here.
495494
final bool shouldDisplayValue = !semanticsObject.isIncrementable && semanticsObject.hasValue;
496495

497496
return computeDomSemanticsLabel(

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import '../dom.dart';
66
import '../semantics.dart';
77

88
/// Provides accessibility for links.
9-
class Link extends PrimaryRoleManager {
10-
Link(SemanticsObject semanticsObject) : super.withBasics(
11-
PrimaryRole.link,
9+
class SemanticLink extends SemanticRole {
10+
SemanticLink(SemanticsObject semanticsObject) : super.withBasics(
11+
SemanticRoleKind.link,
1212
semanticsObject,
1313
preferredLabelRepresentation: LabelRepresentation.domText,
1414
) {

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@ import 'semantics.dart';
1010

1111
/// Manages semantics configurations that represent live regions.
1212
///
13-
/// Assistive technologies treat "aria-live" attribute differently. To keep
14-
/// the behavior consistent, [AccessibilityAnnouncements.announce] is used.
13+
/// A live region is a region whose changes will be announced by the screen
14+
/// reader without the user moving focus onto the node.
15+
///
16+
/// Examples of live regions include snackbars and text field errors. Once
17+
/// identified with this role, they will be able to get the assistive
18+
/// technology's attention right away.
19+
///
20+
/// Different assistive technologies treat "aria-live" attribute differently. To
21+
/// keep the behavior consistent, [AccessibilityAnnouncements.announce] is used.
1522
///
1623
/// When there is an update to [LiveRegion], assistive technologies read the
1724
/// label of the element. See [LabelAndValue]. If there is no label provided
1825
/// no content will be read.
19-
class LiveRegion extends RoleManager {
20-
LiveRegion(SemanticsObject semanticsObject, PrimaryRoleManager owner)
21-
: super(Role.liveRegion, semanticsObject, owner);
26+
class LiveRegion extends SemanticBehavior {
27+
LiveRegion(super.semanticsObject, super.owner);
2228

2329
String? _lastAnnouncement;
2430

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import 'semantics.dart';
2020
/// See also:
2121
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-owns
2222
/// * https://bugs.webkit.org/show_bug.cgi?id=223798
23-
class PlatformViewRoleManager extends PrimaryRoleManager {
24-
PlatformViewRoleManager(SemanticsObject semanticsObject)
23+
class SemanticPlatformView extends SemanticRole {
24+
SemanticPlatformView(SemanticsObject semanticsObject)
2525
: super.withBasics(
26-
PrimaryRole.platformView,
26+
SemanticRoleKind.platformView,
2727
semanticsObject,
2828
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
2929
);

0 commit comments

Comments
 (0)