Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit f81ad65

Browse files
committed
[web] switch from .didGain/LoseAccessibilityFocus to .focus
1 parent ebec9e4 commit f81ad65

File tree

6 files changed

+105
-66
lines changed

6 files changed

+105
-66
lines changed

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,6 +2737,30 @@ DomCompositionEvent createDomCompositionEvent(String type,
27372737
}
27382738
}
27392739

2740+
/// This is a pseudo-type for DOM elements that have the boolean `disabled`
2741+
/// property.
2742+
///
2743+
/// This type cannot be part of the actual type hierarchy because each DOM type
2744+
/// defines its `disabled` property ad hoc, without inheriting it from a common
2745+
/// type, e.g. [DomHTMLInputElement] and [DomHTMLTextAreaElement].
2746+
///
2747+
/// To use, simply cast any element known to have the `disabled` property to
2748+
/// this type using `as DomElementWithDisabledProperty`, then read and write
2749+
/// this property as normal.
2750+
@JS()
2751+
@staticInterop
2752+
class DomElementWithDisabledProperty extends DomHTMLElement {}
2753+
2754+
extension DomElementWithDisabledPropertyExtension on DomElementWithDisabledProperty {
2755+
@JS('disabled')
2756+
external JSBoolean? get _disabled;
2757+
bool? get disabled => _disabled?.toDart;
2758+
2759+
@JS('disabled')
2760+
external set _disabled(JSBoolean? value);
2761+
set disabled(bool? value) => _disabled = value?.toJS;
2762+
}
2763+
27402764
@JS()
27412765
@staticInterop
27422766
class DomHTMLInputElement extends DomHTMLElement {}

lib/web_ui/lib/src/engine/semantics/focusable.dart

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,6 @@ typedef _FocusTarget = ({
8181

8282
/// The listener for the "focus" DOM event.
8383
DomEventListener domFocusListener,
84-
85-
/// The listener for the "blur" DOM event.
86-
DomEventListener domBlurListener,
8784
});
8885

8986
/// Implements accessibility focus management for arbitrary elements.
@@ -135,7 +132,6 @@ class AccessibilityFocusManager {
135132
semanticsNodeId: semanticsNodeId,
136133
element: previousTarget.element,
137134
domFocusListener: previousTarget.domFocusListener,
138-
domBlurListener: previousTarget.domBlurListener,
139135
);
140136
return;
141137
}
@@ -148,14 +144,12 @@ class AccessibilityFocusManager {
148144
final _FocusTarget newTarget = (
149145
semanticsNodeId: semanticsNodeId,
150146
element: element,
151-
domFocusListener: createDomEventListener((_) => _setFocusFromDom(true)),
152-
domBlurListener: createDomEventListener((_) => _setFocusFromDom(false)),
147+
domFocusListener: createDomEventListener((_) => _didReceiveDomFocus()),
153148
);
154149
_target = newTarget;
155150

156151
element.tabIndex = 0;
157152
element.addEventListener('focus', newTarget.domFocusListener);
158-
element.addEventListener('blur', newTarget.domBlurListener);
159153
}
160154

161155
/// Stops managing the focus of the current element, if any.
@@ -170,10 +164,9 @@ class AccessibilityFocusManager {
170164
}
171165

172166
target.element.removeEventListener('focus', target.domFocusListener);
173-
target.element.removeEventListener('blur', target.domBlurListener);
174167
}
175168

