@vaadin/component-base 22.0.0-rc1 → 23.0.0-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export { DirMixin } from './src/dir-mixin.js';
4
4
  export { DisabledMixin } from './src/disabled-mixin.js';
5
5
  export { ElementMixin } from './src/element-mixin.js';
6
6
  export { FocusMixin } from './src/focus-mixin.js';
7
+ export { FocusTrapController } from './src/focus-trap-controller.js';
7
8
  export { KeyboardMixin } from './src/keyboard-mixin.js';
9
+ export { SlotController } from './src/slot-controller.js';
8
10
  export { SlotMixin } from './src/slot-mixin.js';
9
11
  export { TabindexMixin } from './src/tabindex-mixin.js';
package/index.js CHANGED
@@ -4,6 +4,8 @@ export { DirMixin } from './src/dir-mixin.js';
4
4
  export { DisabledMixin } from './src/disabled-mixin.js';
5
5
  export { ElementMixin } from './src/element-mixin.js';
6
6
  export { FocusMixin } from './src/focus-mixin.js';
7
+ export { FocusTrapController } from './src/focus-trap-controller.js';
7
8
  export { KeyboardMixin } from './src/keyboard-mixin.js';
9
+ export { SlotController } from './src/slot-controller.js';
8
10
  export { SlotMixin } from './src/slot-mixin.js';
9
11
  export { TabindexMixin } from './src/tabindex-mixin.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/component-base",
3
- "version": "22.0.0-rc1",
3
+ "version": "23.0.0-alpha2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -41,5 +41,5 @@
41
41
  "@vaadin/testing-helpers": "^0.3.2",
42
42
  "sinon": "^9.2.4"
43
43
  },
44
- "gitHead": "7b6f44bcd2c0fd415028ace666feeb0fedb1d540"
44
+ "gitHead": "070f586dead02ca41b66717820c647f48bf1665f"
45
45
  }
@@ -3,7 +3,7 @@
3
3
  * Copyright (c) 2021 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { GestureEventListeners } from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
6
+ import { addListener } from '@vaadin/component-base/src/gestures.js';
7
7
  import { DisabledMixin } from './disabled-mixin.js';
8
8
  import { KeyboardMixin } from './keyboard-mixin.js';
9
9
 
@@ -19,7 +19,7 @@ import { KeyboardMixin } from './keyboard-mixin.js';
19
19
  * @polymerMixin
20
20
  */
21
21
  export const ActiveMixin = (superclass) =>
