File: focus_manager.js

package info (click to toggle)
chromium 138.0.7204.183-1~deb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-proposed-updates
  • size: 6,080,960 kB
  • sloc: cpp: 34,937,079; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,954; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,811; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (201 lines) | stat: -rw-r--r-- 6,715 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('cr.ui', function() {
  /**
   * Constructor for FocusManager singleton. Checks focus of elements to ensure
   * that elements in "background" pages (i.e., those in a dialog that is not
   * the topmost overlay) do not receive focus.
   * @constructor
   */
  function FocusManager() {}

  FocusManager.prototype = {
    /**
     * Whether focus is being transferred backward or forward through the DOM.
     * @type {boolean}
     * @private
     */
    focusDirBackwards_: false,

    /**
     * Determines whether the |child| is a descendant of |parent| in the page's
     * DOM.
     * @param {Node} parent The parent element to test.
     * @param {Node} child The child element to test.
     * @return {boolean} True if |child| is a descendant of |parent|.
     * @private
     */
    isDescendantOf_(parent, child) {
      return !!parent && !(parent === child) && parent.contains(child);
    },

    /**
     * Returns the parent element containing all elements which should be
     * allowed to receive focus.
     * @return {Element} The element containing focusable elements.
     */
    getFocusParent() {
      return document.body;
    },

    /**
     * Returns the elements on the page capable of receiving focus.
     * @return {Array<Element>} The focusable elements.
     */
    getFocusableElements_() {
      const focusableDiv = this.getFocusParent();

      // Create a TreeWalker object to traverse the DOM from |focusableDiv|.
      const treeWalker = document.createTreeWalker(
          focusableDiv, NodeFilter.SHOW_ELEMENT,
          /** @type {NodeFilter} */
          ({
            acceptNode(node) {
              const style = window.getComputedStyle(node);
              // Reject all hidden nodes. FILTER_REJECT also rejects these
              // nodes' children, so non-hidden elements that are descendants of
              // hidden <div>s will correctly be rejected.
              if (node.hidden || style.display === 'none' ||
                  style.visibility === 'hidden') {
                return NodeFilter.FILTER_REJECT;
              }

              // Skip nodes that cannot receive focus. FILTER_SKIP does not
              // cause this node's children also to be skipped.
              if (node.disabled || node.tabIndex < 0) {
                return NodeFilter.FILTER_SKIP;
              }

              // Accept nodes that are non-hidden and focusable.
              return NodeFilter.FILTER_ACCEPT;
            },
          }),
          false);

      const focusable = [];
      while (treeWalker.nextNode()) {
        focusable.push(treeWalker.currentNode);
      }

      return focusable;
    },

    /**
     * Dispatches an 'elementFocused' event to notify an element that it has
     * received focus. When focus wraps around within the a page, only the
     * element that has focus after the wrapping receives an 'elementFocused'
     * event. This differs from the native 'focus' event which is received by
     * an element outside the page first, followed by a 'focus' on an element
     * within the page after the FocusManager has intervened.
     * @param {EventTarget} element The element that has received focus.
     * @private
     */
    dispatchFocusEvent_(element) {
      cr.dispatchSimpleEvent(element, 'elementFocused', true, false);
    },

    /**
     * Attempts to focus the appropriate element in the current dialog.
     * @private
     */
    setFocus_() {
      const element = this.selectFocusableElement_();
      if (element) {
        element.focus();
        this.dispatchFocusEvent_(element);
      }
    },

    /**
     * Selects first appropriate focusable element according to the
     * current focus direction and element type.  If it is a radio button,
     * checked one is selected from the group.
     * @private
     */
    selectFocusableElement_() {
      // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab"
      // and has caused the focus to be transferred backward, outside of the
      // current dialog. In this case, loop around and try to focus the last
      // element of the dialog; otherwise, try to focus the first element of the
      // dialog.
      const focusableElements = this.getFocusableElements_();
      let element = this.focusDirBackwards_ ? focusableElements.pop() :
                                              focusableElements.shift();
      if (!element) {
        return null;
      }
      if (element.tagName !== 'INPUT' || element.type !== 'radio' ||
          element.name === '') {
        return element;
      }
      if (!element.checked) {
        for (let i = 0; i < focusableElements.length; i++) {
          const e = focusableElements[i];
          if (e && e.tagName === 'INPUT' && e.type === 'radio' &&
              e.name === element.name && e.checked) {
            element = e;
            break;
          }
        }
      }
      return element;
    },

    /**
     * Handler for focus events on the page.
     * @param {Event} event The focus event.
     * @private
     */
    onDocumentFocus_(event) {
      // If the element being focused is a descendant of the currently visible
      // page, focus is valid.
      const targetNode = /** @type {Node} */ (event.target);
      if (this.isDescendantOf_(this.getFocusParent(), targetNode)) {
        this.dispatchFocusEvent_(event.target);
        return;
      }

      // Focus event handlers for descendant elements might dispatch another
      // focus event.
      event.stopPropagation();

      // The target of the focus event is not in the topmost visible page and
      // should not be focused.
      event.target.blur();

      // Attempt to wrap around focus within the current page.
      this.setFocus_();
    },

    /**
     * Handler for keydown events on the page.
     * @param {Event} event The keydown event.
     * @private
     */
    onDocumentKeyDown_(event) {
      /** @const */ const tabKeyCode = 9;

      if (event.keyCode === tabKeyCode) {
        // If the "Shift" key is held, focus is being transferred backward in
        // the page.
        this.focusDirBackwards_ = event.shiftKey ? true : false;
      }
    },

    /**
     * Initializes the FocusManager by listening for events in the document.
     */
    initialize() {
      document.addEventListener(
          'focus', this.onDocumentFocus_.bind(this), true);
      document.addEventListener(
          'keydown', this.onDocumentKeyDown_.bind(this), true);
    },
  };

  return {
    FocusManager: FocusManager,
  };
});