176-
void _setFocusFromDom(bool acquireFocus) {
169+
void _didReceiveDomFocus() {
177170
final _FocusTarget? target = _target;
178171

179172
if (target == null) {
@@ -184,9 +177,7 @@ class AccessibilityFocusManager {
184177

185178
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
186179
target.semanticsNodeId,
187-
acquireFocus
188-
? ui.SemanticsAction.didGainAccessibilityFocus
189-
: ui.SemanticsAction.didLoseAccessibilityFocus,
180+
ui.SemanticsAction.focus,
190181
null,
191182
);
192183
}
@@ -229,7 +220,7 @@ class AccessibilityFocusManager {
229220
// a dialog, and nothing else in the dialog is focused. The Flutter
230221
// framework expects that the screen reader will focus on the first (in
231222
// traversal order) focusable element inside the dialog and send a
232-
// didGainAccessibilityFocus action. Screen readers on the web do not do
223+
// SemanticsAction.focus action. Screen readers on the web do not do
233224
// that, and so the web engine has to implement this behavior directly. So
234225
// the dialog will look for a focusable element and request focus on it,
235226
// but now there may be a race between this method unsetting the focus and

lib/web_ui/lib/src/engine/semantics/text_field.dart

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ class TextField extends PrimaryRoleManager {
257257
editableElement = semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline)
258258
? createDomHTMLTextAreaElement()
259259
: createDomHTMLInputElement();
260+
_updateEnabledState();
260261

261262
// On iOS, even though the semantic text field is transparent, the cursor
262263
// and text highlighting are still visible. The cursor and text selection
@@ -310,16 +311,7 @@ class TextField extends PrimaryRoleManager {
310311
}
311312

312313
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
313-
semanticsObject.id, ui.SemanticsAction.didGainAccessibilityFocus, null);
314-
}));
315-
activeEditableElement.addEventListener('blur',
316-
createDomEventListener((DomEvent event) {
317-
if (EngineSemantics.instance.gestureMode != GestureMode.browserGestures) {
318-
return;
319-
}
320-
321-
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
322-
semanticsObject.id, ui.SemanticsAction.didLoseAccessibilityFocus, null);
314+
semanticsObject.id, ui.SemanticsAction.focus, null);
323315
}));
324316
}
325317