22
- class ActiveMixinClass extends DisabledMixin(GestureEventListeners(KeyboardMixin(superclass))) {
22
+ class ActiveMixinClass extends DisabledMixin(KeyboardMixin(superclass)) {
23
23
  /**
24
24
  * An array of activation keys.
25
25
  *
@@ -37,13 +37,13 @@ export const ActiveMixin = (superclass) =>
37
37
  ready() {
38
38
  super.ready();
39
39
 
40
- this._addEventListenerToNode(this, 'down', (event) => {
40
+ addListener(this, 'down', (event) => {
41
41
  if (this._shouldSetActive(event)) {
42
42
  this._setActive(true);
43
43
  }
44
44
  });
45
45
 
46
- this._addEventListenerToNode(this, 'up', () => {
46
+ addListener(this, 'up', () => {
47
47
  this._setActive(false);
48
48
  });
49
49
  }
package/src/async.js CHANGED
@@ -23,17 +23,17 @@
23
23
  // Microtask implemented using Mutation Observer
24
24
  let microtaskCurrHandle = 0;
25
25
  let microtaskLastHandle = 0;
26
- let microtaskCallbacks = [];
26
+ const microtaskCallbacks = [];
27
27
  let microtaskNodeContent = 0;
28
28
  let microtaskScheduled = false;
29
- let microtaskNode = document.createTextNode('');
29
+ const microtaskNode = document.createTextNode('');
30
30
  new window.MutationObserver(microtaskFlush).observe(microtaskNode, { characterData: true });
31
31
 
32
32
  function microtaskFlush() {
33
33
  microtaskScheduled = false;
34
34
  const len = microtaskCallbacks.length;
35
35
  for (let i = 0; i < len; i++) {
36
- let cb = microtaskCallbacks[i];
36
+ const cb = microtaskCallbacks[i];
37
37
  if (cb) {
38
38
  try {
39
39
  cb();
package/src/debounce.js CHANGED
@@ -17,6 +17,7 @@ export class Debouncer {
17
17
  this._callback = null;
18
18
  this._timer = null;
19
19
  }
20
+
20
21
  /**
21
22
  * Sets the scheduler; that is, a module with the Async interface,
22
23
  * a callback and optional arguments to be passed to the run function
@@ -35,6 +36,7 @@ export class Debouncer {
35
36
  this._callback();
36
37
  });
37
38
  }
39
+
38
40
  /**
39
41
  * Cancels an active debouncer and returns a reference to itself.
40
42
  *
@@ -50,6 +52,7 @@ export class Debouncer {
50
52
  debouncerQueue.delete(this);
51
53
  }
52
54
  }
55
+
53
56
  /**
54
57
  * Cancels a debouncer's async callback.
55
58
  *
@@ -61,6 +64,7 @@ export class Debouncer {
61
64
  this._timer = null;
62
65
  }
63
66
  }
67
+
64
68
  /**
65
69
  * Flushes an active debouncer and returns a reference to itself.
66
70
  *
@@ -72,6 +76,7 @@ export class Debouncer {
72
76
  this._callback();
73
77
  }
74
78
  }
79
+
75
80
  /**
76
81
  * Returns true if the debouncer is active.
77
82
  *
@@ -80,6 +85,7 @@ export class Debouncer {
80
85
  isActive() {
81
86
  return this._timer != null;
82
87
  }
88
+
83
89
  /**
84
90
  * Creates a debouncer if no debouncer is passed as a parameter
85
91
  * or it cancels an active debouncer otherwise. The following
@@ -135,16 +141,16 @@ let debouncerQueue = new Set();
135
141
  * @param {!Debouncer} debouncer Debouncer to enqueue
136
142
  * @return {void}
137
143
  */
138
- export const enqueueDebouncer = function (debouncer) {
144
+ export function enqueueDebouncer(debouncer) {
139
145
  debouncerQueue.add(debouncer);
140
- };
146
+ }
141
147
 
142
148
  /**
143
149
  * Flushes any enqueued debouncers
144
150
  *
145
151
  * @return {boolean} Returns whether any debouncers were flushed
146
152
  */
147
- export const flushDebouncers = function () {
153
+ export function flushDebouncers() {
148
154
  const didFlush = Boolean(debouncerQueue.size);
149
155
  // If new debouncers are added while flushing, Set.forEach will ensure
150
156
  // newly added ones are also flushed
@@ -158,7 +164,7 @@ export const flushDebouncers = function () {
158
164
  }
159
165
  });
160
166
  return didFlush;
161
- };
167
+ }
162
168
 
163
169
  export const flush = () => {
164
170
  let debouncers;
package/src/dir-helper.js CHANGED
@@ -57,8 +57,9 @@ class DirHelper {
57
57
  return element.scrollWidth - element.clientWidth + scrollLeft;
58
58
  case 'reverse':
59
59
  return element.scrollWidth - element.clientWidth - scrollLeft;
60
+ default:
61
+ return scrollLeft;
60
62
  }
61
- return scrollLeft;
62
63
  }
63
64
 
64
65
  /**
package/src/dir-mixin.js CHANGED
@@ -9,29 +9,29 @@ import { DirHelper } from './dir-helper.js';
9
9
  * Array of Vaadin custom element classes that have been subscribed to the dir changes.
10
10
  */
11
11
  const directionSubscribers = [];
12
- const directionUpdater = function () {
12
+ function directionUpdater() {
13
13
  const documentDir = getDocumentDir();
14
14
  directionSubscribers.forEach((element) => {
15
15
  alignDirs(element, documentDir);
16
16
  });
17
- };
17
+ }
18
18
 
19
19
  let scrollType;
20
20
 
21
21
  const directionObserver = new MutationObserver(directionUpdater);
22
22
  directionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
23
23
 
