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

Commit a887f2b

Browse files
committed
do not echo synthetic focus requests
1 parent e830845 commit a887f2b

File tree

2 files changed

+110
-44
lines changed

2 files changed

+110
-44
lines changed

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Focusable extends RoleManager {
4848
/// this role manager did not take the focus. The return value can be used to
4949
/// decide whether to stop searching for a node that should take focus.
5050
bool focusAsRouteDefault() {
51+
_focusManager._lastEvent = AccessibilityFocusManagerEvent.requestedFocus;
5152
owner.element.focus();
5253
return true;
5354
}
@@ -81,8 +82,26 @@ typedef _FocusTarget = ({
8182

8283
/// The listener for the "focus" DOM event.
8384
DomEventListener domFocusListener,
85+
86+
/// The listener for the "blur" DOM event.
87+
DomEventListener domBlurListener,
8488
});
8589

90+
enum AccessibilityFocusManagerEvent {
91+
/// No event has happend for the target element.
92+
nothing,
93+
94+
/// The engine requested focus on the DOM element, possibly because the
95+
/// framework requested it.
96+
requestedFocus,
97+
98+
/// Received the DOM "focus" event.
99+
receivedDomFocus,
100+
101+
/// Received the DOM "blur" event.
102+
receivedDomBlur,
103+
}
104+
86105
/// Implements accessibility focus management for arbitrary elements.
87106
///
88107
/// Unlike [Focusable], which implements focus features on [SemanticsObject]s
@@ -99,6 +118,8 @@ class AccessibilityFocusManager {
99118

100119
_FocusTarget? _target;
101120

121+
AccessibilityFocusManagerEvent _lastEvent = AccessibilityFocusManagerEvent.nothing;
122+
102123
// The last focus value set by this focus manager, used to prevent requesting
103124
// focus on the same element repeatedly. Requesting focus on DOM elements is
104125
// not an idempotent operation. If the element is already focused and focus is
@@ -132,6 +153,7 @@ class AccessibilityFocusManager {
132153
semanticsNodeId: semanticsNodeId,
133154
element: previousTarget.element,
134155
domFocusListener: previousTarget.domFocusListener,
156+
domBlurListener: previousTarget.domBlurListener,
135157
);
136158
return;
137159
}
@@ -145,11 +167,14 @@ class AccessibilityFocusManager {
145167
semanticsNodeId: semanticsNodeId,
146168
element: element,
147169
domFocusListener: createDomEventListener((_) => _didReceiveDomFocus()),
170+
domBlurListener: createDomEventListener((_) => _didReceiveDomBlur()),
148171
);
149172
_target = newTarget;
173+
_lastEvent = AccessibilityFocusManagerEvent.nothing;
150174

151175
element.tabIndex = 0;
152176
element.addEventListener('focus', newTarget.domFocusListener);
177+
element.addEventListener('blur', newTarget.domBlurListener);
153178
}
154179

155180
/// Stops managing the focus of the current element, if any.
@@ -164,6 +189,7 @@ class AccessibilityFocusManager {
164189
}
165190

166191
target.element.removeEventListener('focus', target.domFocusListener);
192+
target.element.removeEventListener('blur', target.domBlurListener);
167193
}
168194

169195
void _didReceiveDomFocus() {
@@ -175,11 +201,23 @@ class AccessibilityFocusManager {
175201
return;
176202
}
177203

178-
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
179-
target.semanticsNodeId,
180-
ui.SemanticsAction.focus,
181-
null,
182-
);
204+
// Do not notify the framework if DOM focus was acquired as a result of
205+
// requesting it programmatically. Only notify the framework if the DOM
206+
// focus was initiated by the browser, e.g. as a result of the screen reader
207+
// shifting focus.
208+
if (_lastEvent != AccessibilityFocusManagerEvent.requestedFocus) {
209+
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
210+
target.semanticsNodeId,
211+
ui.SemanticsAction.focus,
212+
null,
213+
);
214+
}
215+
216+
_lastEvent = AccessibilityFocusManagerEvent.receivedDomFocus;
217+
}
218+
219+
void _didReceiveDomBlur() {
220+
_lastEvent = AccessibilityFocusManagerEvent.receivedDomBlur;
183221
}
184222