@@ -433,20 +425,19 @@ class TextField extends PrimaryRoleManager {
433425
// and wait for a tap event before invoking the iOS workaround and creating
434426
// the editable element.
435427
if (editableElement != null) {
428+
_updateEnabledState();
436429
activeEditableElement.style
437430
..width = '${semanticsObject.rect!.width}px'
438431
..height = '${semanticsObject.rect!.height}px';
439432

440433
if (semanticsObject.hasFocus) {
441-
if (domDocument.activeElement !=
442-
activeEditableElement) {
434+
if (domDocument.activeElement != activeEditableElement && semanticsObject.isEnabled) {
443435
semanticsObject.owner.addOneTimePostUpdateCallback(() {
444436
activeEditableElement.focus();
445437
});
446438
}
447439
SemanticsTextEditingStrategy._instance?.activate(this);
448-
} else if (domDocument.activeElement ==
449-
activeEditableElement) {
440+
} else if (domDocument.activeElement == activeEditableElement) {
450441
if (!isIosSafari) {
451442
SemanticsTextEditingStrategy._instance?.deactivate(this);
452443
// Only apply text, because this node is not focused.
@@ -466,6 +457,16 @@ class TextField extends PrimaryRoleManager {
466457
}
467458
}
468459

460+
void _updateEnabledState() {
461+
final DomElement? element = editableElement;
462+
463+
if (element == null) {
464+
return;
465+
}
466+
467+
(element as DomElementWithDisabledProperty).disabled = !semanticsObject.isEnabled;
468+
}
469+
469470
@override
470471
void dispose() {
471472
super.dispose();

lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,7 +1776,7 @@ void _testIncrementables() {
17761776

17771777
pumpSemantics(isFocused: true);
17781778
expect(capturedActions, <CapturedAction>[
1779-
(0, ui.SemanticsAction.didGainAccessibilityFocus, null),
1779+
(0, ui.SemanticsAction.focus, null),
17801780
]);
17811781
capturedActions.clear();
17821782

@@ -1787,10 +1787,12 @@ void _testIncrementables() {
17871787
isEmpty,
17881788
);
17891789

1790+
// The web doesn't send didLoseAccessibilityFocus as on the web,
1791+
// accessibility focus is not observable, only input focus is. As of this
1792+
// writing, there is no SemanticsAction.unfocus action, so the test simply
1793+
// asserts that no actions are being sent as a result of blur.
17901794
element.blur();
1791-
expect(capturedActions, <CapturedAction>[
1792-
(0, ui.SemanticsAction.didLoseAccessibilityFocus, null),
1793-
]);
1795+
expect(capturedActions, isEmpty);
17941796

17951797
semantics().semanticsEnabled = false;
17961798
});
@@ -1821,15 +1823,14 @@ void _testTextField() {
18211823

18221824

18231825
final SemanticsObject node = owner().debugSemanticsTree![0]!;
1826+
final TextField textFieldRole = node.primaryRole! as TextField;
1827+
final DomHTMLInputElement inputElement = textFieldRole.activeEditableElement as DomHTMLInputElement;
18241828

18251829
// TODO(yjbanov): this used to attempt to test that value="hello" but the
18261830
// test was a false positive. We should revise this test and
18271831
// make sure it tests the right things:
18281832
// https://github.com/flutter/flutter/issues/147200
1829-
expect(
1830-
(node.element as DomHTMLInputElement).value,
1831-
isNull,
1832-
);
1833+
expect(inputElement.value, '');
18331834

18341835
expect(node.primaryRole?.role, PrimaryRole.textField);
18351836
expect(
@@ -1852,8 +1853,8 @@ void _testTextField() {
18521853
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
18531854
updateNode(
18541855
builder,
1855-
actions: 0 | ui.SemanticsAction.didGainAccessibilityFocus.index,
1856-
flags: 0 | ui.SemanticsFlag.isTextField.index,
1856+
actions: 0 | ui.SemanticsAction.focus.index,
1857+
flags: 0 | ui.SemanticsFlag.isTextField.index | ui.SemanticsFlag.isEnabled.index,
18571858
value: 'hello',
18581859
transform: Matrix4.identity().toFloat64(),
18591860
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
@@ -1870,7 +1871,7 @@ void _testTextField() {
18701871

18711872
expect(owner().semanticsHost.ownerDocument?.activeElement, textField);
18721873
expect(await logger.idLog.first, 0);
1873-
expect(await logger.actionLog.first, ui.SemanticsAction.didGainAccessibilityFocus);
1874+
expect(await logger.actionLog.first, ui.SemanticsAction.focus);
18741875

18751876
semantics().semanticsEnabled = false;
18761877
}, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638
@@ -2156,7 +2157,7 @@ void _testCheckables() {
21562157

21572158
pumpSemantics(isFocused: true);
21582159
expect(capturedActions, <CapturedAction>[
2159-
(0, ui.SemanticsAction.didGainAccessibilityFocus, null),
2160+
(0, ui.SemanticsAction.focus, null),
21602161
]);
21612162
capturedActions.clear();
21622163

@@ -2166,15 +2167,12 @@ void _testCheckables() {
21662167
pumpSemantics(isFocused: false);
21672168
expect(capturedActions, isEmpty);
21682169

2169-
// If the element is blurred by the browser, then we do want to notify the
2170-
// framework. This is because screen reader can be focused on something
2171-
// other than what the framework is focused on, and notifying the framework
2172-
// about the loss of focus on a node is information that the framework did
2173-
// not have before.
2170+
// The web doesn't send didLoseAccessibilityFocus as on the web,
2171+
// accessibility focus is not observable, only input focus is. As of this
2172+
// writing, there is no SemanticsAction.unfocus action, so the test simply
2173+
// asserts that no actions are being sent as a result of blur.
21742174
element.blur();
2175-
expect(capturedActions, <CapturedAction>[
2176-
(0, ui.SemanticsAction.didLoseAccessibilityFocus, null),
2177-
]);
2175+
expect(capturedActions, isEmpty);
21782176

21792177
semantics().semanticsEnabled = false;
21802178
});
@@ -2340,17 +2338,19 @@ void _testTappable() {
23402338

23412339
pumpSemantics(isFocused: true);
23422340
expect(capturedActions, <CapturedAction>[
2343-
(0, ui.SemanticsAction.didGainAccessibilityFocus, null),
2341+
(0, ui.SemanticsAction.focus, null),
23442342
]);
23452343
capturedActions.clear();
23462344

23472345
pumpSemantics(isFocused: false);
23482346
expect(capturedActions, isEmpty);
23492347

2348+
// The web doesn't send didLoseAccessibilityFocus as on the web,
2349+
// accessibility focus is not observable, only input focus is. As of this
2350+
// writing, there is no SemanticsAction.unfocus action, so the test simply
2351+
// asserts that no actions are being sent as a result of blur.
23502352
element.blur();
2351-
expect(capturedActions, <CapturedAction>[
2352-
(0, ui.SemanticsAction.didLoseAccessibilityFocus, null),
2353-
]);
2353+
expect(capturedActions, isEmpty);
23542354

23552355
semantics().semanticsEnabled = false;
23562356
});
@@ -3180,7 +3180,7 @@ void _testDialog() {
31803180
expect(
31813181
capturedActions,
31823182
<CapturedAction>[
3183-
(2, ui.SemanticsAction.didGainAccessibilityFocus, null),
3183+
(2, ui.SemanticsAction.focus, null),
31843184
],
31853185
);
31863186

@@ -3242,7 +3242,7 @@ void _testDialog() {
32423242
expect(
32433243
capturedActions,
32443244
<CapturedAction>[
3245-
(3, ui.SemanticsAction.didGainAccessibilityFocus, null),
3245+
(3, ui.SemanticsAction.focus, null),
32463246
],
32473247
);
32483248

@@ -3392,7 +3392,7 @@ void _testFocusable() {
33923392
pumpSemantics(); // triggers post-update callbacks
33933393
expect(domDocument.activeElement, element);
33943394
expect(capturedActions, <CapturedAction>[
3395-
(1, ui.SemanticsAction.didGainAccessibilityFocus, null),
3395+
(1, ui.SemanticsAction.focus, null),
33963396
]);
33973397
capturedActions.clear();
33983398

@@ -3405,17 +3405,19 @@ void _testFocusable() {
34053405
// Browser blurs the element
34063406
element.blur();
34073407
expect(domDocument.activeElement, isNot(element));
3408-
expect(capturedActions, <CapturedAction>[
3409-
(1, ui.SemanticsAction.didLoseAccessibilityFocus, null),
3410-
]);
3408+
// The web doesn't send didLoseAccessibilityFocus as on the web,
3409+
// accessibility focus is not observable, only input focus is. As of this
3410+
// writing, there is no SemanticsAction.unfocus action, so the test simply
3411+
// asserts that no actions are being sent as a result of blur.
3412+
expect(capturedActions, isEmpty);
34113413
capturedActions.clear();
34123414

34133415
// Request focus again
34143416
manager.changeFocus(true);
34153417
pumpSemantics(); // triggers post-update callbacks
34163418
expect(domDocument.activeElement, element);
34173419
expect(capturedActions, <CapturedAction>[
3418-
(1, ui.SemanticsAction.didGainAccessibilityFocus, null),
3420+
(1, ui.SemanticsAction.focus, null),
34193421
]);
34203422
capturedActions.clear();
34213423

lib/web_ui/test/engine/semantics/semantics_tester.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class SemanticsTester {
7575
bool? hasPaste,
7676
bool? hasDidGainAccessibilityFocus,
7777
bool? hasDidLoseAccessibilityFocus,
78+
bool? hasFocus,
7879
bool? hasCustomAction,
7980
bool? hasDismiss,
8081
bool? hasMoveCursorForwardByWord,
@@ -242,6 +243,9 @@ class SemanticsTester {
242243
if (hasDidLoseAccessibilityFocus ?? false) {
243244
actions |= ui.SemanticsAction.didLoseAccessibilityFocus.index;
244245
}
246+
if (hasFocus ?? false) {
247+
actions |= ui.SemanticsAction.focus.index;
248+
}
245249
if (hasCustomAction ?? false) {
246250
actions |= ui.SemanticsAction.customAction.index;
247251
}

0 commit comments

Comments
 (0)