@vaadin/a11y-base 25.2.7 → 25.3.0-alpha10
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 +1 -1
- package/index.js +1 -1
- package/package.json +5 -5
- package/src/aria-element-reference.d.ts +28 -0
- package/src/aria-element-reference.js +43 -0
- package/src/aria-modal-controller.js +7 -4
- package/src/disabled-mixin.js +2 -5
- package/src/field-aria-controller.d.ts +16 -13
- package/src/field-aria-controller.js +95 -93
- package/src/focus-trap-controller.d.ts +5 -0
- package/src/focus-trap-controller.js +29 -36
- package/src/focus-utils.d.ts +3 -2
- package/src/focus-utils.js +3 -7
- package/src/list-mixin.d.ts +5 -0
- package/src/list-mixin.js +12 -7
- package/src/aria-id-reference.d.ts +0 -41
- package/src/aria-id-reference.js +0 -154
package/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { DisabledMixin } from './src/disabled-mixin.js';
|
|
|
5
5
|
export { FieldAriaController } from './src/field-aria-controller.js';
|
|
6
6
|
export { FocusMixin } from './src/focus-mixin.js';
|
|
7
7
|
export {
|
|
8
|
-
|
|
8
|
+
getTabbableElements,
|
|
9
9
|
isElementFocusable,
|
|
10
10
|
isElementFocused,
|
|
11
11
|
isElementHidden,
|
package/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { FocusMixin } from './src/focus-mixin.js';
|
|
|
7
7
|
export { FocusTrapController } from './src/focus-trap-controller.js';
|
|
8
8
|
export { FocusRestorationController } from './src/focus-restoration-controller.js';
|
|
9
9
|
export {
|
|
10
|
-
|
|
10
|
+
getTabbableElements,
|
|
11
11
|
isElementFocusable,
|
|
12
12
|
isElementFocused,
|
|
13
13
|
isElementHidden,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/a11y-base",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.3.0-alpha10",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
35
|
-
"@vaadin/component-base": "
|
|
35
|
+
"@vaadin/component-base": "25.3.0-alpha10",
|
|
36
36
|
"lit": "^3.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@vaadin/chai-plugins": "
|
|
40
|
-
"@vaadin/test-runner-commands": "
|
|
39
|
+
"@vaadin/chai-plugins": "25.3.0-alpha10",
|
|
40
|
+
"@vaadin/test-runner-commands": "25.3.0-alpha10",
|
|
41
41
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
42
42
|
"sinon": "^22.0.0"
|
|
43
43
|
},
|
|
44
44
|
"customElements": "custom-elements.json",
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "f2833abdf9b613fa0d0ed216830e3f4de87b7dac"
|
|
46
46
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2000 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adds the element to the target's `ariaDescribedByElements` or
|
|
9
|
+
* `ariaLabelledByElements` property, based on the given attribute.
|
|
10
|
+
* Unlike ID references, element references also work across shadow roots.
|
|
11
|
+
*/
|
|
12
|
+
export function addAriaElementReference(
|
|
13
|
+
target: HTMLElement,
|
|
14
|
+
attr: 'aria-describedby' | 'aria-labelledby',
|
|
15
|
+
element: HTMLElement,
|
|
16
|
+
): void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Removes the element from the target's `ariaDescribedByElements` or
|
|
20
|
+
* `ariaLabelledByElements` property, based on the given attribute.
|
|
21
|
+
* When the last element is removed, the property is reset to `null`
|
|
22
|
+
* so it no longer overrides the content attribute.
|
|
23
|
+
*/
|
|
24
|
+
export function removeAriaElementReference(
|
|
25
|
+
target: HTMLElement,
|
|
26
|
+
attr: 'aria-describedby' | 'aria-labelledby',
|
|
27
|
+
element: HTMLElement,
|
|
28
|
+
): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2000 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ARIA_REFERENCE_PROPERTIES = {
|
|
8
|
+
'aria-describedby': 'ariaDescribedByElements',
|
|
9
|
+
'aria-labelledby': 'ariaLabelledByElements',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adds the element to the target's `ariaDescribedByElements` or
|
|
14
|
+
* `ariaLabelledByElements` property, based on the given attribute.
|
|
15
|
+
* Unlike ID references, element references also work across shadow roots.
|
|
16
|
+
*
|
|
17
|
+
* @param {HTMLElement} target
|
|
18
|
+
* @param {'aria-describedby' | 'aria-labelledby'} attr
|
|
19
|
+
* @param {HTMLElement} element
|
|
20
|
+
*/
|
|
21
|
+
export function addAriaElementReference(target, attr, element) {
|
|
22
|
+
const property = ARIA_REFERENCE_PROPERTIES[attr];
|
|
23
|
+
const elements = new Set(target[property]);
|
|
24
|
+
elements.add(element);
|
|
25
|
+
target[property] = [...elements];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Removes the element from the target's `ariaDescribedByElements` or
|
|
30
|
+
* `ariaLabelledByElements` property, based on the given attribute.
|
|
31
|
+
* When the last element is removed, the property is reset to `null`
|
|
32
|
+
* so it no longer overrides the content attribute.
|
|
33
|
+
*
|
|
34
|
+
* @param {HTMLElement} target
|
|
35
|
+
* @param {'aria-describedby' | 'aria-labelledby'} attr
|
|
36
|
+
* @param {HTMLElement} element
|
|
37
|
+
*/
|
|
38
|
+
export function removeAriaElementReference(target, attr, element) {
|
|
39
|
+
const property = ARIA_REFERENCE_PROPERTIES[attr];
|
|
40
|
+
const elements = new Set(target[property]);
|
|
41
|
+
elements.delete(element);
|
|
42
|
+
target[property] = elements.size > 0 ? [...elements] : null;
|
|
43
|
+
}
|
|
@@ -13,6 +13,9 @@ import { hideOthers } from './aria-hidden.js';
|
|
|
13
13
|
* consumer web component. This is done in to ensure the controller only does one thing.
|
|
14
14
|
*/
|
|
15
15
|
export class AriaModalController {
|
|
16
|
+
/** @type {Function | null} */
|
|
17
|
+
#showOthers = null;
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* @param {HTMLElement} host
|
|
18
21
|
*/
|
|
@@ -42,7 +45,7 @@ export class AriaModalController {
|
|
|
42
45
|
*/
|
|
43
46
|
showModal() {
|
|
44
47
|
const targets = this.callback();
|
|
45
|
-
this
|
|
48
|
+
this.#showOthers = hideOthers(targets);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/**
|
|
@@ -50,9 +53,9 @@ export class AriaModalController {
|
|
|
50
53
|
* controller hosts on the page activated by using `showModal()` call.
|
|
51
54
|
*/
|
|
52
55
|
close() {
|
|
53
|
-
if (this
|
|
54
|
-
this
|
|
55
|
-
this
|
|
56
|
+
if (this.#showOthers) {
|
|
57
|
+
this.#showOthers();
|
|
58
|
+
this.#showOthers = null;
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|
package/src/disabled-mixin.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A mixin to provide disabled property for field components.
|
|
@@ -38,11 +39,7 @@ const DisabledMixinImplementation = (superclass) => {
|
|
|
38
39
|
* @protected
|
|
39
40
|
*/
|
|
40
41
|
_setAriaDisabled(disabled) {
|
|
41
|
-
|
|
42
|
-
this.setAttribute('aria-disabled', 'true');
|
|
43
|
-
} else {
|
|
44
|
-
this.removeAttribute('aria-disabled');
|
|
45
|
-
}
|
|
42
|
+
setOrRemoveAttribute(this, 'aria-disabled', disabled);
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
/**
|
|
@@ -33,31 +33,34 @@ export class FieldAriaController {
|
|
|
33
33
|
*
|
|
34
34
|
* To remove the attribute, pass `null` as `label`.
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
setLabel(label: string | null): void;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Links the target element
|
|
40
|
-
* via the target's attribute `aria-labelledby`.
|
|
39
|
+
* Links the target element to one or more elements via the `aria-labelledby` attribute.
|
|
41
40
|
*
|
|
42
|
-
*
|
|
41
|
+
* Pass a space-delimited list of IDs, or `null` to remove the previously linked IDs.
|
|
43
42
|
*/
|
|
44
|
-
|
|
43
|
+
setLabelledBy(labelledBy: string | null): void;
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
|
-
* Links the target element
|
|
48
|
-
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
49
|
-
* - `aria-describedby` otherwise.
|
|
46
|
+
* Links the target element to one or more elements via the `aria-describedby` attribute.
|
|
50
47
|
*
|
|
51
|
-
*
|
|
48
|
+
* @param describedBy the space-delimited list of IDs,
|
|
49
|
+
* or `null` to remove the previously linked IDs
|
|
50
|
+
*/
|
|
51
|
+
setDescribedBy(describedBy: string | null): void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Links the target element to a slotted error element via the `aria-describedby` attribute.
|
|
55
|
+
*
|
|
56
|
+
* Pass the ID of the error element, or `null` to remove the previously linked ID.
|
|
52
57
|
*/
|
|
53
58
|
setErrorId(errorId: string | null): void;
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
|
-
* Links the target element
|
|
57
|
-
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
58
|
-
* - `aria-describedby` otherwise.
|
|
61
|
+
* Links the target element to a slotted helper element via the `aria-describedby` attribute.
|
|
59
62
|
*
|
|
60
|
-
*
|
|
63
|
+
* Pass the ID of the helper element, or `null` to remove the previously linked ID.
|
|
61
64
|
*/
|
|
62
65
|
setHelperId(helperId: string | null): void;
|
|
63
66
|
}
|
|
@@ -3,16 +3,46 @@
|
|
|
3
3
|
* Copyright (c) 2021 - 2026 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
addValuesToAttribute,
|
|
8
|
+
removeValuesFromAttribute,
|
|
9
|
+
setOrRemoveAttribute,
|
|
10
|
+
} from '@vaadin/component-base/src/dom-utils.js';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* A controller for managing ARIA attributes for a field element:
|
|
10
14
|
* either the component itself or slotted `<input>` element.
|
|
11
15
|
*/
|
|
12
16
|
export class FieldAriaController {
|
|
17
|
+
/** @type {HTMLElement | undefined} */
|
|
18
|
+
#target;
|
|
19
|
+
|
|
20
|
+
/** @type {boolean} */
|
|
21
|
+
#required = false;
|
|
22
|
+
|
|
23
|
+
/** @type {string | null | undefined} */
|
|
24
|
+
#label;
|
|
25
|
+
|
|
26
|
+
/** @type {string | null | undefined} */
|
|
27
|
+
#labelledBy;
|
|
28
|
+
|
|
29
|
+
/** @type {string | null | undefined} */
|
|
30
|
+
#describedBy;
|
|
31
|
+
|
|
32
|
+
/** @type {string | null | undefined} */
|
|
33
|
+
#errorId;
|
|
34
|
+
|
|
35
|
+
/** @type {string | null | undefined} */
|
|
36
|
+
#helperId;
|
|
37
|
+
|
|
38
|
+
/** @type {string[]} */
|
|
39
|
+
#ariaLabelledByAttributeIds = [];
|
|
40
|
+
|
|
41
|
+
/** @type {string[]} */
|
|
42
|
+
#ariaDescribedByAttributeIds = [];
|
|
43
|
+
|
|
13
44
|
constructor(host) {
|
|
14
45
|
this.host = host;
|
|
15
|
-
this.__required = false;
|
|
16
46
|
}
|
|
17
47
|
|
|
18
48
|
/**
|
|
@@ -21,16 +51,11 @@ export class FieldAriaController {
|
|
|
21
51
|
* @param {HTMLElement} target
|
|
22
52
|
*/
|
|
23
53
|
setTarget(target) {
|
|
24
|
-
this
|
|
25
|
-
this
|
|
26
|
-
|
|
27
|
-
this
|
|
28
|
-
|
|
29
|
-
this.__setLabelIdToAriaAttribute(this.__labelIdFromUser, this.__labelIdFromUser, true);
|
|
30
|
-
}
|
|
31
|
-
this.__setErrorIdToAriaAttribute(this.__errorId);
|
|
32
|
-
this.__setHelperIdToAriaAttribute(this.__helperId);
|
|
33
|
-
this.setAriaLabel(this.__label);
|
|
54
|
+
this.#target = target;
|
|
55
|
+
this.#updateAriaLabelAttribute();
|
|
56
|
+
this.#updateAriaLabelledByAttribute();
|
|
57
|
+
this.#updateAriaDescribedByAttribute();
|
|
58
|
+
this.#updateAriaRequiredAttribute();
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
/**
|
|
@@ -41,8 +66,8 @@ export class FieldAriaController {
|
|
|
41
66
|
* @param {boolean} required
|
|
42
67
|
*/
|
|
43
68
|
setRequired(required) {
|
|
44
|
-
this
|
|
45
|
-
this
|
|
69
|
+
this.#required = required;
|
|
70
|
+
this.#updateAriaRequiredAttribute();
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
/**
|
|
@@ -52,120 +77,97 @@ export class FieldAriaController {
|
|
|
52
77
|
*
|
|
53
78
|
* @param {string | null | undefined} label
|
|
54
79
|
*/
|
|
55
|
-
|
|
56
|
-
this
|
|
57
|
-
this
|
|
80
|
+
setLabel(label) {
|
|
81
|
+
this.#label = label;
|
|
82
|
+
this.#updateAriaLabelAttribute();
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
/**
|
|
61
|
-
* Links the target element
|
|
62
|
-
* via the target's attribute `aria-labelledby`.
|
|
63
|
-
*
|
|
64
|
-
* To unlink the previous slotted label element, pass `null` as `labelId`.
|
|
86
|
+
* Links the target element to one or more elements via the `aria-labelledby` attribute.
|
|
65
87
|
*
|
|
66
|
-
* @param {string | null}
|
|
88
|
+
* @param {string | null} labelledBy the space-delimited list of IDs,
|
|
89
|
+
* or `null` to remove the previously linked IDs
|
|
67
90
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this
|
|
71
|
-
if (fromUser) {
|
|
72
|
-
this.__labelIdFromUser = labelId;
|
|
73
|
-
} else {
|
|
74
|
-
this.__labelId = labelId;
|
|
75
|
-
}
|
|
91
|
+
setLabelledBy(labelledBy) {
|
|
92
|
+
this.#labelledBy = labelledBy;
|
|
93
|
+
this.#updateAriaLabelledByAttribute();
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
/**
|
|
79
|
-
* Links the target element
|
|
80
|
-
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
81
|
-
* - `aria-describedby` otherwise.
|
|
97
|
+
* Links the target element to one or more elements via the `aria-describedby` attribute.
|
|
82
98
|
*
|
|
83
|
-
*
|
|
99
|
+
* @param {string | null} describedBy the space-delimited list of IDs,
|
|
100
|
+
* or `null` to remove the previously linked IDs
|
|
101
|
+
*/
|
|
102
|
+
setDescribedBy(describedBy) {
|
|
103
|
+
this.#describedBy = describedBy;
|
|
104
|
+
this.#updateAriaDescribedByAttribute();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Links the target element to a slotted error element via the `aria-describedby` attribute.
|
|
84
109
|
*
|
|
85
|
-
* @param {string | null} errorId
|
|
110
|
+
* @param {string | null} errorId the ID of the error element,
|
|
111
|
+
* or `null` to remove the previously linked ID
|
|
86
112
|
*/
|
|
87
113
|
setErrorId(errorId) {
|
|
88
|
-
this
|
|
89
|
-
this
|
|
114
|
+
this.#errorId = errorId;
|
|
115
|
+
this.#updateAriaDescribedByAttribute();
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
/**
|
|
93
|
-
* Links the target element
|
|
94
|
-
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
95
|
-
* - `aria-describedby` otherwise.
|
|
119
|
+
* Links the target element to a slotted helper element via the `aria-describedby` attribute.
|
|
96
120
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* @param {string | null} helperId
|
|
121
|
+
* @param {string | null} helperId the ID of the helper element,
|
|
122
|
+
* or `null` to remove the previously linked ID
|
|
100
123
|
*/
|
|
101
124
|
setHelperId(helperId) {
|
|
102
|
-
this
|
|
103
|
-
this
|
|
125
|
+
this.#helperId = helperId;
|
|
126
|
+
this.#updateAriaDescribedByAttribute();
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* @private
|
|
109
|
-
* */
|
|
110
|
-
__setAriaLabelToAttribute(label) {
|
|
111
|
-
if (!this.__target) {
|
|
129
|
+
#updateAriaLabelAttribute() {
|
|
130
|
+
if (!this.#target) {
|
|
112
131
|
return;
|
|
113
132
|
}
|
|
114
|
-
if (label) {
|
|
115
|
-
removeAriaIDReference(this.__target, 'aria-labelledby');
|
|
116
|
-
this.__target.setAttribute('aria-label', label);
|
|
117
|
-
} else if (this.__label) {
|
|
118
|
-
restoreGeneratedAriaIDReference(this.__target, 'aria-labelledby');
|
|
119
|
-
this.__target.removeAttribute('aria-label');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
* @param {string | null | undefined} labelId
|
|
125
|
-
* @param {string | null | undefined} oldLabelId
|
|
126
|
-
* @param {boolean | null | undefined} fromUser
|
|
127
|
-
* @private
|
|
128
|
-
*/
|
|
129
|
-
__setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser) {
|
|
130
|
-
setAriaIDReference(this.__target, 'aria-labelledby', { newId: labelId, oldId: oldLabelId, fromUser });
|
|
134
|
+
setOrRemoveAttribute(this.#target, 'aria-label', this.#label);
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
#updateAriaLabelledByAttribute() {
|
|
138
|
+
if (!this.#target) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
removeValuesFromAttribute(this.#target, 'aria-labelledby', this.#ariaLabelledByAttributeIds);
|
|
143
|
+
|
|
144
|
+
this.#ariaLabelledByAttributeIds = [this.#labelledBy];
|
|
145
|
+
|
|
146
|
+
addValuesToAttribute(this.#target, 'aria-labelledby', this.#ariaLabelledByAttributeIds);
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
#updateAriaDescribedByAttribute() {
|
|
150
|
+
if (!this.#target) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
removeValuesFromAttribute(this.#target, 'aria-describedby', this.#ariaDescribedByAttributeIds);
|
|
155
|
+
|
|
156
|
+
this.#ariaDescribedByAttributeIds = [this.#describedBy, this.#helperId, this.#errorId];
|
|
157
|
+
|
|
158
|
+
addValuesToAttribute(this.#target, 'aria-describedby', this.#ariaDescribedByAttributeIds);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* @private
|
|
154
|
-
*/
|
|
155
|
-
__setAriaRequiredAttribute(required) {
|
|
156
|
-
if (!this.__target) {
|
|
161
|
+
#updateAriaRequiredAttribute() {
|
|
162
|
+
if (!this.#target) {
|
|
157
163
|
return;
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
if (['input', 'textarea'].includes(this.
|
|
166
|
+
if (['input', 'textarea'].includes(this.#target.localName)) {
|
|
161
167
|
// Native <input> or <textarea>, required is enough
|
|
162
168
|
return;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
|
|
166
|
-
this.__target.setAttribute('aria-required', 'true');
|
|
167
|
-
} else {
|
|
168
|
-
this.__target.removeAttribute('aria-required');
|
|
169
|
-
}
|
|
171
|
+
setOrRemoveAttribute(this.#target, 'aria-required', this.#required);
|
|
170
172
|
}
|
|
171
173
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Copyright (c) 2021 - 2026 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { getTabbableElements, isElementFocused, isKeyboardActive } from './focus-utils.js';
|
|
7
7
|
|
|
8
8
|
const instances = [];
|
|
9
9
|
|
|
@@ -17,8 +17,8 @@ const instances = [];
|
|
|
17
17
|
export function getActiveTrappingNode(element) {
|
|
18
18
|
// Iterate backwards since instances are ordered outer-to-inner (push/pop)
|
|
19
19
|
for (let i = instances.length - 1; i >= 0; i--) {
|
|
20
|
-
if (instances[i].
|
|
21
|
-
return instances[i].
|
|
20
|
+
if (instances[i].trapNode?.contains(element)) {
|
|
21
|
+
return instances[i].trapNode;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
return null;
|
|
@@ -28,6 +28,13 @@ export function getActiveTrappingNode(element) {
|
|
|
28
28
|
* A controller for trapping focus within a DOM node.
|
|
29
29
|
*/
|
|
30
30
|
export class FocusTrapController {
|
|
31
|
+
/**
|
|
32
|
+
* A node for trapping focus in.
|
|
33
|
+
*
|
|
34
|
+
* @type {HTMLElement | null}
|
|
35
|
+
*/
|
|
36
|
+
trapNode = null;
|
|
37
|
+
|
|
31
38
|
/**
|
|
32
39
|
* @param {HTMLElement} host
|
|
33
40
|
*/
|
|
@@ -38,45 +45,33 @@ export class FocusTrapController {
|
|
|
38
45
|
* @type {HTMLElement}
|
|
39
46
|
*/
|
|
40
47
|
this.host = host;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A node for trapping focus in.
|
|
44
|
-
*
|
|
45
|
-
* @type {HTMLElement | null}
|
|
46
|
-
* @private
|
|
47
|
-
*/
|
|
48
|
-
this.__trapNode = null;
|
|
49
|
-
|
|
50
|
-
this.__onKeyDown = this.__onKeyDown.bind(this);
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
54
51
|
* An array of tab-ordered focusable elements inside the trap node.
|
|
55
52
|
*
|
|
56
53
|
* @return {HTMLElement[]}
|
|
57
|
-
* @private
|
|
58
54
|
*/
|
|
59
|
-
get
|
|
60
|
-
return
|
|
55
|
+
get #focusableElements() {
|
|
56
|
+
return getTabbableElements(this.trapNode);
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
/**
|
|
64
60
|
* The index of the element inside the trap node that currently has focus.
|
|
65
61
|
*
|
|
66
62
|
* @return {HTMLElement | undefined}
|
|
67
|
-
* @private
|
|
68
63
|
*/
|
|
69
|
-
get
|
|
70
|
-
const focusableElements = this
|
|
64
|
+
get #focusedElementIndex() {
|
|
65
|
+
const focusableElements = this.#focusableElements;
|
|
71
66
|
return focusableElements.indexOf(focusableElements.filter(isElementFocused).pop());
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
hostConnected() {
|
|
75
|
-
document.addEventListener('keydown', this
|
|
70
|
+
document.addEventListener('keydown', this.#onKeyDown);
|
|
76
71
|
}
|
|
77
72
|
|
|
78
73
|
hostDisconnected() {
|
|
79
|
-
document.removeEventListener('keydown', this
|
|
74
|
+
document.removeEventListener('keydown', this.#onKeyDown);
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
/**
|
|
@@ -94,17 +89,17 @@ export class FocusTrapController {
|
|
|
94
89
|
* @param {HTMLElement} trapNode
|
|
95
90
|
*/
|
|
96
91
|
trapFocus(trapNode) {
|
|
97
|
-
this.
|
|
92
|
+
this.trapNode = trapNode;
|
|
98
93
|
|
|
99
|
-
if (this.
|
|
100
|
-
this.
|
|
94
|
+
if (this.#focusableElements.length === 0) {
|
|
95
|
+
this.trapNode = null;
|
|
101
96
|
throw new Error('The trap node should have at least one focusable descendant or be focusable itself.');
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
instances.push(this);
|
|
105
100
|
|
|
106
|
-
if (this
|
|
107
|
-
this
|
|
101
|
+
if (this.#focusedElementIndex === -1) {
|
|
102
|
+
this.#focusableElements[0].focus({ focusVisible: isKeyboardActive() });
|
|
108
103
|
}
|
|
109
104
|
}
|
|
110
105
|
|
|
@@ -113,7 +108,7 @@ export class FocusTrapController {
|
|
|
113
108
|
* so that it becomes possible to tab outside the trap node.
|
|
114
109
|
*/
|
|
115
110
|
releaseFocus() {
|
|
116
|
-
this.
|
|
111
|
+
this.trapNode = null;
|
|
117
112
|
|
|
118
113
|
instances.pop();
|
|
119
114
|
}
|
|
@@ -127,10 +122,9 @@ export class FocusTrapController {
|
|
|
127
122
|
* When no prev element to focus, the method moves focus to the last focusable element.
|
|
128
123
|
*
|
|
129
124
|
* @param {KeyboardEvent} event
|
|
130
|
-
* @private
|
|
131
125
|
*/
|
|
132
|
-
|
|
133
|
-
if (!this.
|
|
126
|
+
#onKeyDown = (event) => {
|
|
127
|
+
if (!this.trapNode) {
|
|
134
128
|
return;
|
|
135
129
|
}
|
|
136
130
|
|
|
@@ -148,9 +142,9 @@ export class FocusTrapController {
|
|
|
148
142
|
event.preventDefault();
|
|
149
143
|
|
|
150
144
|
const backward = event.shiftKey;
|
|
151
|
-
this
|
|
145
|
+
this.#focusNextElement(backward);
|
|
152
146
|
}
|
|
153
|
-
}
|
|
147
|
+
};
|
|
154
148
|
|
|
155
149
|
/**
|
|
156
150
|
* - Moves focus to the next focusable element if `backward === false`.
|
|
@@ -161,12 +155,11 @@ export class FocusTrapController {
|
|
|
161
155
|
* If no focusable elements, the method returns immediately.
|
|
162
156
|
*
|
|
163
157
|
* @param {boolean} backward
|
|
164
|
-
* @private
|
|
165
158
|
*/
|
|
166
|
-
|
|
167
|
-
const focusableElements = this
|
|
159
|
+
#focusNextElement(backward = false) {
|
|
160
|
+
const focusableElements = this.#focusableElements;
|
|
168
161
|
const step = backward ? -1 : 1;
|
|
169
|
-
const currentIndex = this
|
|
162
|
+
const currentIndex = this.#focusedElementIndex;
|
|
170
163
|
const nextIndex = (focusableElements.length + currentIndex + step) % focusableElements.length;
|
|
171
164
|
const element = focusableElements[nextIndex];
|
|
172
165
|
element.focus({ focusVisible: true });
|
package/src/focus-utils.d.ts
CHANGED
|
@@ -26,7 +26,8 @@ export declare function isKeyboardActive(): boolean;
|
|
|
26
26
|
export declare function isElementHidden(element: HTMLElement): boolean;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Returns true if the element is focusable,
|
|
29
|
+
* Returns true if the element is focusable, i.e. can be focused with a
|
|
30
|
+
* mouse click or a `focus()` call, otherwise false.
|
|
30
31
|
*
|
|
31
32
|
* The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
|
|
32
33
|
* However, there isn't a definite list, it's up to the browser.
|
|
@@ -54,4 +55,4 @@ export declare function isElementFocused(element: HTMLElement): boolean;
|
|
|
54
55
|
*
|
|
55
56
|
* The method traverses nodes in shadow DOM trees too if any.
|
|
56
57
|
*/
|
|
57
|
-
export declare function
|
|
58
|
+
export declare function getTabbableElements(element: HTMLElement): HTMLElement[];
|
package/src/focus-utils.js
CHANGED
|
@@ -165,7 +165,8 @@ export function isElementHidden(element) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* Returns true if the element is focusable,
|
|
168
|
+
* Returns true if the element is focusable, i.e. can be focused with a
|
|
169
|
+
* mouse click or a `focus()` call, otherwise false.
|
|
169
170
|
*
|
|
170
171
|
* The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
|
|
171
172
|
* However, there isn't a definite list, it's up to the browser.
|
|
@@ -184,11 +185,6 @@ export function isElementHidden(element) {
|
|
|
184
185
|
* @return {boolean}
|
|
185
186
|
*/
|
|
186
187
|
export function isElementFocusable(element) {
|
|
187
|
-
// The element cannot be focused if its `tabindex` attribute is set to `-1`.
|
|
188
|
-
if (element.matches('[tabindex="-1"]')) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
188
|
// Elements that cannot be focused if they have a `disabled` attribute.
|
|
193
189
|
if (element.matches('input, select, textarea, button, object')) {
|
|
194
190
|
return element.matches(':not([disabled])');
|
|
@@ -271,7 +267,7 @@ function collectFocusableNodes(node, result) {
|
|
|
271
267
|
* @param {HTMLElement} element
|
|
272
268
|
* @return {HTMLElement[]}
|
|
273
269
|
*/
|
|
274
|
-
export function
|
|
270
|
+
export function getTabbableElements(element) {
|
|
275
271
|
const focusableElements = [];
|
|
276
272
|
const needsSortByTabIndex = collectFocusableNodes(element, focusableElements);
|
|
277
273
|
// If there is at least one element with tabindex > 0,
|
package/src/list-mixin.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export declare class ListMixinClass {
|
|
|
35
35
|
*/
|
|
36
36
|
orientation: 'horizontal' | 'vertical';
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Used for mixin detection because `instanceof` does not work with mixins.
|
|
40
|
+
*/
|
|
41
|
+
protected _hasVaadinListMixin: boolean;
|
|
42
|
+
|
|
38
43
|
/**
|
|
39
44
|
* A read-only list of items from which a selection can be made.
|
|
40
45
|
* It is populated from the elements passed to the light DOM,
|
package/src/list-mixin.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { timeOut } from '@vaadin/component-base/src/async.js';
|
|
7
7
|
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
|
8
8
|
import { getNormalizedScrollLeft, setNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
|
|
9
|
+
import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
9
10
|
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
|
|
10
11
|
import { isElementHidden } from './focus-utils.js';
|
|
11
12
|
import { KeyboardDirectionMixin } from './keyboard-direction-mixin.js';
|
|
@@ -77,6 +78,16 @@ export const ListMixin = (superClass) =>
|
|
|
77
78
|
return ['_enhanceItems(items, orientation, selected, disabled)'];
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Used for mixin detection because `instanceof` does not work with mixins.
|
|
86
|
+
* @protected
|
|
87
|
+
*/
|
|
88
|
+
this._hasVaadinListMixin = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
/**
|
|
81
92
|
* @return {boolean}
|
|
82
93
|
* @protected
|
|
@@ -153,13 +164,7 @@ export const ListMixin = (superClass) =>
|
|
|
153
164
|
if (!disabled) {
|
|
154
165
|
if (items) {
|
|
155
166
|
this.setAttribute('aria-orientation', orientation || 'vertical');
|
|
156
|
-
items.forEach((item) =>
|
|
157
|
-
if (orientation) {
|
|
158
|
-
item.setAttribute('orientation', orientation);
|
|
159
|
-
} else {
|
|
160
|
-
item.removeAttribute('orientation');
|
|
161
|
-
}
|
|
162
|
-
});
|
|
167
|
+
items.forEach((item) => setOrRemoveAttribute(item, 'orientation', orientation));
|
|
163
168
|
|
|
164
169
|
// When selected is set to -1, focus the first available item.
|
|
165
170
|
this._setFocusable(selected < 0 || !selected ? 0 : selected);
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2023 - 2026 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export type AriaIDReferenceConfig = {
|
|
8
|
-
newId: string | null;
|
|
9
|
-
oldId: string | null;
|
|
10
|
-
fromUser: boolean | null;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Sets a new ID reference for a target element and an ARIA attribute.
|
|
15
|
-
*
|
|
16
|
-
* @param config.newId
|
|
17
|
-
* The new ARIA ID reference to set. If `null`, the attribute is removed,
|
|
18
|
-
* and `config.fromUser` is `true`, any stored values are restored. If there
|
|
19
|
-
* are stored values and `config.fromUser` is `false`, then `config.newId`
|
|
20
|
-
* is added to the stored values set.
|
|
21
|
-
* @param config.oldId
|
|
22
|
-
* The ARIA ID reference to be removed from the attribute. If there are stored
|
|
23
|
-
* values and `config.fromUser` is `false`, then `config.oldId` is removed from
|
|
24
|
-
* the stored values set.
|
|
25
|
-
* @param config.fromUser
|
|
26
|
-
* Indicates whether the function is called by the user or internally.
|
|
27
|
-
* When `config.fromUser` is called with `true` for the first time,
|
|
28
|
-
* the function will clear and store the attribute value for the given element.
|
|
29
|
-
*/
|
|
30
|
-
export function setAriaIDReference(target: HTMLElement, attr: string, config: AriaIDReferenceConfig): void;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Removes the attribute value of the given target element.
|
|
34
|
-
* It also stores the current value, if no stored values are present.
|
|
35
|
-
*/
|
|
36
|
-
export function removeAriaIDReference(target: HTMLElement, attr: string): void;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Restores the generated values of the attribute to the given target element.
|
|
40
|
-
*/
|
|
41
|
-
export function restoreGeneratedAriaIDReference(target: HTMLElement, attr: string): void;
|
package/src/aria-id-reference.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2023 - 2026 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
addValueToAttribute,
|
|
8
|
-
deserializeAttributeValue,
|
|
9
|
-
removeValueFromAttribute,
|
|
10
|
-
serializeAttributeValue,
|
|
11
|
-
} from '@vaadin/component-base/src/dom-utils.js';
|
|
12
|
-
|
|
13
|
-
const attributeToTargets = new Map();
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Gets or creates a Set with the stored values for each element controlled by this helper
|
|
17
|
-
*
|
|
18
|
-
* @param {string} attr the attribute name used as key in the map
|
|
19
|
-
*
|
|
20
|
-
* @return {WeakMap<HTMLElement, Set<string>>} a weak map with the stored values for the elements being controlled by the helper
|
|
21
|
-
*/
|
|
22
|
-
function getAttrMap(attr) {
|
|
23
|
-
if (!attributeToTargets.has(attr)) {
|
|
24
|
-
attributeToTargets.set(attr, new WeakMap());
|
|
25
|
-
}
|
|
26
|
-
return attributeToTargets.get(attr);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Cleans the values set on the attribute to the given element.
|
|
31
|
-
* It also stores the current values in the map, if `storeValue` is `true`.
|
|
32
|
-
*
|
|
33
|
-
* @param {HTMLElement} target
|
|
34
|
-
* @param {string} attr the attribute to be cleared
|
|
35
|
-
*/
|
|
36
|
-
function cleanAriaIDReference(target, attr) {
|
|
37
|
-
if (!target) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
target.removeAttribute(attr);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Storing values of the accessible attributes in a Set inside of the WeakMap.
|
|
46
|
-
*
|
|
47
|
-
* @param {HTMLElement} target
|
|
48
|
-
* @param {string} attr the attribute to be stored
|
|
49
|
-
*/
|
|
50
|
-
function storeAriaIDReference(target, attr) {
|
|
51
|
-
if (!target || !attr) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const attributeMap = getAttrMap(attr);
|
|
55
|
-
if (attributeMap.has(target)) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const values = deserializeAttributeValue(target.getAttribute(attr));
|
|
59
|
-
attributeMap.set(target, new Set(values));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Restores the generated values of the attribute to the given element.
|
|
64
|
-
*
|
|
65
|
-
* @param {HTMLElement} target
|
|
66
|
-
* @param {string} attr
|
|
67
|
-
*/
|
|
68
|
-
export function restoreGeneratedAriaIDReference(target, attr) {
|
|
69
|
-
if (!target || !attr) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const attributeMap = getAttrMap(attr);
|
|
73
|
-
const values = attributeMap.get(target);
|
|
74
|
-
if (!values || values.size === 0) {
|
|
75
|
-
target.removeAttribute(attr);
|
|
76
|
-
} else {
|
|
77
|
-
addValueToAttribute(target, attr, serializeAttributeValue(values));
|
|
78
|
-
}
|
|
79
|
-
attributeMap.delete(target);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Sets a new ID reference for a target element and an ARIA attribute.
|
|
84
|
-
*
|
|
85
|
-
* @typedef {Object} AriaIdReferenceConfig
|
|
86
|
-
* @property {string | null | undefined} newId
|
|
87
|
-
* @property {string | null | undefined} oldId
|
|
88
|
-
* @property {boolean | null | undefined} fromUser
|
|
89
|
-
* @param {HTMLElement} target
|
|
90
|
-
* @param {string} attr
|
|
91
|
-
* @param {AriaIdReferenceConfig | null | undefined} config
|
|
92
|
-
* @param config.newId The new ARIA ID reference to set. If `null`, the attribute is removed,
|
|
93
|
-
* and `config.fromUser` is true, any stored values are restored. If there are stored values
|
|
94
|
-
* and `config.fromUser` is `false`, then `config.newId` is added to the stored values set.
|
|
95
|
-
* @param config.oldId The ARIA ID reference to be removed from the attribute. If there are
|
|
96
|
-
* stored values and `config.fromUser` is `false`, then `config.oldId` is removed from the
|
|
97
|
-
* stored values set.
|
|
98
|
-
* @param config.fromUser Indicates whether the function is called by the user or internally.
|
|
99
|
-
* When `config.fromUser` is called with `true` for the first time, the function will clear
|
|
100
|
-
* and store the attribute value for the given element.
|
|
101
|
-
*/
|
|
102
|
-
export function setAriaIDReference(target, attr, config = { newId: null, oldId: null, fromUser: false }) {
|
|
103
|
-
if (!target || !attr) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const { newId, oldId, fromUser } = config;
|
|
108
|
-
|
|
109
|
-
const attributeMap = getAttrMap(attr);
|
|
110
|
-
const storedValues = attributeMap.get(target);
|
|
111
|
-
|
|
112
|
-
if (!fromUser && !!storedValues) {
|
|
113
|
-
// If there's any stored values, it means the attribute is being handled by the user
|
|
114
|
-
// Replace the "oldId" with "newId" on the stored values set and leave
|
|
115
|
-
oldId && storedValues.delete(oldId);
|
|
116
|
-
newId && storedValues.add(newId);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (fromUser) {
|
|
121
|
-
if (!storedValues) {
|
|
122
|
-
// If it's called from user and there's no stored values for the attribute,
|
|
123
|
-
// then store the current value
|
|
124
|
-
storeAriaIDReference(target, attr);
|
|
125
|
-
} else if (!newId) {
|
|
126
|
-
// If called from user with newId == null, it means the attribute will no longer
|
|
127
|
-
// be in control of the user and the stored values should be restored
|
|
128
|
-
// Removing the entry on the map for this target
|
|
129
|
-
attributeMap.delete(target);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// If it's from user, then clear the attribute value before setting newId
|
|
133
|
-
cleanAriaIDReference(target, attr);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
removeValueFromAttribute(target, attr, oldId);
|
|
137
|
-
|
|
138
|
-
const attributeValue = !newId ? serializeAttributeValue(storedValues) : newId;
|
|
139
|
-
if (attributeValue) {
|
|
140
|
-
addValueToAttribute(target, attr, attributeValue);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Removes the {@link attr | attribute} value of the given {@link target} element.
|
|
146
|
-
* It also stores the current value, if no stored values are present.
|
|
147
|
-
*
|
|
148
|
-
* @param {HTMLElement} target
|
|
149
|
-
* @param {string} attr
|
|
150
|
-
*/
|
|
151
|
-
export function removeAriaIDReference(target, attr) {
|
|
152
|
-
storeAriaIDReference(target, attr);
|
|
153
|
-
cleanAriaIDReference(target, attr);
|
|
154
|
-
}
|