185223
/// Requests focus or blur on the DOM element.
@@ -240,6 +278,7 @@ class AccessibilityFocusManager {
240278
return;
241279
}
242280

281+
_lastEvent = AccessibilityFocusManagerEvent.requestedFocus;
243282
target.element.focus();
244283
});
245284
}

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

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,9 +1844,22 @@ void _testIncrementables() {
18441844
expect(capturedActions, isEmpty);
18451845

18461846
pumpSemantics(isFocused: true);
1847-
expect(capturedActions, <CapturedAction>[
1848-
(0, ui.SemanticsAction.focus, null),
1849-
]);
1847+
expect(
1848+
reason: 'Framework requested focus. No need to circle the event back to the framework.',
1849+
capturedActions,
1850+
isEmpty,
1851+
);
1852+
capturedActions.clear();
1853+
1854+
element.blur();
1855+
element.focus();
1856+
expect(
1857+
reason: 'Browser-initiated focus even should be communicated to the framework.',
1858+
capturedActions,
1859+
<CapturedAction>[
1860+
(0, ui.SemanticsAction.focus, null),
1861+
],
1862+
);
18501863
capturedActions.clear();
18511864

18521865
pumpSemantics(isFocused: false);
@@ -2189,24 +2202,30 @@ void _testCheckables() {
21892202
expect(capturedActions, isEmpty);
21902203

21912204
pumpSemantics(isFocused: true);
2192-
expect(capturedActions, <CapturedAction>[
2193-
(0, ui.SemanticsAction.focus, null),
2194-
]);
2205+
expect(
2206+
reason: 'Framework requested focus. No need to circle the event back to the framework.',
2207+
capturedActions,
2208+
isEmpty,
2209+
);
21952210
capturedActions.clear();
21962211

2197-
// The framework removes focus from the widget (i.e. "blurs" it). Since the
2198-
// blurring is initiated by the framework, there's no need to send any
2199-
// notifications back to the framework about it.
2200-
pumpSemantics(isFocused: false);
2201-
expect(capturedActions, isEmpty);
2202-
22032212
// The web doesn't send didLoseAccessibilityFocus as on the web,
22042213
// accessibility focus is not observable, only input focus is. As of this
22052214
// writing, there is no SemanticsAction.unfocus action, so the test simply
22062215
// asserts that no actions are being sent as a result of blur.
22072216
element.blur();
22082217
expect(capturedActions, isEmpty);
22092218

2219+
element.focus();
2220+
expect(
2221+
reason: 'Browser-initiated focus even should be communicated to the framework.',
2222+
capturedActions,
2223+
<CapturedAction>[
2224+
(0, ui.SemanticsAction.focus, null),
2225+
],
2226+
);
2227+
capturedActions.clear();
2228+
22102229
semantics().semanticsEnabled = false;
22112230
});
22122231
}
@@ -2370,21 +2389,34 @@ void _testTappable() {
23702389
expect(capturedActions, isEmpty);
23712390

23722391
pumpSemantics(isFocused: true);
2373-
expect(capturedActions, <CapturedAction>[
2374-
(0, ui.SemanticsAction.focus, null),
2375-
]);
2392+
expect(
2393+
reason: 'Framework requested focus. No need to circle the event back to the framework.',
2394+
capturedActions,
2395+
isEmpty,
2396+
);
2397+
expect(domDocument.activeElement, element);
23762398
capturedActions.clear();
23772399

2378-
pumpSemantics(isFocused: false);
2379-
expect(capturedActions, isEmpty);
2380-
23812400
// The web doesn't send didLoseAccessibilityFocus as on the web,
23822401
// accessibility focus is not observable, only input focus is. As of this
23832402
// writing, there is no SemanticsAction.unfocus action, so the test simply
23842403
// asserts that no actions are being sent as a result of blur.
23852404
element.blur();
23862405
expect(capturedActions, isEmpty);
23872406

2407+
element.focus();
2408+
expect(
2409+
reason: 'Browser-initiated focus even should be communicated to the framework.',
2410+
capturedActions,
2411+
<CapturedAction>[
2412+
(0, ui.SemanticsAction.focus, null),
2413+
],
2414+
);
2415+
capturedActions.clear();
2416+
2417+
pumpSemantics(isFocused: false);
2418+
expect(capturedActions, isEmpty);
2419+
23882420
semantics().semanticsEnabled = false;
23892421
});
23902422