24
- const alignDirs = function (element, documentDir, elementDir = element.getAttribute('dir')) {
24
+ function alignDirs(element, documentDir, elementDir = element.getAttribute('dir')) {
25
25
  if (documentDir) {
26
26
  element.setAttribute('dir', documentDir);
27
27
  } else if (elementDir != null) {
28
28
  element.removeAttribute('dir');
29
29
  }
30
- };
30
+ }
31
31
 
32
- const getDocumentDir = function () {
32
+ function getDocumentDir() {
33
33
  return document.documentElement.getAttribute('dir');
34
- };
34
+ }
35
35
 
36
36
  /**
37
37
  * A mixin to handle `dir` attribute based on the one set on the `<html>` element.
@@ -32,7 +32,7 @@ const registered = new Set();
32
32
  export const ElementMixin = (superClass) =>
33
33
  class VaadinElementMixin extends DirMixin(superClass) {
34
34
  static get version() {
35
- return '22.0.0-rc1';
35
+ return '23.0.0-alpha2';
36
36
  }
37
37
 
38
38
  /** @protected */
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { ReactiveController } from 'lit';
7
+
8
+ /**
9
+ * A controller for trapping focus within a DOM node.
10
+ */
11
+ declare class FocusTrapController implements ReactiveController {
12
+ constructor(node: HTMLElement);
13
+
14
+ hostConnected(): void;
15
+
16
+ hostDisconnected(): void;
17
+
18
+ /**
19
+ * The controller host element.
20
+ */
21
+ host: HTMLElement;
22
+
23
+ /**
24
+ * Activates a focus trap for a DOM node that will prevent focus from escaping the node.
25
+ * The trap can be deactivated with the `.releaseFocus()` method.
26
+ *
27
+ * If focus is initially outside the trap, the method will move focus inside,
28
+ * on the first focusable element of the trap in the tab order.
29
+ * The first focusable element can be the trap node itself if it is focusable
30
+ * and comes first in the tab order.
31
+ */
32
+ trapFocus(trapNode: HTMLElement): void;
33
+
34
+ /**
35
+ * Deactivates the focus trap set with the `.trapFocus()` method
36
+ * so that it becomes possible to tab outside the trap node.
37
+ */
38
+ releaseFocus(): void;
39
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { getFocusableElements, isElementFocused } from './focus-utils.js';
7
+
8
+ /**
9
+ * A controller for trapping focus within a DOM node.
10
+ */
11
+ export class FocusTrapController {
12
+ /**
13
+ * @param {HTMLElement} host
14
+ */
15
+ constructor(host) {
16
+ /**
17
+ * The controller host element.
18
+ *
19
+ * @type {HTMLElement}
20
+ */
21
+ this.host = host;
22
+
23
+ /**
24
+ * A node for trapping focus in.
25
+ *
26
+ * @type {HTMLElement | null}
27
+ * @private
28
+ */
29
+ this.__trapNode = null;
30
+
31
+ this.__onKeyDown = this.__onKeyDown.bind(this);
32
+ }
33
+
34
+ hostConnected() {
35
+ document.addEventListener('keydown', this.__onKeyDown);
36
+ }
37
+
38
+ hostDisconnected() {
39
+ document.removeEventListener('keydown', this.__onKeyDown);
40
+ }
41
+
42
+ /**
43
+ * Activates a focus trap for a DOM node that will prevent focus from escaping the node.
44
+ * The trap can be deactivated with the `.releaseFocus()` method.
45
+ *
46
+ * If focus is initially outside the trap, the method will move focus inside,
47
+ * on the first focusable element of the trap in the tab order.
48
+ * The first focusable element can be the trap node itself if it is focusable
49
+ * and comes first in the tab order.
50
+ *
51
+ * If there are no focusable elements, the method will throw an exception
52
+ * and the trap will not be set.
53
+ *
54
+ * @param {HTMLElement} trapNode
55
+ */
56
+ trapFocus(trapNode) {
57
+ this.__trapNode = trapNode;
58
+
59
+ if (this.__focusableElements.length === 0) {
60
+ this.__trapNode = null;
61
+ throw new Error('The trap node should have at least one focusable descendant or be focusable itself.');
62
+ }
63
+
64
+ if (this.__focusedElementIndex === -1) {
65
+ this.__focusableElements[0].focus();
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Deactivates the focus trap set with the `.trapFocus()` method
71
+ * so that it becomes possible to tab outside the trap node.
72
+ */
73
+ releaseFocus() {
74
+ this.__trapNode = null;
75
+ }
76
+
77
+ /**
78
+ * A `keydown` event handler that manages tabbing navigation when the trap is enabled.
79
+ *
80
+ * - Moves focus to the next focusable element of the trap on `Tab` press.
81
+ * When no next element to focus, the method moves focus to the first focusable element.
82
+ * - Moves focus to the prev focusable element of the trap on `Shift+Tab` press.
83
+ * When no prev element to focus, the method moves focus to the last focusable element.
84
+ *
85
+ * @param {KeyboardEvent} event
86
+ * @private
87
+ */
88
+ __onKeyDown(event) {
89
+ if (!this.__trapNode) {
90
+ return;
91
+ }
92
+
93
+ if (event.key === 'Tab') {
94
+ event.preventDefault();
95
+
96
+ const backward = event.shiftKey;
97
+ this.__focusNextElement(backward);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * - Moves focus to the next focusable element if `backward === false`.
103
+ * When no next element to focus, the method moves focus to the first focusable element.
104
+ * - Moves focus to the prev focusable element if `backward === true`.
105
+ * When no prev element to focus the method moves focus to the last focusable element.
106
+ *
107
+ * If no focusable elements, the method returns immediately.
108
+ *
109
+ * @param {boolean} backward
110
+ * @private
111
+ */
112
+ __focusNextElement(backward = false) {
113
+ const focusableElements = this.__focusableElements;
114
+ const step = backward ? -1 : 1;
115
+ const currentIndex = this.__focusedElementIndex;
116
+ const nextIndex = (focusableElements.length + currentIndex + step) % focusableElements.length;
117
+ focusableElements[nextIndex].focus();
118
+ }
119
+
120
+ /**
121
+ * An array of tab-ordered focusable elements inside the trap node.
122
+ *
123
+ * @return {HTMLElement[]}
124
+ * @private
125
+ */
126
+ get __focusableElements() {
127
+ return getFocusableElements(this.__trapNode);
128
+ }
129
+
130
+ /**
131
+ * The index of the element inside the trap node that currently has focus.
132
+ *
133
+ * @return {HTMLElement | undefined}
134
+ * @private
135
+ */
136
+ get __focusedElementIndex() {
137
+ return this.__focusableElements.findIndex(isElementFocused);
138
+ }
139
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * Returns true if the element is hidden, false otherwise.
9
+ *
10
+ * An element is treated as hidden when any of the following conditions are met:
11
+ * - the element itself or one of its ancestors has `display: none`.
12
+ * - the element has or inherits `visibility: hidden`.
13
+ */
14
+ export declare function isElementHidden(element: HTMLElement): boolean;
15
+
16
+ /**
17
+ * Returns true if the element is focusable, otherwise false.
18
+ *
19
+ * The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
20
+ * However, there isn't a definite list, it's up to the browser.
21
+ * The only standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html,
22
+ * according to which the only elements that have a `focus()` method are:
23
+ * - HTMLInputElement
24
+ * - HTMLSelectElement
25
+ * - HTMLTextAreaElement
26
+ * - HTMLAnchorElement
27
+ *
28
+ * This notably omits HTMLButtonElement and HTMLAreaElement.
29
+ * Referring to these tests with tabbables in different browsers
30
+ * http://allyjs.io/data-tables/focusable.html
31
+ */
32
+ export declare function isElementFocusable(element: HTMLElement): boolean;
33
+
34
+ /**
35
+ * Returns true if the element is focused, false otherwise.
36
+ */
37
+ export declare function isElementFocused(element: HTMLElement): boolean;
38
+
39
+ /**
40
+ * Returns a tab-ordered array of focusable elements for a root element.
41
+ * The resulting array will include the root element if it is focusable.
42
+ *
43
+ * The method traverses nodes in shadow DOM trees too if any.
44
+ */
45
+ export declare function getFocusableElements(element: HTMLElement): HTMLElement[];
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * Returns true if the element is hidden directly with `display: none` or `visibility: hidden`,
9
+ * false otherwise.
10
+ *
11
+ * The method doesn't traverse the element's ancestors, it only checks for the CSS properties
12
+ * set directly to or inherited by the element.
13
+ *
14
+ * @param {HTMLElement} element
15
+ * @return {boolean}
16
+ */
17
+ function isElementHiddenDirectly(element) {
18
+ // Check inline style first to save a re-flow.
19
+ const style = element.style;
20
+ if (style.visibility === 'hidden' || style.display === 'none') {
21
+ return true;
22
+ }
23
+
24
+ const computedStyle = window.getComputedStyle(element);
25
+ if (computedStyle.visibility === 'hidden' || computedStyle.display === 'none') {
26
+ return true;
27
+ }
28
+
29
+ return false;
30
+ }
31
+
32
+ /**
33
+ * Returns the normalized element tabindex. If not focusable, returns -1.
34
+ * It checks for the attribute "tabindex" instead of the element property
35
+ * `tabIndex` since browsers assign different values to it.
36
+ * e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
37
+ *
38
+ * @param {HTMLElement} element
39
+ * @return {number}
40
+ */
41
+ function normalizeTabIndex(element) {
42
+ if (!isElementFocusable(element) || isElementHiddenDirectly(element)) {
43
+ return -1;
44
+ }
45
+
46
+ const tabIndex = element.getAttribute('tabindex') || 0;
47
+ return Number(tabIndex);
48
+ }
49
+
50
+ /**
51
+ * Returns if element `a` has lower tab order compared to element `b`
52
+ * (both elements are assumed to be focusable and tabbable).
53
+ * Elements with tabindex = 0 have lower tab order compared to elements
54
+ * with tabindex > 0.
55
+ * If both have same tabindex, it returns false.
56
+ *
57
+ * @param {HTMLElement} a
58
+ * @param {HTMLElement} b
59
+ * @return {boolean}
60
+ */
61
+ function hasLowerTabOrder(a, b) {
62
+ // Normalize tabIndexes
63
+ // e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
64
+ const ati = Math.max(a.tabIndex, 0);
65
+ const bti = Math.max(b.tabIndex, 0);
66
+ return ati === 0 || bti === 0 ? bti > ati : ati > bti;
67
+ }
68
+
69
+ /**
70
+ * Merge sort iterator, merges the two arrays into one, sorted by tabindex.
71
+ *
72
+ * @param {HTMLElement[]} left
73
+ * @param {HTMLElement[]} right
74
+ * @return {HTMLElement[]}
75
+ */
76
+ function mergeSortByTabIndex(left, right) {
77
+ const result = [];
78
+ while (left.length > 0 && right.length > 0) {
79
+ if (hasLowerTabOrder(left[0], right[0])) {
80
+ result.push(right.shift());
81
+ } else {
82
+ result.push(left.shift());
83
+ }
84
+ }
85
+
86
+ return result.concat(left, right);
87
+ }
88
+
89
+ /**
90
+ * Sorts an array of elements by tabindex. Returns a new array.
91
+ *
92
+ * @param {HTMLElement[]} elements
93
+ * @return {HTMLElement[]}
94
+ */
95
+ function sortElementsByTabIndex(elements) {
96
+ // Implement a merge sort as Array.prototype.sort does a non-stable sort
97
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
98
+ const len = elements.length;
99
+ if (len < 2) {
100
+ return elements;
101
+ }
102
+ const pivot = Math.ceil(len / 2);
103
+ const left = sortElementsByTabIndex(elements.slice(0, pivot));
104
+ const right = sortElementsByTabIndex(elements.slice(pivot));
105
+
106
+ return mergeSortByTabIndex(left, right);
107
+ }
108
+
109
+ /**
110
+ * Searches for nodes that are tabbable and adds them to the `result` array.
111
+ * Returns if the `result` array needs to be sorted by tabindex.
112
+ *
113
+ * @param {Node} node The starting point for the search; added to `result` if tabbable.
114
+ * @param {HTMLElement[]} result
115
+ * @return {boolean}
116
+ * @private
117
+ */
118
+ function collectFocusableNodes(node, result) {
119
+ if (node.nodeType !== Node.ELEMENT_NODE) {
120
+ // Don't traverse children if the node is not an HTML element.
121
+ return false;
122
+ }
123
+
124
+ const element = /** @type {HTMLElement} */ (node);
125
+ const tabIndex = normalizeTabIndex(element);
126
+ let needsSort = tabIndex > 0;
127
+ if (tabIndex >= 0) {
128
+ result.push(element);
129
+ }
130
+
131
+ let children = [];
132
+ if (element.localName === 'slot') {
133
+ children = element.assignedNodes({ flatten: true });
134
+ } else {
135
+ // Use shadow root if possible, will check for distributed nodes.
136
+ children = (element.shadowRoot || element).children;
137
+ }
138
+ [...children].forEach((child) => {
139
+ // Ensure method is always invoked to collect focusable children.
140
+ needsSort = collectFocusableNodes(child, result) || needsSort;
141
+ });
142
+ return needsSort;
143
+ }
144
+
145
+ /**
146
+ * Returns true if the element is hidden, false otherwise.
147
+ *
148
+ * An element is treated as hidden when any of the following conditions are met:
149
+ * - the element itself or one of its ancestors has `display: none`.
150
+ * - the element has or inherits `visibility: hidden`.
151
+ *
152
+ * @param {HTMLElement} element
153
+ * @return {boolean}
154
+ */
155
+ export function isElementHidden(element) {
156
+ // `offsetParent` is `null` when the element itself
157
+ // or one of its ancestors is hidden with `display: none`.
158
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
159
+ if (element.offsetParent === null) {
160
+ return true;
161
+ }
162
+
163
+ return isElementHiddenDirectly(element);
164
+ }
165
+
166
+ /**
167
+ * Returns true if the element is focusable, otherwise false.
168
+ *
169
+ * The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
170
+ * However, there isn't a definite list, it's up to the browser.
171
+ * The only standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html,
172
+ * according to which the only elements that have a `focus()` method are:
173
+ * - HTMLInputElement
174
+ * - HTMLSelectElement
175
+ * - HTMLTextAreaElement
176
+ * - HTMLAnchorElement
177
+ *
178
+ * This notably omits HTMLButtonElement and HTMLAreaElement.
179
+ * Referring to these tests with tabbables in different browsers
180
+ * http://allyjs.io/data-tables/focusable.html
181
+ *
182
+ * @param {HTMLElement} element
183
+ * @return {boolean}
184
+ */
185
+ export function isElementFocusable(element) {
186
+ // The element cannot be focused if its `tabindex` attribute is set to `-1`.
187
+ if (element.matches('[tabindex="-1"]')) {
188
+ return false;
189
+ }
190
+
191
+ // Elements that cannot be focused if they have a `disabled` attribute.
192
+ if (element.matches('input, select, textarea, button, object')) {
193
+ return element.matches(':not([disabled])');
194
+ }
195
+
196
+ // Elements that can be focused even if they have a `disabled` attribute.
197
+ return element.matches('a[href], area[href], iframe, [tabindex], [contentEditable]');
198
+ }
199
+
200
+ /**
201
+ * Returns true if the element is focused, false otherwise.
202
+ *
203
+ * @param {HTMLElement} element
204
+ * @return {boolean}
205
+ */
206
+ export function isElementFocused(element) {
207
+ return element.getRootNode().activeElement === element;
208
+ }
209
+
210
+ /**
211
+ * Returns a tab-ordered array of focusable elements for a root element.
212
+ * The resulting array will include the root element if it is focusable.
213
+ *
214
+ * The method traverses nodes in shadow DOM trees too if any.
215
+ *
216
+ * @param {HTMLElement} element
217
+ * @return {HTMLElement[]}
218
+ */
219
+ export function getFocusableElements(element) {
220
+ const focusableElements = [];
221
+ const needsSortByTabIndex = collectFocusableNodes(element, focusableElements);
222
+ // If there is at least one element with tabindex > 0, we need to sort
223
+ // the final array by tabindex.≈
224
+ if (needsSortByTabIndex) {
225
+ return sortElementsByTabIndex(focusableElements);
226
+ }
227
+ return focusableElements;
228
+ }