Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/ui/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ class SemanticsAction {
/// must immediately become editable, opening a virtual keyboard, if needed.
/// Buttons must respond to tap/click events from the keyboard.
///
/// Widget reaction to this action must be idempotent. It is possible to
/// receive this action more than once, or when the widget is already focused.
///
/// Focus behavior is specific to the platform and to the assistive technology
/// used. Typically on desktop operating systems, such as Windows, macOS, and
/// Linux, moving accessibility focus will also move the input focus. On
Expand Down
24 changes: 24 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2762,6 +2762,30 @@ DomCompositionEvent createDomCompositionEvent(String type,
}
}

/// This is a pseudo-type for DOM elements that have the boolean `disabled`
/// property.
///
/// This type cannot be part of the actual type hierarchy because each DOM type
/// defines its `disabled` property ad hoc, without inheriting it from a common
/// type, e.g. [DomHTMLInputElement] and [DomHTMLTextAreaElement].
///
/// To use, simply cast any element known to have the `disabled` property to
/// this type using `as DomElementWithDisabledProperty`, then read and write
/// this property as normal.
@JS()
@staticInterop
class DomElementWithDisabledProperty extends DomHTMLElement {}

extension DomElementWithDisabledPropertyExtension on DomElementWithDisabledProperty {
@JS('disabled')
external JSBoolean? get _disabled;
bool? get disabled => _disabled?.toDart;

@JS('disabled')
external set _disabled(JSBoolean? value);
set disabled(bool? value) => _disabled = value?.toJS;
}

@JS()
@staticInterop
class DomHTMLInputElement extends DomHTMLElement {}
Expand Down
52 changes: 41 additions & 11 deletions lib/web_ui/lib/src/engine/semantics/focusable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Focusable extends RoleManager {
/// 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.
bool focusAsRouteDefault() {
_focusManager._lastEvent = AccessibilityFocusManagerEvent.requestedFocus;
owner.element.focus();
return true;
}
Expand Down Expand Up @@ -86,6 +87,21 @@ typedef _FocusTarget = ({
DomEventListener domBlurListener,
});

enum AccessibilityFocusManagerEvent {
/// No event has happend for the target element.
nothing,

/// The engine requested focus on the DOM element, possibly because the
/// framework requested it.
requestedFocus,

/// Received the DOM "focus" event.
receivedDomFocus,

/// Received the DOM "blur" event.
receivedDomBlur,
}

/// Implements accessibility focus management for arbitrary elements.
///
/// Unlike [Focusable], which implements focus features on [SemanticsObject]s
Expand All @@ -102,6 +118,8 @@ class AccessibilityFocusManager {

_FocusTarget? _target;

AccessibilityFocusManagerEvent _lastEvent = AccessibilityFocusManagerEvent.nothing;

// The last focus value set by this focus manager, used to prevent requesting
// focus on the same element repeatedly. Requesting focus on DOM elements is
// not an idempotent operation. If the element is already focused and focus is
Expand Down Expand Up @@ -148,10 +166,11 @@ class AccessibilityFocusManager {
final _FocusTarget newTarget = (
semanticsNodeId: semanticsNodeId,
element: element,
domFocusListener: createDomEventListener((_) => _setFocusFromDom(true)),
domBlurListener: createDomEventListener((_) => _setFocusFromDom(false)),
domFocusListener: createDomEventListener((_) => _didReceiveDomFocus()),
domBlurListener: createDomEventListener((_) => _didReceiveDomBlur()),
);
_target = newTarget;
_lastEvent = AccessibilityFocusManagerEvent.nothing;

element.tabIndex = 0;
element.addEventListener('focus', newTarget.domFocusListener);
Expand All @@ -173,7 +192,7 @@ class AccessibilityFocusManager {
target.element.removeEventListener('blur', target.domBlurListener);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to tell the difference between framework request focus and user interacting dom focus by the Event object in the listener?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of one for focus/blur events specifically. The closest thing available is the isTrusted property, but it only detects dispatchEvent, which is not enough. Clicks can be differentiated by looking at their screenX/Y properties (source), but focus/blur doesn't have coordinates.

}

void _setFocusFromDom(bool acquireFocus) {
void _didReceiveDomFocus() {
final _FocusTarget? target = _target;

if (target == null) {
Expand All @@ -182,13 +201,23 @@ class AccessibilityFocusManager {
return;
}

EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
target.semanticsNodeId,
acquireFocus
? ui.SemanticsAction.didGainAccessibilityFocus
: ui.SemanticsAction.didLoseAccessibilityFocus,
null,
);
// Do not notify the framework if DOM focus was acquired as a result of
// requesting it programmatically. Only notify the framework if the DOM
// focus was initiated by the browser, e.g. as a result of the screen reader
// shifting focus.
if (_lastEvent != AccessibilityFocusManagerEvent.requestedFocus) {
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
target.semanticsNodeId,
ui.SemanticsAction.focus,
null,
);
}

_lastEvent = AccessibilityFocusManagerEvent.receivedDomFocus;
}

void _didReceiveDomBlur() {
_lastEvent = AccessibilityFocusManagerEvent.receivedDomBlur;
}

/// Requests focus or blur on the DOM element.
Expand Down Expand Up @@ -229,7 +258,7 @@ class AccessibilityFocusManager {
// a dialog, and nothing else in the dialog is focused. The Flutter
// framework expects that the screen reader will focus on the first (in
// traversal order) focusable element inside the dialog and send a
// didGainAccessibilityFocus action. Screen readers on the web do not do
// SemanticsAction.focus action. Screen readers on the web do not do
// that, and so the web engine has to implement this behavior directly. So
// the dialog will look for a focusable element and request focus on it,
// but now there may be a race between this method unsetting the focus and
Expand All @@ -249,6 +278,7 @@ class AccessibilityFocusManager {
return;
}

_lastEvent = AccessibilityFocusManagerEvent.requestedFocus;
target.element.focus();
});
}
Expand Down
2 changes: 0 additions & 2 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2243,8 +2243,6 @@ class EngineSemantics {
'mousemove',
'mouseleave',
'mouseup',
'keyup',
'keydown',
];

if (pointerEventTypes.contains(event.type)) {
Expand Down
Loading