@@ -3210,12 +3242,8 @@ void _testDialog() {
32103242
);
32113243
tester.apply();
32123244

3213-
expect(
3214-
capturedActions,
3215-
<CapturedAction>[
3216-
(2, ui.SemanticsAction.focus, null),
3217-
],
3218-
);
3245+
// Auto-focus does not notify the framework about the focused widget.
3246+
expect(capturedActions, isEmpty);
32193247

32203248
semantics().semanticsEnabled = false;
32213249
});
@@ -3272,12 +3300,8 @@ void _testDialog() {
32723300
);
32733301
tester.apply();
32743302

3275-
expect(
3276-
capturedActions,
3277-
<CapturedAction>[
3278-
(3, ui.SemanticsAction.focus, null),
3279-
],
3280-
);
3303+
// Auto-focus does not notify the framework about the focused widget.
3304+
expect(capturedActions, isEmpty);
32813305

32823306
semantics().semanticsEnabled = false;
32833307
});
@@ -3424,10 +3448,7 @@ void _testFocusable() {
34243448
manager.changeFocus(true);
34253449
pumpSemantics(); // triggers post-update callbacks
34263450
expect(domDocument.activeElement, element);
3427-
expect(capturedActions, <CapturedAction>[
3428-
(1, ui.SemanticsAction.focus, null),
3429-
]);
3430-
capturedActions.clear();
3451+
expect(capturedActions, isEmpty);
34313452

34323453
// Give up focus
34333454
manager.changeFocus(false);
@@ -3443,16 +3464,12 @@ void _testFocusable() {
34433464
// writing, there is no SemanticsAction.unfocus action, so the test simply
34443465
// asserts that no actions are being sent as a result of blur.
34453466
expect(capturedActions, isEmpty);
3446-
capturedActions.clear();
34473467

34483468
// Request focus again
34493469
manager.changeFocus(true);
34503470
pumpSemantics(); // triggers post-update callbacks
34513471
expect(domDocument.activeElement, element);
3452-
expect(capturedActions, <CapturedAction>[
3453-
(1, ui.SemanticsAction.focus, null),
3454-
]);
3455-
capturedActions.clear();
3472+
expect(capturedActions, isEmpty);
34563473

34573474
// Double-request focus
34583475
manager.changeFocus(true);
@@ -3463,6 +3480,16 @@ void _testFocusable() {
34633480
capturedActions, isEmpty);
34643481
capturedActions.clear();
34653482

3483+
// Blur and emulate browser requesting focus
3484+
element.blur();
3485+
expect(domDocument.activeElement, isNot(element));
3486+
element.focus();
3487+
expect(domDocument.activeElement, element);
3488+
expect(capturedActions, <CapturedAction>[
3489+
(1, ui.SemanticsAction.focus, null),
3490+
]);
3491+
capturedActions.clear();
3492+
34663493
// Stop managing
34673494
manager.stopManaging();
34683495
pumpSemantics(); // triggers post-update callbacks

0 commit comments

Comments
 (0)