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

Commit 767c25d

Browse files
authored
Mark the Flutter Views as focusable by setting a tabindex value. (#50876)
Mark the Flutter View as focusable by setting a tabindex value. * When a given flutter view is focused its tabindex will be `-1` * When a given flutter view is not focused its tabindex will be `0` * When semantics are enabled no tabindex will be set. Relevant Issues are: * Design doc: https://flutter.dev/go/focus-management * Focus in web multiview: flutter/flutter#137443 * Platform dispatcher changes: #49841 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 8e78d3e commit 767c25d

File tree

7 files changed

+237
-148
lines changed

7 files changed

+237
-148
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
7979
_addLocaleChangedListener();
8080
registerHotRestartListener(dispose);
8181
AppLifecycleState.instance.addListener(_setAppLifecycleState);
82-
ViewFocusBinding.instance.addListener(invokeOnViewFocusChange);
82+
_viewFocusBinding.init();
8383
domDocument.body?.prepend(accessibilityPlaceholder);
8484
_onViewDisposedListener = viewManager.onViewDisposed.listen((_) {
8585
// Send a metrics changed event to the framework when a view is disposed.
@@ -123,7 +123,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
123123
_removeLocaleChangedListener();
124124
HighContrastSupport.instance.removeListener(_updateHighContrast);
125125
AppLifecycleState.instance.removeListener(_setAppLifecycleState);
126-
ViewFocusBinding.instance.removeListener(invokeOnViewFocusChange);
126+
_viewFocusBinding.dispose();
127127
accessibilityPlaceholder.remove();
128128
_onViewDisposedListener.cancel();
129129
viewManager.dispose();
@@ -228,6 +228,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
228228
}
229229
}
230230

231+
late final ViewFocusBinding _viewFocusBinding =
232+
ViewFocusBinding(viewManager, invokeOnViewFocusChange);
233+
231234
@override
232235
ui.ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
233236
ui.ViewFocusChangeCallback? _onViewFocusChange;
@@ -248,7 +251,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
248251
);
249252
}
250253

