diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 481c9c3cbc525..57bc1fa30726d 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -220,9 +220,6 @@ 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 diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 32d100df8f114..b81bba41245f2 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -2753,30 +2753,6 @@ 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 {} diff --git a/lib/web_ui/lib/src/engine/semantics/focusable.dart b/lib/web_ui/lib/src/engine/semantics/focusable.dart index 331e1cd50c061..35fff64a50158 100644 --- a/lib/web_ui/lib/src/engine/semantics/focusable.dart +++ b/lib/web_ui/lib/src/engine/semantics/focusable.dart @@ -81,6 +81,9 @@ typedef _FocusTarget = ({ /// The listener for the "focus" DOM event. DomEventListener domFocusListener, + + /// The listener for the "blur" DOM event. + DomEventListener domBlurListener, }); /// Implements accessibility focus management for arbitrary elements. @@ -132,6 +135,7 @@ class AccessibilityFocusManager { semanticsNodeId: semanticsNodeId, element: previousTarget.element, domFocusListener: previousTarget.domFocusListener, + domBlurListener: previousTarget.domBlurListener, ); return; } @@ -144,12 +148,14 @@ class AccessibilityFocusManager { final _FocusTarget newTarget = ( semanticsNodeId: semanticsNodeId, element: element, - domFocusListener: createDomEventListener((_) => _didReceiveDomFocus()), + domFocusListener: createDomEventListener((_) => _setFocusFromDom(true)), + domBlurListener: createDomEventListener((_) => _setFocusFromDom(false)), ); _target = newTarget; element.tabIndex = 0; element.addEventListener('focus', newTarget.domFocusListener); + element.addEventListener('blur', newTarget.domBlurListener); } /// Stops managing the focus of the current element, if any. @@ -164,9 +170,10 @@ class AccessibilityFocusManager { } target.element.removeEventListener('focus', target.domFocusListener); + target.element.removeEventListener('blur', target.domBlurListener); } - void _didReceiveDomFocus() { + void _setFocusFromDom(bool acquireFocus) { final _FocusTarget? target = _target; if (target == null) { @@ -177,7 +184,9 @@ class AccessibilityFocusManager { EnginePlatformDispatcher.instance.invokeOnSemanticsAction( target.semanticsNodeId, - ui.SemanticsAction.focus, + acquireFocus + ? ui.SemanticsAction.didGainAccessibilityFocus + : ui.SemanticsAction.didLoseAccessibilityFocus, null, ); } @@ -220,7 +229,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 - // SemanticsAction.focus action. Screen readers on the web do not do + // didGainAccessibilityFocus 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 diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index 0918ef49c3ff7..c48851d9836a2 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -2218,6 +2218,8 @@ class EngineSemantics { 'mousemove', 'mouseleave', 'mouseup', + 'keyup', + 'keydown', ]; if (pointerEventTypes.contains(event.type)) { 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 3618306d37829..bb79ea1df52d9 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'package:ui/ui.dart' as ui; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; +import '../browser_detection.dart' show isIosSafari; import '../dom.dart'; import '../platform_dispatcher.dart'; import '../text_editing/text_editing.dart'; @@ -120,10 +123,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { // Android). // Otherwise, the keyboard stays on screen even when the user navigates to // a different screen (e.g. by hitting the "back" button). - // Keep this consistent with how DefaultTextEditingStrategy does it. As of - // right now, the only difference is that semantic text fields do not - // participate in form autofill. - DefaultTextEditingStrategy.scheduleFocusFlutterView(activeDomElement, activeDomElementView); + domElement?.blur(); domElement = null; activeTextField = null; _queuedStyle = null; @@ -162,7 +162,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { if (hasAutofillGroup) { placeForm(); } - activeDomElement.focus(preventScroll: true); + activeDomElement.focus(); } @override @@ -207,40 +207,69 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy { /// [EngineSemanticsOwner.gestureMode]. However, in Chrome on Android it ignores /// 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. +/// events even when Voiceover is enabled. class TextField extends PrimaryRoleManager { TextField(SemanticsObject semanticsObject) : super.blank(PrimaryRole.textField, semanticsObject) { - _initializeEditableElement(); + _setupDomElement(); } - /// The element used for editing, e.g. ``, `