diff --git a/lib/web_ui/lib/src/engine/semantics/checkable.dart b/lib/web_ui/lib/src/engine/semantics/checkable.dart index 30b7928a131cb..dcc6b6c42c56c 100644 --- a/lib/web_ui/lib/src/engine/semantics/checkable.dart +++ b/lib/web_ui/lib/src/engine/semantics/checkable.dart @@ -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, ) { diff --git a/lib/web_ui/lib/src/engine/semantics/dialog.dart b/lib/web_ui/lib/src/engine/semantics/dialog.dart index 0eab7758a1d93..84896677dd21b 100644 --- a/lib/web_ui/lib/src/engine/semantics/dialog.dart +++ b/lib/web_ui/lib/src/engine/semantics/dialog.dart @@ -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(); @@ -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; }); } @@ -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() { @@ -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); @@ -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; } } } diff --git a/lib/web_ui/lib/src/engine/semantics/focusable.dart b/lib/web_ui/lib/src/engine/semantics/focusable.dart index 4d10a7570b008..b3289e35da829 100644 --- a/lib/web_ui/lib/src/engine/semantics/focusable.dart +++ b/lib/web_ui/lib/src/engine/semantics/focusable.dart @@ -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 `` is not intrinsically /// focusable. Examples of intrinsically focusable elements include: @@ -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; @@ -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(); @@ -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 `` 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 `` 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); diff --git a/lib/web_ui/lib/src/engine/semantics/heading.dart b/lib/web_ui/lib/src/engine/semantics/heading.dart index c1abfe3c0fe08..2088bacd26290 100644 --- a/lib/web_ui/lib/src/engine/semantics/heading.dart +++ b/lib/web_ui/lib/src/engine/semantics/heading.dart @@ -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(); diff --git a/lib/web_ui/lib/src/engine/semantics/image.dart b/lib/web_ui/lib/src/engine/semantics/image.dart index da36ad2a87b5d..6e79f9050d79b 100644 --- a/lib/web_ui/lib/src/engine/semantics/image.dart +++ b/lib/web_ui/lib/src/engine/semantics/image.dart @@ -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(); diff --git a/lib/web_ui/lib/src/engine/semantics/incrementable.dart b/lib/web_ui/lib/src/engine/semantics/incrementable.dart index 65e4a9f5f9e59..c1b6cbd4dc5d6 100644 --- a/lib/web_ui/lib/src/engine/semantics/incrementable.dart +++ b/lib/web_ui/lib/src/engine/semantics/incrementable.dart @@ -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 `` element. diff --git a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart index 729d3538f29ba..006be31641f1a 100644 --- a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart +++ b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart @@ -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), @@ -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; @@ -109,7 +109,7 @@ abstract final class LabelRepresentationBehavior { /// /// final class AriaLabelRepresentation extends LabelRepresentationBehavior { - AriaLabelRepresentation._(PrimaryRoleManager owner) : super(LabelRepresentation.ariaLabel, owner); + AriaLabelRepresentation._(SemanticRole owner) : super(LabelRepresentation.ariaLabel, owner); String? _previousLabel; @@ -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; @@ -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 @@ -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. /// @@ -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 @@ -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( diff --git a/lib/web_ui/lib/src/engine/semantics/link.dart b/lib/web_ui/lib/src/engine/semantics/link.dart index d957e90d2967b..d13f69fac09d5 100644 --- a/lib/web_ui/lib/src/engine/semantics/link.dart +++ b/lib/web_ui/lib/src/engine/semantics/link.dart @@ -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, ) { diff --git a/lib/web_ui/lib/src/engine/semantics/live_region.dart b/lib/web_ui/lib/src/engine/semantics/live_region.dart index 9669f72d8f1dc..25d0291db90c0 100644 --- a/lib/web_ui/lib/src/engine/semantics/live_region.dart +++ b/lib/web_ui/lib/src/engine/semantics/live_region.dart @@ -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; diff --git a/lib/web_ui/lib/src/engine/semantics/platform_view.dart b/lib/web_ui/lib/src/engine/semantics/platform_view.dart index 89b61507a7942..d66ff170bfd63 100644 --- a/lib/web_ui/lib/src/engine/semantics/platform_view.dart +++ b/lib/web_ui/lib/src/engine/semantics/platform_view.dart @@ -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, ); diff --git a/lib/web_ui/lib/src/engine/semantics/scrollable.dart b/lib/web_ui/lib/src/engine/semantics/scrollable.dart index 03534cf54dd8a..9679b85f37a21 100644 --- a/lib/web_ui/lib/src/engine/semantics/scrollable.dart +++ b/lib/web_ui/lib/src/engine/semantics/scrollable.dart @@ -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, ) { diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index 1e9f26157b73c..a35651ba210e4 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -343,11 +343,11 @@ class SemanticsNodeUpdate { final String? linkUrl; } -/// Identifies [PrimaryRoleManager] implementations. +/// Identifies [SemanticRole] implementations. /// /// Each value corresponds to the most specific role a semantics node plays in /// the semantics tree. -enum PrimaryRole { +enum SemanticRoleKind { /// Supports incrementing and/or decrementing its value. incrementable, @@ -393,7 +393,7 @@ enum PrimaryRole { /// it as a dialog would be wrong. dialog, - /// The node's primary role is to host a platform view. + /// The node's role is to host a platform view. platformView, /// A role used when a more specific role cannot be assigend to @@ -406,48 +406,15 @@ enum PrimaryRole { link, } -/// Identifies one of the secondary [RoleManager]s of a [PrimaryRoleManager]. -enum Role { - /// Supplies generic accessibility focus features to semantics nodes that have - /// [ui.SemanticsFlag.isFocusable] set. - focusable, - - /// Supplies generic tapping/clicking functionality. - tappable, - - /// Provides an `aria-label` from `label`, `value`, and/or `tooltip` values. - /// - /// The two are combined into the same role because they interact with each - /// other. - labelAndValue, - - /// Contains a region whose changes will be announced to the screen reader - /// without having to be in focus. - /// - /// These regions can be a snackbar or a text field error. Once identified - /// with this role, they will be able to get the assistive technology's - /// attention right away. - liveRegion, - - /// Provides a description for an ancestor dialog. - /// - /// This role is assigned to nodes that have `namesRoute` set but not - /// `scopesRoute`. When both flags are set the node only gets the dialog - /// role (see [dialog]). - /// - /// If the ancestor dialog is missing, this role does nothing useful. - routeName, -} - -/// Responsible for setting the `role` ARIA attribute and for attaching zero or -/// more secondary [RoleManager]s to a [SemanticsObject]. -abstract class PrimaryRoleManager { +/// Responsible for setting the `role` ARIA attribute, for attaching +/// [SemanticBehavior]s, and for supplying behaviors unique to the role. +abstract class SemanticRole { /// Initializes a role for a [semanticsObject] that includes basic /// functionality for focus, labels, live regions, and route names. /// /// If `labelRepresentation` is true, configures the [LabelAndValue] role with /// [LabelAndValue.labelRepresentation] set to true. - PrimaryRoleManager.withBasics(this.role, this.semanticsObject, { required LabelRepresentation preferredLabelRepresentation }) { + SemanticRole.withBasics(this.kind, this.semanticsObject, { required LabelRepresentation preferredLabelRepresentation }) { element = _initElement(createElement(), semanticsObject); addFocusManagement(); addLiveRegion(); @@ -458,29 +425,23 @@ abstract class PrimaryRoleManager { /// Initializes a blank role for a [semanticsObject]. /// /// Use this constructor for highly specialized cases where - /// [RoleManager.withBasics] does not work, for example when the default focus + /// [SemanticRole.withBasics] does not work, for example when the default focus /// management intereferes with the widget's functionality. - PrimaryRoleManager.blank(this.role, this.semanticsObject) { + SemanticRole.blank(this.kind, this.semanticsObject) { element = _initElement(createElement(), semanticsObject); } late final DomElement element; - /// The primary role identifier. - final PrimaryRole role; + /// The kind of the role that this . + final SemanticRoleKind kind; /// The semantics object managed by this role. final SemanticsObject semanticsObject; - /// Secondary role managers, if any. - List? get secondaryRoleManagers => _secondaryRoleManagers; - List? _secondaryRoleManagers; - - /// Identifiers of secondary roles used by this primary role manager. - /// - /// This is only meant to be used in tests. - @visibleForTesting - List get debugSecondaryRoles => _secondaryRoleManagers?.map((RoleManager manager) => manager.role).toList() ?? const []; + /// Semantic behaviors provided by this role, if any. + List? get behaviors => _behaviors; + List? _behaviors; @protected DomElement createElement() => domDocument.createElement('flt-semantics'); @@ -517,16 +478,16 @@ abstract class PrimaryRoleManager { return element; } - /// A lifecycle method called after the DOM [element] for this role manager - /// is initialized, and the association with the corresponding - /// [SemanticsObject] established. + /// A lifecycle method called after the DOM [element] for this role is + /// initialized, and the association with the corresponding [SemanticsObject] + /// established. /// /// Override this method to implement expensive one-time initialization of a - /// role manager's state. It is more efficient to do such work in this method - /// compared to [update], because [update] can be called many times during the + /// role's state. It is more efficient to do such work in this method compared + /// to [update], because [update] can be called many times during the /// lifecycle of the semantic node. /// - /// It is safe to access [element], [semanticsObject], [secondaryRoleManagers] + /// It is safe to access [element], [semanticsObject], [behaviors] /// and all helper methods that access these fields, such as [append], /// [focusable], etc. void initState() {} @@ -551,51 +512,51 @@ abstract class PrimaryRoleManager { void removeEventListener(String type, DomEventListener? listener, [bool? useCapture]) => element.removeEventListener(type, listener, useCapture); - /// Convenience getter for the [Focusable] role manager, if any. + /// Convenience getter for the [Focusable] behavior, if any. Focusable? get focusable => _focusable; Focusable? _focusable; /// Adds generic focus management features. void addFocusManagement() { - addSecondaryRole(_focusable = Focusable(semanticsObject, this)); + addSemanticBehavior(_focusable = Focusable(semanticsObject, this)); } /// Adds generic live region features. void addLiveRegion() { - addSecondaryRole(LiveRegion(semanticsObject, this)); + addSemanticBehavior(LiveRegion(semanticsObject, this)); } /// Adds generic route name features. void addRouteName() { - addSecondaryRole(RouteName(semanticsObject, this)); + addSemanticBehavior(RouteName(semanticsObject, this)); } - /// Convenience getter for the [LabelAndValue] role manager, if any. + /// Convenience getter for the [LabelAndValue] behavior, if any. LabelAndValue? get labelAndValue => _labelAndValue; LabelAndValue? _labelAndValue; /// Adds generic label features. void addLabelAndValue({ required LabelRepresentation preferredRepresentation }) { - addSecondaryRole(_labelAndValue = LabelAndValue(semanticsObject, this, preferredRepresentation: preferredRepresentation)); + addSemanticBehavior(_labelAndValue = LabelAndValue(semanticsObject, this, preferredRepresentation: preferredRepresentation)); } /// Adds generic functionality for handling taps and clicks. void addTappable() { - addSecondaryRole(Tappable(semanticsObject, this)); + addSemanticBehavior(Tappable(semanticsObject, this)); } - /// Adds a secondary role to this primary role manager. + /// Adds a semantic behavior to this role. /// /// This method should be called by concrete implementations of - /// [PrimaryRoleManager] during initialization. + /// [SemanticRole] during initialization. @protected - void addSecondaryRole(RoleManager secondaryRoleManager) { + void addSemanticBehavior(SemanticBehavior behavior) { assert( - _secondaryRoleManagers?.any((RoleManager manager) => manager.role == secondaryRoleManager.role) != true, - 'Cannot add secondary role ${secondaryRoleManager.role}. This object already has this secondary role.', + _behaviors?.any((existing) => existing.runtimeType == behavior.runtimeType) != true, + 'Cannot add semantic behavior ${behavior.runtimeType}. This object already has it.', ); - _secondaryRoleManagers ??= []; - _secondaryRoleManagers!.add(secondaryRoleManager); + _behaviors ??= []; + _behaviors!.add(behavior); } /// Called immediately after the fields of the [semanticsObject] are updated @@ -605,16 +566,16 @@ abstract class PrimaryRoleManager { /// "is*Dirty" getters to find out exactly what's changed and apply the /// minimum DOM updates. /// - /// The base implementation requests every secondary role manager to update + /// The base implementation requests every semantics behavior to update /// the object. @mustCallSuper void update() { - final List? secondaryRoles = _secondaryRoleManagers; - if (secondaryRoles == null) { + final List? behaviors = _behaviors; + if (behaviors == null) { return; } - for (final RoleManager secondaryRole in secondaryRoles) { - secondaryRole.update(); + for (final SemanticBehavior behavior in behaviors) { + behavior.update(); } if (semanticsObject.isIdentifierDirty) { @@ -630,7 +591,7 @@ abstract class PrimaryRoleManager { } } - /// Whether this role manager was disposed of. + /// Whether this role was disposed of. bool get isDisposed => _isDisposed; bool _isDisposed = false; @@ -648,7 +609,7 @@ abstract class PrimaryRoleManager { } /// Transfers the accessibility focus to the [element] managed by this role - /// manager as a result of this node taking focus by default. + /// as a result of this node taking focus by default. /// /// For example, when a dialog pops up it is expected that one of its child /// nodes takes accessibility focus. @@ -658,16 +619,16 @@ abstract class PrimaryRoleManager { /// input focus. For example, a plain text node cannot take input focus, but /// it can take accessibility focus. /// - /// 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 role took the focus. Returns `false` if this role + /// 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(); } /// A role used when a more specific role couldn't be assigned to the node. -final class GenericRole extends PrimaryRoleManager { +final class GenericRole extends SemanticRole { GenericRole(SemanticsObject semanticsObject) : super.withBasics( - PrimaryRole.generic, + SemanticRoleKind.generic, semanticsObject, // Prefer sized span because if this is a leaf it is frequently a Text widget. // But if it turns out to be a container, then LabelAndValue will automatically @@ -749,26 +710,29 @@ final class GenericRole extends PrimaryRoleManager { /// Provides a piece of functionality to a [SemanticsObject]. /// -/// A secondary role must not set the `role` ARIA attribute. That responsibility -/// falls on the [PrimaryRoleManager]. One [SemanticsObject] may have more than -/// one [RoleManager] but an element may only have one ARIA role, so setting the -/// `role` attribute from a [RoleManager] would cause conflicts. +/// Semantic behaviors can be shared by multiple types of [SemanticRole]s. For +/// example, [SemanticButton] and [SemanticCheckable] both use the [Tappable] behavior. If a +/// semantic role needs bespoke functionality, it is simpler to implement it +/// directly in the [SemanticRole] implementation. /// -/// The [PrimaryRoleManager] decides the list of [RoleManager]s a given semantics -/// node should use. -abstract class RoleManager { - /// Initializes a secondary role for [semanticsObject]. +/// A behavior must not set the `role` ARIA attribute. That responsibility +/// falls on the [SemanticRole]. One [SemanticsObject] may have more than +/// one [SemanticBehavior] but an element may only have one ARIA role, so +/// setting the `role` attribute from a [SemanticBehavior] would cause +/// conflicts. +/// +/// The [SemanticRole] decides the list of [SemanticBehavior]s a given +/// semantics node should use. +abstract class SemanticBehavior { + /// Initializes a behavior for the [semanticsObject]. /// - /// A single role object manages exactly one [SemanticsObject]. - RoleManager(this.role, this.semanticsObject, this.owner); - - /// Role identifier. - final Role role; + /// A single [SemanticBehavior] object manages exactly one [SemanticsObject]. + SemanticBehavior(this.semanticsObject, this.owner); /// The semantics object managed by this role. final SemanticsObject semanticsObject; - final PrimaryRoleManager owner; + final SemanticRole owner; /// Called immediately after the [semanticsObject] updates some of its fields. /// @@ -777,7 +741,7 @@ abstract class RoleManager { /// minimum DOM updates. void update(); - /// Whether this role manager was disposed of. + /// Whether this behavior was disposed of. bool get isDisposed => _isDisposed; bool _isDisposed = false; @@ -1185,7 +1149,7 @@ class SemanticsObject { bool _isDirty(int fieldIndex) => (_dirtyFields & fieldIndex) != 0; /// The dom element of this semantics object. - DomElement get element => primaryRole!.element; + DomElement get element => semanticRole!.element; /// Returns the HTML element that contains the HTML elements of direct /// children of this object. @@ -1288,13 +1252,9 @@ class SemanticsObject { !isButton; /// Whether this node defines a scope for a route. - /// - /// See also [Role.dialog]. bool get scopesRoute => hasFlag(ui.SemanticsFlag.scopesRoute); /// Whether this node describes a route. - /// - /// See also [Role.dialog]. bool get namesRoute => hasFlag(ui.SemanticsFlag.namesRoute); /// Whether this object carry enabled/disabled state (and if so whether it is @@ -1471,7 +1431,7 @@ class SemanticsObject { } // Apply updates to the DOM. - _updateRoles(); + _updateRole(); // All properties that affect positioning and sizing are checked together // any one of them triggers position and size recomputation. @@ -1668,87 +1628,87 @@ class SemanticsObject { _currentChildrenInRenderOrder = childrenInRenderOrder; } - /// The primary role of this node. + /// The role of this node. /// - /// The primary role is assigned by [updateSelf] based on the combination of + /// The role is assigned by [updateSelf] based on the combination of /// semantics flags and actions. - PrimaryRoleManager? primaryRole; + SemanticRole? semanticRole; - PrimaryRole _getPrimaryRoleIdentifier() { + SemanticRoleKind _getSemanticRoleKind() { // The most specific role should take precedence. if (isPlatformView) { - return PrimaryRole.platformView; + return SemanticRoleKind.platformView; } else if (isHeading) { - return PrimaryRole.heading; + return SemanticRoleKind.heading; } else if (isTextField) { - return PrimaryRole.textField; + return SemanticRoleKind.textField; } else if (isIncrementable) { - return PrimaryRole.incrementable; + return SemanticRoleKind.incrementable; } else if (isVisualOnly) { - return PrimaryRole.image; + return SemanticRoleKind.image; } else if (isCheckable) { - return PrimaryRole.checkable; + return SemanticRoleKind.checkable; } else if (isButton) { - return PrimaryRole.button; + return SemanticRoleKind.button; } else if (isScrollContainer) { - return PrimaryRole.scrollable; + return SemanticRoleKind.scrollable; } else if (scopesRoute) { - return PrimaryRole.dialog; + return SemanticRoleKind.dialog; } else if (isLink) { - return PrimaryRole.link; + return SemanticRoleKind.link; } else { - return PrimaryRole.generic; + return SemanticRoleKind.generic; } } - PrimaryRoleManager _createPrimaryRole(PrimaryRole role) { + SemanticRole _createSemanticRole(SemanticRoleKind role) { return switch (role) { - PrimaryRole.textField => TextField(this), - PrimaryRole.scrollable => Scrollable(this), - PrimaryRole.incrementable => Incrementable(this), - PrimaryRole.button => Button(this), - PrimaryRole.checkable => Checkable(this), - PrimaryRole.dialog => Dialog(this), - PrimaryRole.image => ImageRoleManager(this), - PrimaryRole.platformView => PlatformViewRoleManager(this), - PrimaryRole.link => Link(this), - PrimaryRole.heading => Heading(this), - PrimaryRole.generic => GenericRole(this), + SemanticRoleKind.textField => SemanticTextField(this), + SemanticRoleKind.scrollable => SemanticScrollable(this), + SemanticRoleKind.incrementable => SemanticIncrementable(this), + SemanticRoleKind.button => SemanticButton(this), + SemanticRoleKind.checkable => SemanticCheckable(this), + SemanticRoleKind.dialog => SemanticDialog(this), + SemanticRoleKind.image => SemanticImage(this), + SemanticRoleKind.platformView => SemanticPlatformView(this), + SemanticRoleKind.link => SemanticLink(this), + SemanticRoleKind.heading => SemanticHeading(this), + SemanticRoleKind.generic => GenericRole(this), }; } - /// Detects the roles that this semantics object corresponds to and asks the - /// respective role managers to update the DOM. - void _updateRoles() { - PrimaryRoleManager? currentPrimaryRole = primaryRole; - final PrimaryRole roleId = _getPrimaryRoleIdentifier(); - final DomElement? previousElement = primaryRole?.element; + /// Detects the role that this semantics object corresponds to and asks it to + /// update the DOM. + void _updateRole() { + SemanticRole? currentSemanticRole = semanticRole; + final SemanticRoleKind kind = _getSemanticRoleKind(); + final DomElement? previousElement = semanticRole?.element; - if (currentPrimaryRole != null) { - if (currentPrimaryRole.role == roleId) { - // Already has a primary role assigned and the role is the same as before, + if (currentSemanticRole != null) { + if (currentSemanticRole.kind == kind) { + // Already has a role assigned and the role is the same as before, // so simply perform an update. - currentPrimaryRole.update(); + currentSemanticRole.update(); return; } else { // Role changed. This should be avoided as much as possible, but the // web engine will attempt a best with the switch by cleaning old ARIA // role data and start anew. - currentPrimaryRole.dispose(); - currentPrimaryRole = null; - primaryRole = null; + currentSemanticRole.dispose(); + currentSemanticRole = null; + semanticRole = null; } } // This handles two cases: - // * The node was just created and needs a primary role manager. - // * (Uncommon) the node changed its primary role, its previous primary - // role manager was disposed of, and now it needs a new one. - if (currentPrimaryRole == null) { - currentPrimaryRole = _createPrimaryRole(roleId); - primaryRole = currentPrimaryRole; - currentPrimaryRole.initState(); - currentPrimaryRole.update(); + // * The node was just created and needs a role. + // * (Uncommon) the node changed its role, its previous role was disposed + // of, and now it needs a new one. + if (currentSemanticRole == null) { + currentSemanticRole = _createSemanticRole(kind); + semanticRole = currentSemanticRole; + currentSemanticRole.initState(); + currentSemanticRole.update(); } // Reparent element. @@ -1786,7 +1746,7 @@ class SemanticsObject { /// Role-specific adjustment of the vertical position of the child container. /// - /// This is used, for example, by the [Scrollable] to compensate for the + /// This is used, for example, by the [SemanticScrollable] to compensate for the /// `scrollTop` offset in the DOM. /// /// This field must not be null. @@ -1795,7 +1755,7 @@ class SemanticsObject { /// Role-specific adjustment of the horizontal position of the child /// container. /// - /// This is used, for example, by the [Scrollable] to compensate for the + /// This is used, for example, by the [SemanticScrollable] to compensate for the /// `scrollLeft` offset in the DOM. /// /// This field must not be null. @@ -1970,8 +1930,8 @@ class SemanticsObject { _isDisposed = true; element.remove(); _parent = null; - primaryRole?.dispose(); - primaryRole = null; + semanticRole?.dispose(); + semanticRole = null; } } @@ -2013,7 +1973,7 @@ enum SemanticsUpdatePhase { idle, /// Updating individual [SemanticsObject] nodes by calling - /// [RoleManager.update] and fixing parent-child relationships. + /// [SemanticBehavior.update] and fixing parent-child relationships. /// /// After this phase is done, the owner enters the [postUpdate] phase. updating, @@ -2022,7 +1982,7 @@ enum SemanticsUpdatePhase { /// /// At this point all nodes have been updated, the parent child hierarchy has /// been established, the DOM tree is in sync with the semantics tree, and - /// [RoleManager.dispose] has been called on removed nodes. + /// [SemanticBehavior.dispose] has been called on removed nodes. /// /// After this phase is done, the owner switches back to [idle]. postUpdate, @@ -2626,7 +2586,7 @@ AFTER: $description /// Declares that a semantics node will explicitly request focus. /// - /// This prevents others, [Dialog] in particular, from requesting autofocus, + /// This prevents others, [SemanticDialog] in particular, from requesting autofocus, /// as focus can only be taken by one element. Explicit focus has higher /// precedence than autofocus. void willRequestFocus() { diff --git a/lib/web_ui/lib/src/engine/semantics/tappable.dart b/lib/web_ui/lib/src/engine/semantics/tappable.dart index d5f58d6a6cf94..1bd62cab88dd9 100644 --- a/lib/web_ui/lib/src/engine/semantics/tappable.dart +++ b/lib/web_ui/lib/src/engine/semantics/tappable.dart @@ -6,9 +6,9 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; /// Sets the "button" ARIA role. -class Button extends PrimaryRoleManager { - Button(SemanticsObject semanticsObject) : super.withBasics( - PrimaryRole.button, +class SemanticButton extends SemanticRole { + SemanticButton(SemanticsObject semanticsObject) : super.withBasics( + SemanticRoleKind.button, semanticsObject, preferredLabelRepresentation: LabelRepresentation.domText, ) { @@ -31,15 +31,18 @@ class Button extends PrimaryRoleManager { } } -/// Listens to HTML "click" gestures detected by the browser. +/// Implements clicking and tapping behavior for a semantics node. /// -/// This gestures is different from the click and tap gestures detected by the +/// Listens to HTML DOM "click" events detected by the browser. +/// +/// A DOM "click" is different from the click and tap gestures detected by the /// framework from raw pointer events. When an assistive technology is enabled /// the browser may not send us pointer events. In that mode we forward HTML /// click as [ui.SemanticsAction.tap]. -class Tappable extends RoleManager { - Tappable(SemanticsObject semanticsObject, PrimaryRoleManager owner) - : super(Role.tappable, semanticsObject, owner) { +/// +/// See also [ClickDebouncer]. +class Tappable extends SemanticBehavior { + Tappable(super.semanticsObject, super.owner) { _clickListener = createDomEventListener((DomEvent click) { PointerBinding.clickDebouncer.onClick( click, diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 84f0518a652ae..29e88754aebf2 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -42,7 +42,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { /// The text field whose DOM element is currently used for editing. /// /// If this field is null, no editing takes place. - TextField? activeTextField; + SemanticTextField? activeTextField; /// Current input configuration supplied by the "flutter/textinput" channel. InputConfiguration? inputConfig; @@ -66,7 +66,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { /// /// This method must be called after [enable] to name sure that [inputConfig], /// [onChange], and [onAction] are not null. - void activate(TextField textField) { + void activate(SemanticTextField textField) { assert( inputConfig != null && onChange != null && onAction != null, '"enable" should be called before "enableFromSemantics" and initialize input configuration', @@ -91,7 +91,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { /// /// Typically at this point the element loses focus (blurs) and stops being /// used for editing. - void deactivate(TextField textField) { + void deactivate(SemanticTextField textField) { if (activeTextField == textField) { disable(); } @@ -167,7 +167,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { @override void initializeElementPlacement() { - // Element placement is done by [TextField]. + // Element placement is done by [SemanticTextField]. } @override @@ -176,7 +176,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { @override void updateElementPlacement(EditableTextGeometry textGeometry) { - // Element placement is done by [TextField]. + // Element placement is done by [SemanticTextField]. } EditableTextStyle? _queuedStyle; @@ -208,8 +208,8 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { /// browser gestures when in pointer mode. In Safari on iOS pointer events are /// used to detect text box invocation. This is because Safari issues touch /// events even when VoiceOver is enabled. -class TextField extends PrimaryRoleManager { - TextField(SemanticsObject semanticsObject) : super.blank(PrimaryRole.textField, semanticsObject) { +class SemanticTextField extends SemanticRole { + SemanticTextField(SemanticsObject semanticsObject) : super.blank(SemanticRoleKind.textField, semanticsObject) { _initializeEditableElement(); } diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 93ca2bfbf4ace..5c5e25fda0269 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -48,11 +48,11 @@ void runSemanticsTests() { group('longestIncreasingSubsequence', () { _testLongestIncreasingSubsequence(); }); - group(PrimaryRoleManager, () { - _testPrimaryRoleManager(); + group(SemanticRole, () { + _testSemanticRole(); }); - group('Role managers', () { - _testRoleManagerLifecycle(); + group('Roles', () { + _testRoleLifecycle(); }); group('Text', () { _testText(); @@ -113,7 +113,7 @@ void runSemanticsTests() { }); } -void _testPrimaryRoleManager() { +void _testSemanticRole() { test('Sets id and flt-semantics-identifier on the element', () { semantics() ..debugOverrideTimestampFunction(() => _testTime) @@ -175,8 +175,8 @@ void _testPrimaryRoleManager() { }); } -void _testRoleManagerLifecycle() { - test('Secondary role managers are added upon node initialization', () { +void _testRoleLifecycle() { + test('Semantic behaviors are added upon node initialization', () { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -194,10 +194,10 @@ void _testRoleManagerLifecycle() { tester.expectSemantics(''); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.button); + expect(node.semanticRole?.kind, SemanticRoleKind.button); expect( - node.primaryRole?.debugSecondaryRoles, - containsAll([Role.focusable, Role.tappable, Role.labelAndValue]), + node.semanticRole?.debugSemanticBehaviorTypes, + containsAll([Focusable, Tappable, LabelAndValue]), ); expect(tester.getSemanticsObject(0).element.tabIndex, -1); } @@ -217,10 +217,10 @@ void _testRoleManagerLifecycle() { tester.expectSemantics('a label'); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.button); + expect(node.semanticRole?.kind, SemanticRoleKind.button); expect( - node.primaryRole?.debugSecondaryRoles, - containsAll([Role.focusable, Role.tappable, Role.labelAndValue]), + node.semanticRole?.debugSemanticBehaviorTypes, + containsAll([Focusable, Tappable, LabelAndValue]), ); expect(tester.getSemanticsObject(0).element.tabIndex, 0); } @@ -661,16 +661,16 @@ void _testEngineSemanticsOwner() { SemanticsUpdatePhase.idle, ); - // Rudely replace the role manager with a mock, and trigger an update. - final MockRoleManager mockRoleManager = MockRoleManager(PrimaryRole.generic, semanticsObject); - semanticsObject.primaryRole = mockRoleManager; + // Rudely replace the role with a mock, and trigger an update. + final MockRole mockRole = MockRole(SemanticRoleKind.generic, semanticsObject); + semanticsObject.semanticRole = mockRole; pumpSemantics(label: 'World'); expect( reason: 'While updating must be in SemanticsUpdatePhase.updating phase', - mockRoleManager.log, - [ + mockRole.log, + [ (method: 'update', phase: SemanticsUpdatePhase.updating), ], ); @@ -679,15 +679,15 @@ void _testEngineSemanticsOwner() { }); } -typedef MockRoleManagerLogEntry = ({ +typedef MockRoleLogEntry = ({ String method, SemanticsUpdatePhase phase, }); -class MockRoleManager extends PrimaryRoleManager { - MockRoleManager(super.role, super.semanticsObject) : super.blank(); +class MockRole extends SemanticRole { + MockRole(super.role, super.semanticsObject) : super.blank(); - final List log = []; + final List log = []; void _log(String method) { log.add(( @@ -882,9 +882,9 @@ void _testText() { ); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.generic); + expect(node.semanticRole?.kind, SemanticRoleKind.generic); expect( - node.primaryRole!.secondaryRoleManagers!.map((RoleManager m) => m.runtimeType).toList(), + node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), [ Focusable, LiveRegion, @@ -915,9 +915,9 @@ void _testText() { ); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.generic); + expect(node.semanticRole?.kind, SemanticRoleKind.generic); expect( - node.primaryRole!.secondaryRoleManagers!.map((RoleManager m) => m.runtimeType).toList(), + node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), [ Focusable, LiveRegion, @@ -1710,11 +1710,11 @@ void _testIncrementables() { '''); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.incrementable); + expect(node.semanticRole?.kind, SemanticRoleKind.incrementable); expect( reason: 'Incrementables use custom focus management', - node.primaryRole!.debugSecondaryRoles, - isNot(contains(Role.focusable)), + node.semanticRole!.debugSemanticBehaviorTypes, + isNot(contains(Focusable)), ); semantics().semanticsEnabled = false; @@ -1905,7 +1905,7 @@ void _testTextField() { final SemanticsObject node = owner().debugSemanticsTree![0]!; - final TextField textFieldRole = node.primaryRole! as TextField; + final SemanticTextField textFieldRole = node.semanticRole! as SemanticTextField; final DomHTMLInputElement inputElement = textFieldRole.editableElement as DomHTMLInputElement; // TODO(yjbanov): this used to attempt to test that value="hello" but the @@ -1914,11 +1914,11 @@ void _testTextField() { // https://github.com/flutter/flutter/issues/147200 expect(inputElement.value, ''); - expect(node.primaryRole?.role, PrimaryRole.textField); + expect(node.semanticRole?.kind, SemanticRoleKind.textField); expect( reason: 'Text fields use custom focus management', - node.primaryRole!.debugSecondaryRoles, - isNot(contains(Role.focusable)), + node.semanticRole!.debugSemanticBehaviorTypes, + isNot(contains(Focusable)), ); semantics().semanticsEnabled = false; @@ -1952,11 +1952,11 @@ void _testCheckables() { '''); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.checkable); + expect(node.semanticRole?.kind, SemanticRoleKind.checkable); expect( - reason: 'Checkables use generic secondary roles', - node.primaryRole!.debugSecondaryRoles, - containsAll([Role.focusable, Role.tappable]), + reason: 'Checkables use generic semantic behaviors', + node.semanticRole!.debugSemanticBehaviorTypes, + containsAll([Focusable, Tappable]), ); semantics().semanticsEnabled = false; @@ -2253,10 +2253,10 @@ void _testTappable() { '''); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.button); + expect(node.semanticRole?.kind, SemanticRoleKind.button); expect( - node.primaryRole?.debugSecondaryRoles, - containsAll([Role.focusable, Role.tappable]), + node.semanticRole?.debugSemanticBehaviorTypes, + containsAll([Focusable, Tappable]), ); expect(tester.getSemanticsObject(0).element.tabIndex, 0); @@ -2999,8 +2999,8 @@ void _testDialog() { '''); expect( - owner().debugSemanticsTree![0]!.primaryRole?.role, - PrimaryRole.dialog, + owner().debugSemanticsTree![0]!.semanticRole?.kind, + SemanticRoleKind.dialog, ); semantics().semanticsEnabled = false; @@ -3044,8 +3044,8 @@ void _testDialog() { '''); expect( - owner().debugSemanticsTree![0]!.primaryRole?.role, - PrimaryRole.dialog, + owner().debugSemanticsTree![0]!.semanticRole?.kind, + SemanticRoleKind.dialog, ); semantics().semanticsEnabled = false; @@ -3093,16 +3093,16 @@ void _testDialog() { pumpSemantics(label: 'Dialog label'); expect( - owner().debugSemanticsTree![0]!.primaryRole?.role, - PrimaryRole.dialog, + owner().debugSemanticsTree![0]!.semanticRole?.kind, + SemanticRoleKind.dialog, ); expect( - owner().debugSemanticsTree![2]!.primaryRole?.role, - PrimaryRole.generic, + owner().debugSemanticsTree![2]!.semanticRole?.kind, + SemanticRoleKind.generic, ); expect( - owner().debugSemanticsTree![2]!.primaryRole?.debugSecondaryRoles, - contains(Role.routeName), + owner().debugSemanticsTree![2]!.semanticRole?.debugSemanticBehaviorTypes, + contains(RouteName), ); pumpSemantics(label: 'Updated dialog label'); @@ -3131,12 +3131,12 @@ void _testDialog() { '''); expect( - owner().debugSemanticsTree![0]!.primaryRole?.role, - PrimaryRole.dialog, + owner().debugSemanticsTree![0]!.semanticRole?.kind, + SemanticRoleKind.dialog, ); expect( - owner().debugSemanticsTree![0]!.primaryRole?.secondaryRoleManagers, - isNot(contains(Role.routeName)), + owner().debugSemanticsTree![0]!.semanticRole?.behaviors, + isNot(contains(RouteName)), ); semantics().semanticsEnabled = false; @@ -3179,12 +3179,12 @@ void _testDialog() { '''); expect( - owner().debugSemanticsTree![0]!.primaryRole?.role, - PrimaryRole.generic, + owner().debugSemanticsTree![0]!.semanticRole?.kind, + SemanticRoleKind.generic, ); expect( - owner().debugSemanticsTree![2]!.primaryRole?.debugSecondaryRoles, - contains(Role.routeName), + owner().debugSemanticsTree![2]!.semanticRole?.debugSemanticBehaviorTypes, + contains(RouteName), ); semantics().semanticsEnabled = false; @@ -3550,12 +3550,12 @@ void _testFocusable() { final SemanticsObject node = owner().debugSemanticsTree![1]!; expect(node.isFocusable, isTrue); expect( - node.primaryRole?.role, - PrimaryRole.generic, + node.semanticRole?.kind, + SemanticRoleKind.generic, ); expect( - node.primaryRole?.debugSecondaryRoles, - contains(Role.focusable), + node.semanticRole?.debugSemanticBehaviorTypes, + contains(Focusable), ); final DomElement element = node.element; diff --git a/lib/web_ui/test/engine/semantics/semantics_tester.dart b/lib/web_ui/test/engine/semantics/semantics_tester.dart index 5621ff8c20063..d639b7f72b9d2 100644 --- a/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -338,9 +338,9 @@ class SemanticsTester { return owner.debugSemanticsTree![id]!; } - /// Locates the [TextField] role manager of the semantics object with the give [id]. - TextField getTextField(int id) { - return getSemanticsObject(id).primaryRole! as TextField; + /// Locates the [SemanticTextField] role of the semantics object with the give [id]. + SemanticTextField getTextField(int id) { + return getSemanticsObject(id).semanticRole! as SemanticTextField; } void expectSemantics(String semanticsHtml) { @@ -401,3 +401,8 @@ class SemanticsActionLogger { Stream get actionLog => _actionLog; late Stream _actionLog; } + +extension SemanticRoleExtension on SemanticRole { + /// Types of semantics behaviors used by this role. + List get debugSemanticBehaviorTypes => behaviors?.map((behavior) => behavior.runtimeType).toList() ?? const []; +} diff --git a/lib/web_ui/test/engine/semantics/semantics_text_test.dart b/lib/web_ui/test/engine/semantics/semantics_text_test.dart index 3a4e1f495aba2..599d1f7dc66ca 100644 --- a/lib/web_ui/test/engine/semantics/semantics_text_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_text_test.dart @@ -49,11 +49,11 @@ Future testMain() async { ); final SemanticsObject node = owner().debugSemanticsTree![0]!; - expect(node.primaryRole?.role, PrimaryRole.generic); + expect(node.semanticRole?.kind, SemanticRoleKind.generic); expect( reason: 'A node with a label should get a LabelAndValue role', - node.primaryRole!.debugSecondaryRoles, - contains(Role.labelAndValue), + node.semanticRole!.debugSemanticBehaviorTypes, + contains(LabelAndValue), ); } @@ -213,7 +213,7 @@ Future testMain() async { final DomElement span = node.element.querySelector('span')!; expect(span.getAttribute('tabindex'), isNull); - node.primaryRole!.focusAsRouteDefault(); + node.semanticRole!.focusAsRouteDefault(); expect(span.getAttribute('tabindex'), '-1'); expect(domDocument.activeElement, span); @@ -237,7 +237,7 @@ Future testMain() async { final SemanticsObject node = owner().debugSemanticsTree![0]!; // Set DOM text as preferred representation - final LabelAndValue lav = node.primaryRole!.labelAndValue!; + final LabelAndValue lav = node.semanticRole!.labelAndValue!; lav.preferredRepresentation = LabelRepresentation.domText; lav.update(); @@ -246,7 +246,7 @@ Future testMain() async { ); expect(node.element.getAttribute('tabindex'), isNull); - node.primaryRole!.focusAsRouteDefault(); + node.semanticRole!.focusAsRouteDefault(); expect(node.element.getAttribute('tabindex'), '-1'); expect(domDocument.activeElement, node.element); @@ -270,7 +270,7 @@ Future testMain() async { final SemanticsObject node = owner().debugSemanticsTree![0]!; // Set DOM text as preferred representation - final LabelAndValue lav = node.primaryRole!.labelAndValue!; + final LabelAndValue lav = node.semanticRole!.labelAndValue!; lav.preferredRepresentation = LabelRepresentation.ariaLabel; lav.update(); @@ -279,7 +279,7 @@ Future testMain() async { ); expect(node.element.getAttribute('tabindex'), isNull); - node.primaryRole!.focusAsRouteDefault(); + node.semanticRole!.focusAsRouteDefault(); expect(node.element.getAttribute('tabindex'), '-1'); expect(domDocument.activeElement, node.element); diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index bd5f6f81d9fa2..fb1ad1991d1bf 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -60,7 +60,7 @@ void testMain() { value: 'hi', isFocused: true, ); - final TextField textField = textFieldSemantics.primaryRole! as TextField; + final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; // ensureInitialized() isn't called prior to calling dispose() here. // Since we are conditionally calling dispose() on our @@ -102,7 +102,7 @@ void testMain() { // make sure it tests the right things: // https://github.com/flutter/flutter/issues/147200 final SemanticsObject node = owner().debugSemanticsTree![0]!; - final TextField textFieldRole = node.primaryRole! as TextField; + final SemanticTextField textFieldRole = node.semanticRole! as SemanticTextField; final DomHTMLInputElement inputElement = textFieldRole.editableElement as DomHTMLInputElement; expect(inputElement.tagName.toLowerCase(), 'input'); @@ -114,7 +114,7 @@ void testMain() { createTextFieldSemantics(isEnabled: false, value: 'hello'); expectSemanticsTree(owner(), ''''''); final SemanticsObject node = owner().debugSemanticsTree![0]!; - final TextField textFieldRole = node.primaryRole! as TextField; + final SemanticTextField textFieldRole = node.semanticRole! as SemanticTextField; final DomHTMLInputElement inputElement = textFieldRole.editableElement as DomHTMLInputElement; expect(inputElement.tagName.toLowerCase(), 'input'); @@ -170,7 +170,7 @@ void testMain() { rect: const ui.Rect.fromLTWH(0, 0, 10, 15), ); - final TextField textField = textFieldSemantics.primaryRole! as TextField; + final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); @@ -238,7 +238,7 @@ void testMain() { isFocused: true, rect: const ui.Rect.fromLTWH(0, 0, 10, 15)); - final TextField textField = textFieldSemantics.primaryRole! as TextField; + final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; final DomHTMLInputElement editableElement = textField.editableElement as DomHTMLInputElement; @@ -269,7 +269,7 @@ void testMain() { isFocused: true, rect: const ui.Rect.fromLTWH(0, 0, 10, 15)); - final TextField textField = textFieldSemantics.primaryRole! as TextField; + final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; final DomHTMLInputElement editableElement = textField.editableElement as DomHTMLInputElement; @@ -311,7 +311,7 @@ void testMain() { isFocused: true, ); - final TextField textField = textFieldSemantics.primaryRole! as TextField; + final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; expect(textField.editableElement, strategy.domElement); expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement); @@ -347,7 +347,7 @@ void testMain() { expect(strategy.domElement, isNull); // It doesn't remove the DOM element. - final TextField textField = textFieldSemantics.primaryRole! as TextField; + final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; expect(owner().semanticsHost.contains(textField.editableElement), isTrue); // Editing element is not enabled. expect(strategy.isEnabled, isFalse);