251-
252254
@override
253255
void requestViewFocusChange({
254256
required int viewId,
@@ -258,7 +260,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
258260
// TODO(tugorez): implement this method. At the moment will be a no op call.
259261
}
260262

261-
262263
/// A set of views which have rendered in the current `onBeginFrame` or
263264
/// `onDrawFrame` scope.
264265
Set<ui.FlutterView>? _viewsRenderedInCurrentFrame;

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

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,36 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'package:ui/src/engine.dart';
67
import 'package:ui/ui.dart' as ui;
78

89
/// Tracks the [FlutterView]s focus changes.
910
final class ViewFocusBinding {
10-
/// Creates a [ViewFocusBinding] instance.
11-
ViewFocusBinding._();
11+
ViewFocusBinding(this._viewManager, this._onViewFocusChange);
1212

13-
/// The [ViewFocusBinding] singleton.
14-
static final ViewFocusBinding instance = ViewFocusBinding._();
13+
final FlutterViewManager _viewManager;
14+
final ui.ViewFocusChangeCallback _onViewFocusChange;
1515

16-
final List<ui.ViewFocusChangeCallback> _listeners = <ui.ViewFocusChangeCallback>[];
1716
int? _lastViewId;
1817
ui.ViewFocusDirection _viewFocusDirection = ui.ViewFocusDirection.forward;
1918

20-
/// Subscribes the [listener] to [ui.ViewFocusEvent] events.
21-
void addListener(ui.ViewFocusChangeCallback listener) {
22-
if (_listeners.isEmpty) {
23-
domDocument.body?.addEventListener(_keyDown, _handleKeyDown);
24-
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
25-
domDocument.body?.addEventListener(_focusin, _handleFocusin);
26-
domDocument.body?.addEventListener(_focusout, _handleFocusout);
27-
}
28-
_listeners.add(listener);
29-
}
19+
StreamSubscription<int>? _onViewCreatedListener;
3020

31-
/// Removes the [listener] from the [ui.ViewFocusEvent] events subscription.
32-
void removeListener(ui.ViewFocusChangeCallback listener) {
33-
_listeners.remove(listener);
34-
if (_listeners.isEmpty) {
35-
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
36-
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
37-
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
38-
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
39-
}
21+
void init() {
22+
domDocument.body?.addEventListener(_keyDown, _handleKeyDown);
23+
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
24+
domDocument.body?.addEventListener(_focusin, _handleFocusin);
25+
domDocument.body?.addEventListener(_focusout, _handleFocusout);
26+
_onViewCreatedListener = _viewManager.onViewCreated.listen(_handleViewCreated);
4027
}
4128

42-
void _notify(ui.ViewFocusEvent event) {
43-
for (final ui.ViewFocusChangeCallback listener in _listeners) {
44-
listener(event);
45-
}
29+
void dispose() {
30+
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
31+
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
32+
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
33+
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
34+
_onViewCreatedListener?.cancel();
4635
}
4736

4837
late final DomEventListener _handleFocusin = createDomEventListener((DomEvent event) {
@@ -71,6 +60,7 @@ final class ViewFocusBinding {
7160
if (viewId == _lastViewId) {
7261
return;
7362
}
63+
7464
final ui.ViewFocusEvent event;
7565
if (viewId == null) {
7666
event = ui.ViewFocusEvent(
@@ -85,18 +75,44 @@ final class ViewFocusBinding {
8575
direction: _viewFocusDirection,
8676
);
8777
}
78+
_maybeMarkViewAsFocusable(_lastViewId, reachableByKeyboard: true);
79+
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: false);
8880
_lastViewId = viewId;
89-
_notify(event);
81+
_onViewFocusChange(event);
82+
}
83+
84+
int? _viewId(DomElement? element) {
85+
final DomElement? rootElement = element?.closest(DomManager.flutterViewTagName);
86+
if (rootElement == null) {
87+
return null;
88+
}
89+
return _viewManager.viewIdForRootElement(rootElement);
9090
}
9191

92-
static int? _viewId(DomElement? element) {
93-
final DomElement? viewElement = element?.closest(
94-
DomManager.flutterViewTagName,
95-
);
96-
final String? viewIdAttribute = viewElement?.getAttribute(
97-
GlobalHtmlAttributes.flutterViewIdAttributeName,
98-
);
99-
return viewIdAttribute == null ? null : int.tryParse(viewIdAttribute);
92+
void _handleViewCreated(int viewId) {
93+
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: true);
94+
}
95+
96+
void _maybeMarkViewAsFocusable(
97+
int? viewId, {
98+
required bool reachableByKeyboard,
99+
}) {
100+
if (viewId == null) {
101+
return;
102+
}
103+
104+
final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;
105+
if (EngineSemantics.instance.semanticsEnabled) {
106+
rootElement?.removeAttribute('tabindex');
107+
} else {
108+
// A tabindex with value zero means the DOM element can be reached by using
109+
// the keyboard (tab, shift + tab). When its value is -1 it is still focusable
110+
// but can't be focused by the result of keyboard events This is specially
111+
// important when the semantics tree is enabled as it puts DOM nodes inside
112+
// the flutter view and having it with a zero tabindex messes the focus
113+
// traversal order when pressing tab or shift tab.
114+
rootElement?.setAttribute('tabindex', reachableByKeyboard ? 0 : -1);
115+
}
100116
}
101117

102118
static const String _focusin = 'focusin';

lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ class FlutterViewManager {
9696
return _jsViewOptions[viewId];
9797
}
9898

99+
/// Returns the [viewId] if [rootElement] corresponds to any of the [views].
100+
int? viewIdForRootElement(DomElement rootElement) {
101+
for(final EngineFlutterView view in views) {
102+
if (view.dom.rootElement == rootElement) {
103+
return view.viewId;
104+
}
105+
}
106+
return null;
107+
}
108+
99109
void dispose() {
100110
// We need to call `toList()` in order to avoid concurrent modification
101111
// inside the loop.

lib/web_ui/lib/src/engine/view_embedder/style_manager.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ void applyGlobalCssRulesToSheet(
116116
// Hide placeholder text
117117
'$cssSelectorPrefix .flt-text-editing::placeholder {'
118118
' opacity: 0;'
119+
'}'
120+
121+
// Hide outline when the flutter-view root element is focused.
122+
'$cssSelectorPrefix:focus {'
123+
' outline: none;'
119124
'}',
120125
);
121126

0 commit comments

Comments
 (0)