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

Commit dec7eb9

Browse files
authored
Flutter views can gain focus (#54985)
I am unsure why the `tabindex` was removed when semantics were enabled. It seems this change was made without a clear explanation (by me). This PR shouldn't cause any issues as Flutter Views already have a tabindex, we're not adding a new one. This change is necessary because the semantics text strategy refocuses the view on deactivation, requiring the Flutter view to be focusable. ThIs PR is a requirement to enable #54966. flutter/flutter#153022 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent b79353d commit dec7eb9

File tree

2 files changed

+33
-49
lines changed

2 files changed

+33
-49
lines changed

lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,20 @@ final class ViewFocusBinding {
2727
StreamSubscription<int>? _onViewCreatedListener;
2828

2929
void init() {
30+
// We need a global listener here to know if the user was pressing "shift"
31+
// when the Flutter view receives focus, to move the Flutter focus to the
32+
// *last* focusable element.
3033
domDocument.body?.addEventListener(_keyDown, _handleKeyDown);
3134
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
32-
domDocument.body?.addEventListener(_focusin, _handleFocusin);
33-
domDocument.body?.addEventListener(_focusout, _handleFocusout);
35+
36+
// If so, update `_handleViewCreated` and add a `_handleViewDisposed` to attach
37+
// and remove the focus/blur listener.
3438
_onViewCreatedListener = _viewManager.onViewCreated.listen(_handleViewCreated);
3539
}
3640

3741
void dispose() {
3842
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
3943
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
40-
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
41-
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
4244
_onViewCreatedListener?.cancel();
4345
}
4446

@@ -48,13 +50,14 @@ final class ViewFocusBinding {
4850
}
4951
final DomElement? viewElement = _viewManager[viewId]?.dom.rootElement;
5052

51-
if (state == ui.ViewFocusState.focused) {
52-
// Only move the focus to the flutter view if nothing inside it is focused already.
53-
if (viewId != _viewId(domDocument.activeElement)) {
54-
viewElement?.focusWithoutScroll();
55-
}
56-
} else {
57-
viewElement?.blur();
53+
switch (state) {
54+
case ui.ViewFocusState.focused:
55+
// Only move the focus to the flutter view if nothing inside it is focused already.
56+
if (viewId != _viewId(domDocument.activeElement)) {
57+
viewElement?.focusWithoutScroll();
58+
}
59+
case ui.ViewFocusState.unfocused:
60+
viewElement?.blur();
5861
}
5962
}
6063

@@ -115,8 +118,8 @@ final class ViewFocusBinding {
115118
direction: _viewFocusDirection,
116119
);
117120
}
118-
_maybeMarkViewAsFocusable(_lastViewId, reachableByKeyboard: true);
119-
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: false);
121+
_updateViewKeyboardReachability(_lastViewId, reachable: true);
122+
_updateViewKeyboardReachability(viewId, reachable: false);
120123
_lastViewId = viewId;
121124
_onViewFocusChange(event);
122125
}
@@ -127,29 +130,32 @@ final class ViewFocusBinding {
127130
}
128131

129132
void _handleViewCreated(int viewId) {
130-
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: true);
133+
final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;
134+
135+
rootElement?.addEventListener(_focusin, _handleFocusin);
136+
rootElement?.addEventListener(_focusout, _handleFocusout);
137+
138+
_updateViewKeyboardReachability(viewId, reachable: true);
131139
}
132140

133-
void _maybeMarkViewAsFocusable(
141+
// Controls whether the Flutter view identified by [viewId] is reachable by
142+
// keyboard.
143+
void _updateViewKeyboardReachability(
134144
int? viewId, {
135-
required bool reachableByKeyboard,
145+
required bool reachable,
136146
}) {
137147
if (viewId == null) {
138148
return;
139149
}
140150

141151
final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;
142-
if (EngineSemantics.instance.semanticsEnabled) {
143-
rootElement?.removeAttribute('tabindex');
144-
} else {
145-
// A tabindex with value zero means the DOM element can be reached by using
146-
// the keyboard (tab, shift + tab). When its value is -1 it is still focusable
147-
// but can't be focused by the result of keyboard events This is specially
148-
// important when the semantics tree is enabled as it puts DOM nodes inside
149-
// the flutter view and having it with a zero tabindex messes the focus
150-
// traversal order when pressing tab or shift tab.
151-
rootElement?.setAttribute('tabindex', reachableByKeyboard ? 0 : -1);
152-
}
152+
// A tabindex with value zero means the DOM element can be reached using the
153+
// keyboard (tab, shift + tab). When its value is -1 it is still focusable
154+
// but can't be focused as the result of keyboard events. This is specially
155+
// important when the semantics tree is enabled as it puts DOM nodes inside
156+
// the flutter view and having it with a zero tabindex messes the focus
157+
// traversal order when pressing tab or shift tab.
158+
rootElement?.setAttribute('tabindex', reachable ? 0 : -1);
153159
}
154160

155161
static const String _focusin = 'focusin';

lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,28 +68,6 @@ void testMain() {
6868
expect(view2.dom.rootElement.getAttribute('tabindex'), '0');
6969
});
7070

71-
test('never marks the views as focusable with semantincs enabled', () async {
72-
EngineSemantics.instance.semanticsEnabled = true;
73-
74-
final EngineFlutterView view1 = createAndRegisterView(dispatcher);
75-
final EngineFlutterView view2 = createAndRegisterView(dispatcher);
76-
77-
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
78-
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);
79-
80-
view1.dom.rootElement.focusWithoutScroll();
81-
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
82-
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);
83-
84-
view2.dom.rootElement.focusWithoutScroll();
85-
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
86-
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);
87-
88-
view2.dom.rootElement.blur();
89-
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
90-
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);
91-
});
92-
9371
test('fires a focus event - a view was focused', () async {
9472
final EngineFlutterView view = createAndRegisterView(dispatcher);
9573

0 commit comments

Comments
 (0)