mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] better class names for semantics (flutter/engine#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
```
This commit is contained in:
parent
f4ca71ce57
commit
7de2134117
@ -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,
|
||||
) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
@ -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 `<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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
///
|
||||
/// <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;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
|
||||
@ -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<RoleManager>? get secondaryRoleManagers => _secondaryRoleManagers;
|
||||
List<RoleManager>? _secondaryRoleManagers;
|
||||
|
||||
/// Identifiers of secondary roles used by this primary role manager.
|
||||
///
|
||||
/// This is only meant to be used in tests.
|
||||
@visibleForTesting
|
||||
List<Role> get debugSecondaryRoles => _secondaryRoleManagers?.map((RoleManager manager) => manager.role).toList() ?? const <Role>[];
|
||||
/// Semantic behaviors provided by this role, if any.
|
||||
List<SemanticBehavior>? get behaviors => _behaviors;
|
||||
List<SemanticBehavior>? _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 ??= <RoleManager>[];
|
||||
_secondaryRoleManagers!.add(secondaryRoleManager);
|
||||
_behaviors ??= <SemanticBehavior>[];
|
||||
_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<RoleManager>? secondaryRoles = _secondaryRoleManagers;
|
||||
if (secondaryRoles == null) {
|
||||
final List<SemanticBehavior>? 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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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('<sem role="button"></sem>');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.primaryRole?.role, PrimaryRole.button);
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.button);
|
||||
expect(
|
||||
node.primaryRole?.debugSecondaryRoles,
|
||||
containsAll(<Role>[Role.focusable, Role.tappable, Role.labelAndValue]),
|
||||
node.semanticRole?.debugSemanticBehaviorTypes,
|
||||
containsAll(<Type>[Focusable, Tappable, LabelAndValue]),
|
||||
);
|
||||
expect(tester.getSemanticsObject(0).element.tabIndex, -1);
|
||||
}
|
||||
@ -217,10 +217,10 @@ void _testRoleManagerLifecycle() {
|
||||
tester.expectSemantics('<sem role="button">a label</sem>');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.primaryRole?.role, PrimaryRole.button);
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.button);
|
||||
expect(
|
||||
node.primaryRole?.debugSecondaryRoles,
|
||||
containsAll(<Role>[Role.focusable, Role.tappable, Role.labelAndValue]),
|
||||
node.semanticRole?.debugSemanticBehaviorTypes,
|
||||
containsAll(<Type>[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,
|
||||
<MockRoleManagerLogEntry>[
|
||||
mockRole.log,
|
||||
<MockRoleLogEntry>[
|
||||
(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<MockRoleManagerLogEntry> log = <MockRoleManagerLogEntry>[];
|
||||
final List<MockRoleLogEntry> log = <MockRoleLogEntry>[];
|
||||
|
||||
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(),
|
||||
<Type>[
|
||||
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(),
|
||||
<Type>[
|
||||
Focusable,
|
||||
LiveRegion,
|
||||
@ -1710,11 +1710,11 @@ void _testIncrementables() {
|
||||
</sem>''');
|
||||
|
||||
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>[Role.focusable, Role.tappable]),
|
||||
reason: 'Checkables use generic semantic behaviors',
|
||||
node.semanticRole!.debugSemanticBehaviorTypes,
|
||||
containsAll(<Type>[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>[Role.focusable, Role.tappable]),
|
||||
node.semanticRole?.debugSemanticBehaviorTypes,
|
||||
containsAll(<Type>[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;
|
||||
|
||||
@ -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<ui.SemanticsAction> get actionLog => _actionLog;
|
||||
late Stream<ui.SemanticsAction> _actionLog;
|
||||
}
|
||||
|
||||
extension SemanticRoleExtension on SemanticRole {
|
||||
/// Types of semantics behaviors used by this role.
|
||||
List<Type> get debugSemanticBehaviorTypes => behaviors?.map((behavior) => behavior.runtimeType).toList() ?? const <Type>[];
|
||||
}
|
||||
|
||||
@ -49,11 +49,11 @@ Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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);
|
||||
|
||||
|
||||
@ -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(), '''<sem><input /></sem>''');
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user