@vaadin/component-base 24.0.0 → 24.1.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.
@@ -1,30 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import type { Constructor } from '@open-wc/dedupe-mixin';
7
-
8
- /**
9
- * A mixin to handle `focused` and `focus-ring` attributes based on focus.
10
- */
11
- export declare function FocusMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<FocusMixinClass> & T;
12
-
13
- export declare class FocusMixinClass {
14
- protected readonly _keyboardActive: boolean;
15
-
16
- /**
17
- * Override to change how focused and focus-ring attributes are set.
18
- */
19
- protected _setFocused(focused: boolean): void;
20
-
21
- /**
22
- * Override to define if the field receives focus based on the event.
23
- */
24
- protected _shouldSetFocus(event: FocusEvent): boolean;
25
-
26
- /**
27
- * Override to define if the field loses focus based on the event.
28
- */
29
- protected _shouldRemoveFocus(event: FocusEvent): boolean;
30
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
7
- import { isKeyboardActive } from './focus-utils.js';
8
-
9
- /**
10
- * A mixin to handle `focused` and `focus-ring` attributes based on focus.
11
- *
12
- * @polymerMixin
13
- */
14
- export const FocusMixin = dedupingMixin(
15
- (superclass) =>
16
- class FocusMixinClass extends superclass {
17
- /**
18
- * @protected
19
- * @return {boolean}
20
- */
21
- get _keyboardActive() {
22
- return isKeyboardActive();
23
- }
24
-
25
- /** @protected */
26
- ready() {
27
- this.addEventListener('focusin', (e) => {
28
- if (this._shouldSetFocus(e)) {
29
- this._setFocused(true);
30
- }
31
- });
32
-
33
- this.addEventListener('focusout', (e) => {
34
- if (this._shouldRemoveFocus(e)) {
35
- this._setFocused(false);
36
- }
37
- });
38
-
39
- // In super.ready() other 'focusin' and 'focusout' listeners might be
40
- // added, so we call it after our own ones to ensure they execute first.
41
- // Issue to watch out: when incorrect, <vaadin-combo-box> refocuses the
42
- // input field on iOS after "Done" is pressed.
43
- super.ready();
44
- }
45
-
46
- /** @protected */
47
- disconnectedCallback() {
48
- super.disconnectedCallback();
49
-
50
- // In non-Chrome browsers, blur does not fire on the element when it is disconnected.
51
- // reproducible in `<vaadin-date-picker>` when closing on `Cancel` or `Today` click.
52
- if (this.hasAttribute('focused')) {
53
- this._setFocused(false);
54
- }
55
- }
56
-
57
- /**
58
- * Override to change how focused and focus-ring attributes are set.
59
- *
60
- * @param {boolean} focused
61
- * @protected
62
- */
63
- _setFocused(focused) {
64
- this.toggleAttribute('focused', focused);
65
-
66
- // Focus-ring is true when the element was focused from the keyboard.
67
- // Focus Ring [A11ycasts]: https://youtu.be/ilj2P5-5CjI
68
- this.toggleAttribute('focus-ring', focused && this._keyboardActive);
69
- }
70
-
71
- /**
72
- * Override to define if the field receives focus based on the event.
73
- *
74
- * @param {FocusEvent} _event
75
- * @return {boolean}
76
- * @protected
77
- */
78
- _shouldSetFocus(_event) {
79
- return true;
80
- }
81
-
82
- /**
83
- * Override to define if the field loses focus based on the event.
84
- *
85
- * @param {FocusEvent} _event
86
- * @return {boolean}
87
- * @protected
88
- */
89
- _shouldRemoveFocus(_event) {
90
- return true;
91
- }
92
- },
93
- );
@@ -1,39 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import type { ReactiveController } from 'lit';
7
-
8
- /**
9
- * A controller for trapping focus within a DOM node.
10
- */
11
- export class FocusTrapController implements ReactiveController {
12
- /**
13
- * The controller host element.
14
- */
15
- host: HTMLElement;
16
-
17
- constructor(node: HTMLElement);
18
-
19
- hostConnected(): void;
20
-
21
- hostDisconnected(): void;
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
- }
@@ -1,155 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2023 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
- const instances = [];
9
-
10
- /**
11
- * A controller for trapping focus within a DOM node.
12
- */
13
- export class FocusTrapController {
14
- /**
15
- * @param {HTMLElement} host
16
- */
17
- constructor(host) {
18
- /**
19
- * The controller host element.
20
- *
21
- * @type {HTMLElement}
22
- */
23
- this.host = host;
24
-
25
- /**
26
- * A node for trapping focus in.
27
- *
28
- * @type {HTMLElement | null}
29
- * @private
30
- */
31
- this.__trapNode = null;
32
-
33
- this.__onKeyDown = this.__onKeyDown.bind(this);
34
- }
35
-
36
- /**
37
- * An array of tab-ordered focusable elements inside the trap node.
38
- *
39
- * @return {HTMLElement[]}
40
- * @private
41
- */
42
- get __focusableElements() {
43
- return getFocusableElements(this.__trapNode);
44
- }
45
-
46
- /**
47
- * The index of the element inside the trap node that currently has focus.
48
- *
49
- * @return {HTMLElement | undefined}
50
- * @private
51
- */
52
- get __focusedElementIndex() {
53
- const focusableElements = this.__focusableElements;
54
- return focusableElements.indexOf(focusableElements.filter(isElementFocused).pop());
55
- }
56
-
57
- hostConnected() {
58
- document.addEventListener('keydown', this.__onKeyDown);
59
- }
60
-
61
- hostDisconnected() {
62
- document.removeEventListener('keydown', this.__onKeyDown);
63
- }
64
-
65
- /**
66
- * Activates a focus trap for a DOM node that will prevent focus from escaping the node.
67
- * The trap can be deactivated with the `.releaseFocus()` method.
68
- *
69
- * If focus is initially outside the trap, the method will move focus inside,
70
- * on the first focusable element of the trap in the tab order.
71
- * The first focusable element can be the trap node itself if it is focusable
72
- * and comes first in the tab order.
73
- *
74
- * If there are no focusable elements, the method will throw an exception
75
- * and the trap will not be set.
76
- *
77
- * @param {HTMLElement} trapNode
78
- */
79
- trapFocus(trapNode) {
80
- this.__trapNode = trapNode;
81
-
82
- if (this.__focusableElements.length === 0) {
83
- this.__trapNode = null;
84
- throw new Error('The trap node should have at least one focusable descendant or be focusable itself.');
85
- }
86
-
87
- instances.push(this);
88
-
89
- if (this.__focusedElementIndex === -1) {
90
- this.__focusableElements[0].focus();
91
- }
92
- }
93
-
94
- /**
95
- * Deactivates the focus trap set with the `.trapFocus()` method
96
- * so that it becomes possible to tab outside the trap node.
97
- */
98
- releaseFocus() {
99
- this.__trapNode = null;
100
-
101
- instances.pop();
102
- }
103
-
104
- /**
105
- * A `keydown` event handler that manages tabbing navigation when the trap is enabled.
106
- *
107
- * - Moves focus to the next focusable element of the trap on `Tab` press.
108
- * When no next element to focus, the method moves focus to the first focusable element.
109
- * - Moves focus to the prev focusable element of the trap on `Shift+Tab` press.
110
- * When no prev element to focus, the method moves focus to the last focusable element.
111
- *
112
- * @param {KeyboardEvent} event
113
- * @private
114
- */
115
- __onKeyDown(event) {
116
- if (!this.__trapNode) {
117
- return;
118
- }
119
-
120
- // Only handle events for the last instance
121
- if (this !== Array.from(instances).pop()) {
122
- return;
123
- }
124
-
125
- if (event.key === 'Tab') {
126
- event.preventDefault();
127
-
128
- const backward = event.shiftKey;
129
- this.__focusNextElement(backward);
130
- }
131
- }
132
-
133
- /**
134
- * - Moves focus to the next focusable element if `backward === false`.
135
- * When no next element to focus, the method moves focus to the first focusable element.
136
- * - Moves focus to the prev focusable element if `backward === true`.
137
- * When no prev element to focus the method moves focus to the last focusable element.
138
- *
139
- * If no focusable elements, the method returns immediately.
140
- *
141
- * @param {boolean} backward
142
- * @private
143
- */
144
- __focusNextElement(backward = false) {
145
- const focusableElements = this.__focusableElements;
146
- const step = backward ? -1 : 1;
147
- const currentIndex = this.__focusedElementIndex;
148
- const nextIndex = (focusableElements.length + currentIndex + step) % focusableElements.length;
149
- const element = focusableElements[nextIndex];
150
- element.focus();
151
- if (element.localName === 'input') {
152
- element.select();
153
- }
154
- }
155
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2023 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 window has received a keydown
9
- * event since the last mousedown event.
10
- */
11
- export declare function isKeyboardActive(): boolean;
12
-
13
- /**
14
- * Returns true if the element is hidden, false otherwise.
15
- *
16
- * An element is treated as hidden when any of the following conditions are met:
17
- * - the element itself or one of its ancestors has `display: none`.
18
- * - the element has or inherits `visibility: hidden`.
19
- */
20
- export declare function isElementHidden(element: HTMLElement): boolean;
21
-
22
- /**
23
- * Returns true if the element is focusable, otherwise false.
24
- *
25
- * The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
26
- * However, there isn't a definite list, it's up to the browser.
27
- * The only standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html,
28
- * according to which the only elements that have a `focus()` method are:
29
- * - HTMLInputElement
30
- * - HTMLSelectElement
31
- * - HTMLTextAreaElement
32
- * - HTMLAnchorElement
33
- *
34
- * This notably omits HTMLButtonElement and HTMLAreaElement.
35
- * Referring to these tests with tabbables in different browsers
36
- * http://allyjs.io/data-tables/focusable.html
37
- */
38
- export declare function isElementFocusable(element: HTMLElement): boolean;
39
-
40
- /**
41
- * Returns true if the element is focused, false otherwise.
42
- */
43
- export declare function isElementFocused(element: HTMLElement): boolean;
44
-
45
- /**
46
- * Returns a tab-ordered array of focusable elements for a root element.
47
- * The resulting array will include the root element if it is focusable.
48
- *
49
- * The method traverses nodes in shadow DOM trees too if any.
50
- */
51
- export declare function getFocusableElements(element: HTMLElement): HTMLElement[];
@@ -1,260 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
-
7
- // We consider the keyboard to be active if the window has received a keydown
8
- // event since the last mousedown event.
9
- let keyboardActive = false;
10
-
11
- // Listen for top-level keydown and mousedown events.
12
- // Use capture phase so we detect events even if they're handled.
13
- window.addEventListener(
14
- 'keydown',
15
- () => {
16
- keyboardActive = true;
17
- },
18
- { capture: true },
19
- );
20
-
21
- window.addEventListener(
22
- 'mousedown',
23
- () => {
24
- keyboardActive = false;
25
- },
26
- { capture: true },
27
- );
28
-
29
- /**
30
- * Returns true if the window has received a keydown
31
- * event since the last mousedown event.
32
- *
33
- * @return {boolean}
34
- */
35
- export function isKeyboardActive() {
36
- return keyboardActive;
37
- }
38
-
39
- /**
40
- * Returns true if the element is hidden directly with `display: none` or `visibility: hidden`,
41
- * false otherwise.
42
- *
43
- * The method doesn't traverse the element's ancestors, it only checks for the CSS properties
44
- * set directly to or inherited by the element.
45
- *
46
- * @param {HTMLElement} element
47
- * @return {boolean}
48
- */
49
- function isElementHiddenDirectly(element) {
50
- // Check inline style first to save a re-flow.
51
- const style = element.style;
52
- if (style.visibility === 'hidden' || style.display === 'none') {
53
- return true;
54
- }
55
-
56
- const computedStyle = window.getComputedStyle(element);
57
- if (computedStyle.visibility === 'hidden' || computedStyle.display === 'none') {
58
- return true;
59
- }
60
-
61
- return false;
62
- }
63
-
64
- /**
65
- * Returns if element `a` has lower tab order compared to element `b`
66
- * (both elements are assumed to be focusable and tabbable).
67
- * Elements with tabindex = 0 have lower tab order compared to elements
68
- * with tabindex > 0.
69
- * If both have same tabindex, it returns false.
70
- *
71
- * @param {HTMLElement} a
72
- * @param {HTMLElement} b
73
- * @return {boolean}
74
- */
75
- function hasLowerTabOrder(a, b) {
76
- // Normalize tabIndexes
77
- // e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
78
- const ati = Math.max(a.tabIndex, 0);
79
- const bti = Math.max(b.tabIndex, 0);
80
- return ati === 0 || bti === 0 ? bti > ati : ati > bti;
81
- }
82
-
83
- /**
84
- * Merge sort iterator, merges the two arrays into one, sorted by tabindex.
85
- *
86
- * @param {HTMLElement[]} left
87
- * @param {HTMLElement[]} right
88
- * @return {HTMLElement[]}
89
- */
90
- function mergeSortByTabIndex(left, right) {
91
- const result = [];
92
- while (left.length > 0 && right.length > 0) {
93
- if (hasLowerTabOrder(left[0], right[0])) {
94
- result.push(right.shift());
95
- } else {
96
- result.push(left.shift());
97
- }
98
- }
99
-
100
- return result.concat(left, right);
101
- }
102
-
103
- /**
104
- * Sorts an array of elements by tabindex. Returns a new array.
105
- *
106
- * @param {HTMLElement[]} elements
107
- * @return {HTMLElement[]}
108
- */
109
- function sortElementsByTabIndex(elements) {
110
- // Implement a merge sort as Array.prototype.sort does a non-stable sort
111
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
112
- const len = elements.length;
113
- if (len < 2) {
114
- return elements;
115
- }
116
- const pivot = Math.ceil(len / 2);
117
- const left = sortElementsByTabIndex(elements.slice(0, pivot));
118
- const right = sortElementsByTabIndex(elements.slice(pivot));
119
-
120
- return mergeSortByTabIndex(left, right);
121
- }
122
-
123
- /**
124
- * Returns true if the element is hidden, false otherwise.
125
- *
126
- * An element is treated as hidden when any of the following conditions are met:
127
- * - the element itself or one of its ancestors has `display: none`.
128
- * - the element has or inherits `visibility: hidden`.
129
- *
130
- * @param {HTMLElement} element
131
- * @return {boolean}
132
- */
133
- export function isElementHidden(element) {
134
- // `offsetParent` is `null` when the element itself
135
- // or one of its ancestors is hidden with `display: none`.
136
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
137
- if (element.offsetParent === null) {
138
- return true;
139
- }
140
-
141
- return isElementHiddenDirectly(element);
142
- }
143
-
144
- /**
145
- * Returns true if the element is focusable, otherwise false.
146
- *
147
- * The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
148
- * However, there isn't a definite list, it's up to the browser.
149
- * The only standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html,
150
- * according to which the only elements that have a `focus()` method are:
151
- * - HTMLInputElement
152
- * - HTMLSelectElement
153
- * - HTMLTextAreaElement
154
- * - HTMLAnchorElement
155
- *
156
- * This notably omits HTMLButtonElement and HTMLAreaElement.
157
- * Referring to these tests with tabbables in different browsers
158
- * http://allyjs.io/data-tables/focusable.html
159
- *
160
- * @param {HTMLElement} element
161
- * @return {boolean}
162
- */
163
- export function isElementFocusable(element) {
164
- // The element cannot be focused if its `tabindex` attribute is set to `-1`.
165
- if (element.matches('[tabindex="-1"]')) {
166
- return false;
167
- }
168
-
169
- // Elements that cannot be focused if they have a `disabled` attribute.
170
- if (element.matches('input, select, textarea, button, object')) {
171
- return element.matches(':not([disabled])');
172
- }
173
-
174
- // Elements that can be focused even if they have a `disabled` attribute.
175
- return element.matches('a[href], area[href], iframe, [tabindex], [contentEditable]');
176
- }
177
-
178
- /**
179
- * Returns true if the element is focused, false otherwise.
180
- *
181
- * @param {HTMLElement} element
182
- * @return {boolean}
183
- */
184
- export function isElementFocused(element) {
185
- return element.getRootNode().activeElement === element;
186
- }
187
-
188
- /**
189
- * Returns the normalized element tabindex. If not focusable, returns -1.
190
- * It checks for the attribute "tabindex" instead of the element property
191
- * `tabIndex` since browsers assign different values to it.
192
- * e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
193
- *
194
- * @param {HTMLElement} element
195
- * @return {number}
196
- */
197
- function normalizeTabIndex(element) {
198
- if (!isElementFocusable(element)) {
199
- return -1;
200
- }
201
-
202
- const tabIndex = element.getAttribute('tabindex') || 0;
203
- return Number(tabIndex);
204
- }
205
-
206
- /**
207
- * Searches for nodes that are tabbable and adds them to the `result` array.
208
- * Returns if the `result` array needs to be sorted by tabindex.
209
- *
210
- * @param {Node} node The starting point for the search; added to `result` if tabbable.
211
- * @param {HTMLElement[]} result
212
- * @return {boolean}
213
- * @private
214
- */
215
- function collectFocusableNodes(node, result) {
216
- if (node.nodeType !== Node.ELEMENT_NODE || isElementHiddenDirectly(node)) {
217
- // Don't traverse children if the node is not an HTML element or not visible.
218
- return false;
219
- }
220
-
221
- const element = /** @type {HTMLElement} */ (node);
222
- const tabIndex = normalizeTabIndex(element);
223
- let needsSort = tabIndex > 0;
224
- if (tabIndex >= 0) {
225
- result.push(element);
226
- }
227
-
228
- let children = [];
229
- if (element.localName === 'slot') {
230
- children = element.assignedNodes({ flatten: true });
231
- } else {
232
- // Use shadow root if possible, will check for distributed nodes.
233
- children = (element.shadowRoot || element).children;
234
- }
235
- [...children].forEach((child) => {
236
- // Ensure method is always invoked to collect focusable children.
237
- needsSort = collectFocusableNodes(child, result) || needsSort;
238
- });
239
- return needsSort;
240
- }
241
-
242
- /**
243
- * Returns a tab-ordered array of focusable elements for a root element.
244
- * The resulting array will include the root element if it is focusable.
245
- *
246
- * The method traverses nodes in shadow DOM trees too if any.
247
- *
248
- * @param {HTMLElement} element
249
- * @return {HTMLElement[]}
250
- */
251
- export function getFocusableElements(element) {
252
- const focusableElements = [];
253
- const needsSortByTabIndex = collectFocusableNodes(element, focusableElements);
254
- // If there is at least one element with tabindex > 0,
255
- // we need to sort the final array by tabindex.
256
- if (needsSortByTabIndex) {
257
- return sortElementsByTabIndex(focusableElements);
258
- }
259
- return focusableElements;
260
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2022 - 2023 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import type { Constructor } from '@open-wc/dedupe-mixin';
7
- import type { KeyboardMixinClass } from './keyboard-mixin.js';
8
-
9
- /**
10
- * A mixin for navigating items with keyboard.
11
- */
12
- export declare function KeyboardDirectionMixin<T extends Constructor<HTMLElement>>(
13
- base: T,
14
- ): Constructor<KeyboardDirectionMixinClass> & Constructor<KeyboardMixinClass> & T;
15
-
16
- export declare class KeyboardDirectionMixinClass {
17
- protected readonly focused: Element | null;
18
-
19
- protected readonly _vertical: boolean;
20
-
21
- /**
22
- * Returns index of the next item that satisfies the given condition,
23
- * based on the index of the current item and a numeric increment.
24
- */
25
- protected _getAvailableIndex(
26
- items: Element[],
27
- index: number,
28
- increment: number,
29
- condition: (item: Element) => boolean,
30
- ): number;
31
-
32
- /**
33
- * Focus the item at given index. Override this method to add custom logic.
34
- */
35
- protected _focus(index: number, navigating: boolean): void;
36
-
37
- /**
38
- * Focus the given item. Override this method to add custom logic.
39
- */
40
- protected _focusItem(item: Element, navigating: boolean): void;
41
- }