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' ;
56import 'package:ui/src/engine.dart' ;
67import 'package:ui/ui.dart' as ui;
78
89/// Tracks the [FlutterView] s focus changes.
910final 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' ;
0 commit comments