@vaadin/a11y-base 24.1.0-alpha1
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/LICENSE +190 -0
- package/README.md +14 -0
- package/index.d.ts +18 -0
- package/index.js +18 -0
- package/package.json +44 -0
- package/src/active-mixin.d.ts +41 -0
- package/src/active-mixin.js +106 -0
- package/src/announce.d.ts +10 -0
- package/src/announce.js +47 -0
- package/src/aria-hidden.d.ts +46 -0
- package/src/aria-hidden.js +240 -0
- package/src/aria-modal-controller.d.ts +34 -0
- package/src/aria-modal-controller.js +49 -0
- package/src/delegate-focus-mixin.d.ts +48 -0
- package/src/delegate-focus-mixin.js +228 -0
- package/src/disabled-mixin.d.ts +20 -0
- package/src/disabled-mixin.js +62 -0
- package/src/field-aria-controller.d.ts +56 -0
- package/src/field-aria-controller.js +172 -0
- package/src/focus-mixin.d.ts +30 -0
- package/src/focus-mixin.js +93 -0
- package/src/focus-trap-controller.d.ts +39 -0
- package/src/focus-trap-controller.js +155 -0
- package/src/focus-utils.d.ts +51 -0
- package/src/focus-utils.js +260 -0
- package/src/keyboard-direction-mixin.d.ts +41 -0
- package/src/keyboard-direction-mixin.js +192 -0
- package/src/keyboard-mixin.d.ts +40 -0
- package/src/keyboard-mixin.js +85 -0
- package/src/list-mixin.d.ts +57 -0
- package/src/list-mixin.js +354 -0
- package/src/tabindex-mixin.d.ts +36 -0
- package/src/tabindex-mixin.js +78 -0
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* A controller for managing ARIA attributes for a field element:
|
|
9
|
+
* either the component itself or slotted `<input>` element.
|
|
10
|
+
*/
|
|
11
|
+
export class FieldAriaController {
|
|
12
|
+
/**
|
|
13
|
+
* The controller host element.
|
|
14
|
+
*/
|
|
15
|
+
host: HTMLElement;
|
|
16
|
+
|
|
17
|
+
constructor(host: HTMLElement);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sets a target element to which ARIA attributes are added.
|
|
21
|
+
*/
|
|
22
|
+
setTarget(target: HTMLElement): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Toggles the `aria-required` attribute on the target element
|
|
26
|
+
* if the target is the host component (e.g. a field group).
|
|
27
|
+
* Otherwise, it does nothing.
|
|
28
|
+
*/
|
|
29
|
+
setRequired(required: boolean): void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Links the target element with a slotted label element
|
|
33
|
+
* via the target's attribute `aria-labelledby`.
|
|
34
|
+
*
|
|
35
|
+
* To unlink the previous slotted label element, pass `null` as `labelId`.
|
|
36
|
+
*/
|
|
37
|
+
setLabelId(labelId: string | null): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Links the target element with a slotted error element via the target's attribute:
|
|
41
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
42
|
+
* - `aria-describedby` otherwise.
|
|
43
|
+
*
|
|
44
|
+
* To unlink the previous slotted error element, pass `null` as `errorId`.
|
|
45
|
+
*/
|
|
46
|
+
setErrorId(errorId: string | null): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Links the target element with a slotted helper element via the target's attribute:
|
|
50
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
51
|
+
* - `aria-describedby` otherwise.
|
|
52
|
+
*
|
|
53
|
+
* To unlink the previous slotted helper element, pass `null` as `helperId`.
|
|
54
|
+
*/
|
|
55
|
+
setHelperId(helperId: string | null): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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 { addValueToAttribute, removeValueFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A controller for managing ARIA attributes for a field element:
|
|
10
|
+
* either the component itself or slotted `<input>` element.
|
|
11
|
+
*/
|
|
12
|
+
export class FieldAriaController {
|
|
13
|
+
constructor(host) {
|
|
14
|
+
this.host = host;
|
|
15
|
+
this.__required = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `true` if the target element is the host component itself, `false` otherwise.
|
|
20
|
+
*
|
|
21
|
+
* @return {boolean}
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
get __isGroupField() {
|
|
25
|
+
return this.__target === this.host;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sets a target element to which ARIA attributes are added.
|
|
30
|
+
*
|
|
31
|
+
* @param {HTMLElement} target
|
|
32
|
+
*/
|
|
33
|
+
setTarget(target) {
|
|
34
|
+
this.__target = target;
|
|
35
|
+
this.__setAriaRequiredAttribute(this.__required);
|
|
36
|
+
this.__setLabelIdToAriaAttribute(this.__labelId);
|
|
37
|
+
this.__setErrorIdToAriaAttribute(this.__errorId);
|
|
38
|
+
this.__setHelperIdToAriaAttribute(this.__helperId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Toggles the `aria-required` attribute on the target element
|
|
43
|
+
* if the target is the host component (e.g. a field group).
|
|
44
|
+
* Otherwise, it does nothing.
|
|
45
|
+
*
|
|
46
|
+
* @param {boolean} required
|
|
47
|
+
*/
|
|
48
|
+
setRequired(required) {
|
|
49
|
+
this.__setAriaRequiredAttribute(required);
|
|
50
|
+
this.__required = required;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Links the target element with a slotted label element
|
|
55
|
+
* via the target's attribute `aria-labelledby`.
|
|
56
|
+
*
|
|
57
|
+
* To unlink the previous slotted label element, pass `null` as `labelId`.
|
|
58
|
+
*
|
|
59
|
+
* @param {string | null} labelId
|
|
60
|
+
*/
|
|
61
|
+
setLabelId(labelId) {
|
|
62
|
+
this.__setLabelIdToAriaAttribute(labelId, this.__labelId);
|
|
63
|
+
this.__labelId = labelId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Links the target element with a slotted error element via the target's attribute:
|
|
68
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
69
|
+
* - `aria-describedby` otherwise.
|
|
70
|
+
*
|
|
71
|
+
* To unlink the previous slotted error element, pass `null` as `errorId`.
|
|
72
|
+
*
|
|
73
|
+
* @param {string | null} errorId
|
|
74
|
+
*/
|
|
75
|
+
setErrorId(errorId) {
|
|
76
|
+
this.__setErrorIdToAriaAttribute(errorId, this.__errorId);
|
|
77
|
+
this.__errorId = errorId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Links the target element with a slotted helper element via the target's attribute:
|
|
82
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
83
|
+
* - `aria-describedby` otherwise.
|
|
84
|
+
*
|
|
85
|
+
* To unlink the previous slotted helper element, pass `null` as `helperId`.
|
|
86
|
+
*
|
|
87
|
+
* @param {string | null} helperId
|
|
88
|
+
*/
|
|
89
|
+
setHelperId(helperId) {
|
|
90
|
+
this.__setHelperIdToAriaAttribute(helperId, this.__helperId);
|
|
91
|
+
this.__helperId = helperId;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string | null | undefined} labelId
|
|
96
|
+
* @param {string | null | undefined} oldLabelId
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
__setLabelIdToAriaAttribute(labelId, oldLabelId) {
|
|
100
|
+
this.__setAriaAttributeId('aria-labelledby', labelId, oldLabelId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string | null | undefined} errorId
|
|
105
|
+
* @param {string | null | undefined} oldErrorId
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
__setErrorIdToAriaAttribute(errorId, oldErrorId) {
|
|
109
|
+
// For groups, add all IDs to aria-labelledby rather than aria-describedby -
|
|
110
|
+
// that should guarantee that it's announced when the group is entered.
|
|
111
|
+
if (this.__isGroupField) {
|
|
112
|
+
this.__setAriaAttributeId('aria-labelledby', errorId, oldErrorId);
|
|
113
|
+
} else {
|
|
114
|
+
this.__setAriaAttributeId('aria-describedby', errorId, oldErrorId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string | null | undefined} helperId
|
|
120
|
+
* @param {string | null | undefined} oldHelperId
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
__setHelperIdToAriaAttribute(helperId, oldHelperId) {
|
|
124
|
+
// For groups, add all IDs to aria-labelledby rather than aria-describedby -
|
|
125
|
+
// that should guarantee that it's announced when the group is entered.
|
|
126
|
+
if (this.__isGroupField) {
|
|
127
|
+
this.__setAriaAttributeId('aria-labelledby', helperId, oldHelperId);
|
|
128
|
+
} else {
|
|
129
|
+
this.__setAriaAttributeId('aria-describedby', helperId, oldHelperId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {boolean} required
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
__setAriaRequiredAttribute(required) {
|
|
138
|
+
if (!this.__target) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (['input', 'textarea'].includes(this.__target.localName)) {
|
|
143
|
+
// Native <input> or <textarea>, required is enough
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (required) {
|
|
148
|
+
this.__target.setAttribute('aria-required', 'true');
|
|
149
|
+
} else {
|
|
150
|
+
this.__target.removeAttribute('aria-required');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string | null | undefined} newId
|
|
156
|
+
* @param {string | null | undefined} oldId
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
__setAriaAttributeId(attr, newId, oldId) {
|
|
160
|
+
if (!this.__target) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (oldId) {
|
|
165
|
+
removeValueFromAttribute(this.__target, attr, oldId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (newId) {
|
|
169
|
+
addValueToAttribute(this.__target, attr, newId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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[];
|