Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
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
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/checkable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ _CheckableKind _checkableKindFromSemanticsFlag(
/// See also [ui.SemanticsFlag.hasCheckedState], [ui.SemanticsFlag.isChecked],
/// [ui.SemanticsFlag.isInMutuallyExclusiveGroup], [ui.SemanticsFlag.isToggled],
/// [ui.SemanticsFlag.hasToggledState]
class Checkable extends PrimaryRoleManager {
Checkable(SemanticsObject semanticsObject)
class SemanticCheckable extends SemanticRole {
SemanticCheckable(SemanticsObject semanticsObject)
: _kind = _checkableKindFromSemanticsFlag(semanticsObject),
super.withBasics(
PrimaryRole.checkable,
SemanticRoleKind.checkable,
semanticsObject,
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
) {
Expand Down
42 changes: 22 additions & 20 deletions lib/web_ui/lib/src/engine/semantics/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import '../dom.dart';
import '../semantics.dart';
import '../util.dart';

/// Provides accessibility for dialogs.
///
/// See also [Role.dialog].
class Dialog extends PrimaryRoleManager {
Dialog(SemanticsObject semanticsObject) : super.blank(PrimaryRole.dialog, semanticsObject) {
// The following secondary roles can coexist with dialog. Generic `RouteName`
/// 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`.
addFocusManagement();
Expand Down Expand Up @@ -39,14 +37,14 @@ class Dialog extends PrimaryRoleManager {

void _setDefaultFocus() {
semanticsObject.visitDepthFirstInTraversalOrder((SemanticsObject node) {
final PrimaryRoleManager? roleManager = node.primaryRole;
if (roleManager == null) {
final SemanticRole? role = node.semanticRole;
if (role == null) {
return true;
}

// If the node does not take focus (e.g. focusing on it does not make
// sense at all). Despair not. Keep looking.
final bool didTakeFocus = roleManager.focusAsRouteDefault();
final bool didTakeFocus = role.focusAsRouteDefault();
return !didTakeFocus;
});
}
Expand Down Expand Up @@ -99,14 +97,18 @@ class Dialog extends PrimaryRoleManager {
}
}

/// Supplies a description for the nearest ancestor [Dialog].
class RouteName extends RoleManager {
RouteName(
SemanticsObject semanticsObject,
PrimaryRoleManager owner,
) : super(Role.routeName, semanticsObject, owner);
/// Supplies a description for the nearest ancestor [SemanticDialog].
///
/// 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.
///
/// 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.
class RouteName extends SemanticBehavior {
RouteName(super.semanticsObject, super.owner);

Dialog? _dialog;
SemanticDialog? _dialog;

@override
void update() {
Expand All @@ -124,7 +126,7 @@ class RouteName extends RoleManager {
}

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

void _lookUpNearestAncestorDialog() {
SemanticsObject? parent = semanticsObject.parent;
while (parent != null && parent.primaryRole?.role != PrimaryRole.dialog) {
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.dialog) {
parent = parent.parent;
}
if (parent != null && parent.primaryRole?.role == PrimaryRole.dialog) {
_dialog = parent.primaryRole! as Dialog;
if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.dialog) {
_dialog = parent.semanticRole! as SemanticDialog;
}
}
}
27 changes: 13 additions & 14 deletions lib/web_ui/lib/src/engine/semantics/focusable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import 'semantics.dart';
/// Supplies generic accessibility focus features to semantics nodes that have
/// [ui.SemanticsFlag.isFocusable] set.
///
/// Assumes that the element being focused on is [SemanticsObject.element]. Role
/// managers with special needs can implement custom focus management and
/// exclude this role manager.
/// Assumes that the element being focused on is [SemanticsObject.element].
/// Semantic roles with special needs can implement custom focus management and
/// exclude this behavior.
///
/// `"tab-index=0"` is used because `<flt-semantics>` is not intrinsically
/// focusable. Examples of intrinsically focusable elements include:
Expand All @@ -27,10 +27,9 @@ import 'semantics.dart';
/// See also:
///
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets
class Focusable extends RoleManager {
Focusable(SemanticsObject semanticsObject, PrimaryRoleManager owner)
: _focusManager = AccessibilityFocusManager(semanticsObject.owner),
super(Role.focusable, semanticsObject, owner);
class Focusable extends SemanticBehavior {
Focusable(super.semanticsObject, super.owner)
: _focusManager = AccessibilityFocusManager(semanticsObject.owner);

final AccessibilityFocusManager _focusManager;

Expand All @@ -44,9 +43,9 @@ class Focusable extends RoleManager {
/// programmatically, simulating the screen reader choosing a default element
/// to focus on.
///
/// Returns `true` if the role manager took the focus. Returns `false` if
/// this role manager did not take the focus. The return value can be used to
/// decide whether to stop searching for a node that should take focus.
/// Returns `true` if the node took the focus. Returns `false` if the node did
/// not take the focus. The return value can be used to decide whether to stop
/// searching for a node that should take focus.
bool focusAsRouteDefault() {
_focusManager._lastEvent = AccessibilityFocusManagerEvent.requestedFocus;
owner.element.focusWithoutScroll();
Expand Down Expand Up @@ -106,10 +105,10 @@ enum AccessibilityFocusManagerEvent {
///
/// Unlike [Focusable], which implements focus features on [SemanticsObject]s
/// whose [SemanticsObject.element] is directly focusable, this class can help
/// implementing focus features on custom elements. For example, [Incrementable]
/// uses a custom `<input>` tag internally while its root-level element is not
/// focusable. However, it can still use this class to manage the focus of the
/// internal element.
/// implementing focus features on custom elements. For example,
/// [SemanticIncrementable] uses a custom `<input>` tag internally while its
/// root-level element is not focusable. However, it can still use this class to
/// manage the focus of the internal element.
class AccessibilityFocusManager {
/// Creates a focus manager tied to a specific [EngineSemanticsOwner].
AccessibilityFocusManager(this._owner);
Expand Down
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/heading.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import 'semantics.dart';

/// Renders semantics objects as headings with the corresponding
/// level (h1 ... h6).
class Heading extends PrimaryRoleManager {
Heading(SemanticsObject semanticsObject)
: super.blank(PrimaryRole.heading, semanticsObject) {
class SemanticHeading extends SemanticRole {
SemanticHeading(SemanticsObject semanticsObject)
: super.blank(SemanticRoleKind.heading, semanticsObject) {
addFocusManagement();
addLiveRegion();
addRouteName();
Expand Down
10 changes: 5 additions & 5 deletions lib/web_ui/lib/src/engine/semantics/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import 'semantics.dart';
/// Uses aria img role to convey this semantic information to the element.
///
/// Screen-readers takes advantage of "aria-label" to describe the visual.
class ImageRoleManager extends PrimaryRoleManager {
ImageRoleManager(SemanticsObject semanticsObject)
: super.blank(PrimaryRole.image, semanticsObject) {
// The following secondary roles can coexist with images. `LabelAndValue` is
// not used because this role manager uses special auxiliary elements to
class SemanticImage extends SemanticRole {
SemanticImage(SemanticsObject semanticsObject)
: super.blank(SemanticRoleKind.image, semanticsObject) {
// The following behaviors can coexist with images. `LabelAndValue` is
// not used because this behavior uses special auxiliary elements to
// supply ARIA labels.
// TODO(yjbanov): reevaluate usage of aux elements, https://github.com/flutter/flutter/issues/129317
addFocusManagement();
Expand Down
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/incrementable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import 'semantics.dart';
/// The input element is disabled whenever the gesture mode switches to pointer
/// events. This is to prevent the browser from taking over drag gestures. Drag
/// gestures must be interpreted by the Flutter framework.
class Incrementable extends PrimaryRoleManager {
Incrementable(SemanticsObject semanticsObject)
class SemanticIncrementable extends SemanticRole {
SemanticIncrementable(SemanticsObject semanticsObject)
: _focusManager = AccessibilityFocusManager(semanticsObject.owner),
super.blank(PrimaryRole.incrementable, semanticsObject) {
super.blank(SemanticRoleKind.incrementable, semanticsObject) {
// The following generic roles can coexist with incrementables. Generic focus
// management is not used by this role because the root DOM element is not
// the one being focused on, but the internal `<input>` element.
Expand Down
29 changes: 14 additions & 15 deletions lib/web_ui/lib/src/engine/semantics/label_and_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ enum LabelRepresentation {
sizedSpan;

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

final LabelRepresentation kind;

/// The role manager that this label representation is attached to.
final PrimaryRoleManager owner;
/// The role that this label representation is attached to.
final SemanticRole owner;

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

String? _previousLabel;

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

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

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

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

return computeDomSemanticsLabel(
Expand Down
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import '../dom.dart';
import '../semantics.dart';

/// Provides accessibility for links.
class Link extends PrimaryRoleManager {
Link(SemanticsObject semanticsObject) : super.withBasics(
PrimaryRole.link,
class SemanticLink extends SemanticRole {
SemanticLink(SemanticsObject semanticsObject) : super.withBasics(
SemanticRoleKind.link,
semanticsObject,
preferredLabelRepresentation: LabelRepresentation.domText,
) {
Expand Down
16 changes: 11 additions & 5 deletions lib/web_ui/lib/src/engine/semantics/live_region.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@ import 'semantics.dart';

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

String? _lastAnnouncement;

Expand Down
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/platform_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import 'semantics.dart';
/// See also:
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-owns
/// * https://bugs.webkit.org/show_bug.cgi?id=223798
class PlatformViewRoleManager extends PrimaryRoleManager {
PlatformViewRoleManager(SemanticsObject semanticsObject)
class SemanticPlatformView extends SemanticRole {
SemanticPlatformView(SemanticsObject semanticsObject)
: super.withBasics(
PrimaryRole.platformView,
SemanticRoleKind.platformView,
semanticsObject,
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
);
Expand Down
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/scrollable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import 'package:ui/ui.dart' as ui;
/// contents is less than the size of the viewport the browser snaps
/// "scrollTop" back to zero. If there is more content than available in the
/// viewport "scrollTop" may take positive values.
class Scrollable extends PrimaryRoleManager {
Scrollable(SemanticsObject semanticsObject)
class SemanticScrollable extends SemanticRole {
SemanticScrollable(SemanticsObject semanticsObject)
: super.withBasics(
PrimaryRole.scrollable,
SemanticRoleKind.scrollable,
semanticsObject,
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
) {
Expand Down
Loading