@vaadin/a11y-base 25.3.0-alpha6 → 25.3.0-alpha8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/a11y-base",
|
|
3
|
-
"version": "25.3.0-
|
|
3
|
+
"version": "25.3.0-alpha8",
|
|
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": "25.3.0-
|
|
35
|
+
"@vaadin/component-base": "25.3.0-alpha8",
|
|
36
36
|
"lit": "^3.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@vaadin/chai-plugins": "25.3.0-
|
|
40
|
-
"@vaadin/test-runner-commands": "25.3.0-
|
|
39
|
+
"@vaadin/chai-plugins": "25.3.0-alpha8",
|
|
40
|
+
"@vaadin/test-runner-commands": "25.3.0-alpha8",
|
|
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": "ccbb4aaffb63c745c6da0426b532d4d05e47af29"
|
|
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
|
}
|
|
@@ -10,9 +10,29 @@ import { removeAriaIDReference, restoreGeneratedAriaIDReference, setAriaIDRefere
|
|
|
10
10
|
* either the component itself or slotted `<input>` element.
|
|
11
11
|
*/
|
|
12
12
|
export class FieldAriaController {
|
|
13
|
+
/** @type {HTMLElement | undefined} */
|
|
14
|
+
#target;
|
|
15
|
+
|
|
16
|
+
/** @type {boolean} */
|
|
17
|
+
#required = false;
|
|
18
|
+
|
|
19
|
+
/** @type {string | null | undefined} */
|
|
20
|
+
#label;
|
|
21
|
+
|
|
22
|
+
/** @type {string | null | undefined} */
|
|
23
|
+
#labelId;
|
|
24
|
+
|
|
25
|
+
/** @type {string | null | undefined} */
|
|
26
|
+
#labelIdFromUser;
|
|
27
|
+
|
|
28
|
+
/** @type {string | null | undefined} */
|
|
29
|
+
#errorId;
|
|
30
|
+
|
|
31
|
+
/** @type {string | null | undefined} */
|
|
32
|
+
#helperId;
|
|
33
|
+
|
|
13
34
|
constructor(host) {
|
|
14
35
|
this.host = host;
|
|
15
|
-
this.__required = false;
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
/**
|
|
@@ -21,16 +41,16 @@ export class FieldAriaController {
|
|
|
21
41
|
* @param {HTMLElement} target
|
|
22
42
|
*/
|
|
23
43
|
setTarget(target) {
|
|
24
|
-
this
|
|
25
|
-
this
|
|
26
|
-
// We need to make sure that value in
|
|
27
|
-
this
|
|
28
|
-
if (this
|
|
29
|
-
this
|
|
44
|
+
this.#target = target;
|
|
45
|
+
this.#setAriaRequiredAttribute(this.#required);
|
|
46
|
+
// We need to make sure that value in #labelId is stored
|
|
47
|
+
this.#setLabelIdToAriaAttribute(this.#labelId, this.#labelId);
|
|
48
|
+
if (this.#labelIdFromUser != null) {
|
|
49
|
+
this.#setLabelIdToAriaAttribute(this.#labelIdFromUser, this.#labelIdFromUser, true);
|
|
30
50
|
}
|
|
31
|
-
this
|
|
32
|
-
this
|
|
33
|
-
this.setAriaLabel(this
|
|
51
|
+
this.#setErrorIdToAriaAttribute(this.#errorId);
|
|
52
|
+
this.#setHelperIdToAriaAttribute(this.#helperId);
|
|
53
|
+
this.setAriaLabel(this.#label);
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
/**
|
|
@@ -41,8 +61,8 @@ export class FieldAriaController {
|
|
|
41
61
|
* @param {boolean} required
|
|
42
62
|
*/
|
|
43
63
|
setRequired(required) {
|
|
44
|
-
this
|
|
45
|
-
this
|
|
64
|
+
this.#setAriaRequiredAttribute(required);
|
|
65
|
+
this.#required = required;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
/**
|
|
@@ -53,8 +73,8 @@ export class FieldAriaController {
|
|
|
53
73
|
* @param {string | null | undefined} label
|
|
54
74
|
*/
|
|
55
75
|
setAriaLabel(label) {
|
|
56
|
-
this
|
|
57
|
-
this
|
|
76
|
+
this.#setAriaLabelToAttribute(label);
|
|
77
|
+
this.#label = label;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
/**
|
|
@@ -66,12 +86,12 @@ export class FieldAriaController {
|
|
|
66
86
|
* @param {string | null} labelId
|
|
67
87
|
*/
|
|
68
88
|
setLabelId(labelId, fromUser = false) {
|
|
69
|
-
const oldLabelId = fromUser ? this
|
|
70
|
-
this
|
|
89
|
+
const oldLabelId = fromUser ? this.#labelIdFromUser : this.#labelId;
|
|
90
|
+
this.#setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser);
|
|
71
91
|
if (fromUser) {
|
|
72
|
-
this
|
|
92
|
+
this.#labelIdFromUser = labelId;
|
|
73
93
|
} else {
|
|
74
|
-
this
|
|
94
|
+
this.#labelId = labelId;
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
|
|
@@ -85,8 +105,8 @@ export class FieldAriaController {
|
|
|
85
105
|
* @param {string | null} errorId
|
|
86
106
|
*/
|
|
87
107
|
setErrorId(errorId) {
|
|
88
|
-
this
|
|
89
|
-
this
|
|
108
|
+
this.#setErrorIdToAriaAttribute(errorId, this.#errorId);
|
|
109
|
+
this.#errorId = errorId;
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
/**
|
|
@@ -99,24 +119,23 @@ export class FieldAriaController {
|
|
|
99
119
|
* @param {string | null} helperId
|
|
100
120
|
*/
|
|
101
121
|
setHelperId(helperId) {
|
|
102
|
-
this
|
|
103
|
-
this
|
|
122
|
+
this.#setHelperIdToAriaAttribute(helperId, this.#helperId);
|
|
123
|
+
this.#helperId = helperId;
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
/**
|
|
107
127
|
* @param {string | null | undefined} label
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!this.__target) {
|
|
128
|
+
*/
|
|
129
|
+
#setAriaLabelToAttribute(label) {
|
|
130
|
+
if (!this.#target) {
|
|
112
131
|
return;
|
|
113
132
|
}
|
|
114
133
|
if (label) {
|
|
115
|
-
removeAriaIDReference(this
|
|
116
|
-
this.
|
|
117
|
-
} else if (this
|
|
118
|
-
restoreGeneratedAriaIDReference(this
|
|
119
|
-
this.
|
|
134
|
+
removeAriaIDReference(this.#target, 'aria-labelledby');
|
|
135
|
+
this.#target.setAttribute('aria-label', label);
|
|
136
|
+
} else if (this.#label) {
|
|
137
|
+
restoreGeneratedAriaIDReference(this.#target, 'aria-labelledby');
|
|
138
|
+
this.#target.removeAttribute('aria-label');
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
|
|
@@ -124,48 +143,44 @@ export class FieldAriaController {
|
|
|
124
143
|
* @param {string | null | undefined} labelId
|
|
125
144
|
* @param {string | null | undefined} oldLabelId
|
|
126
145
|
* @param {boolean | null | undefined} fromUser
|
|
127
|
-
* @private
|
|
128
146
|
*/
|
|
129
|
-
|
|
130
|
-
setAriaIDReference(this
|
|
147
|
+
#setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser) {
|
|
148
|
+
setAriaIDReference(this.#target, 'aria-labelledby', { newId: labelId, oldId: oldLabelId, fromUser });
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
/**
|
|
134
152
|
* @param {string | null | undefined} errorId
|
|
135
153
|
* @param {string | null | undefined} oldErrorId
|
|
136
|
-
* @private
|
|
137
154
|
*/
|
|
138
|
-
|
|
139
|
-
setAriaIDReference(this
|
|
155
|
+
#setErrorIdToAriaAttribute(errorId, oldErrorId) {
|
|
156
|
+
setAriaIDReference(this.#target, 'aria-describedby', { newId: errorId, oldId: oldErrorId, fromUser: false });
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
/**
|
|
143
160
|
* @param {string | null | undefined} helperId
|
|
144
161
|
* @param {string | null | undefined} oldHelperId
|
|
145
|
-
* @private
|
|
146
162
|
*/
|
|
147
|
-
|
|
148
|
-
setAriaIDReference(this
|
|
163
|
+
#setHelperIdToAriaAttribute(helperId, oldHelperId) {
|
|
164
|
+
setAriaIDReference(this.#target, 'aria-describedby', { newId: helperId, oldId: oldHelperId, fromUser: false });
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
/**
|
|
152
168
|
* @param {boolean} required
|
|
153
|
-
* @private
|
|
154
169
|
*/
|
|
155
|
-
|
|
156
|
-
if (!this
|
|
170
|
+
#setAriaRequiredAttribute(required) {
|
|
171
|
+
if (!this.#target) {
|
|
157
172
|
return;
|
|
158
173
|
}
|
|
159
174
|
|
|
160
|
-
if (['input', 'textarea'].includes(this.
|
|
175
|
+
if (['input', 'textarea'].includes(this.#target.localName)) {
|
|
161
176
|
// Native <input> or <textarea>, required is enough
|
|
162
177
|
return;
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
if (required) {
|
|
166
|
-
this.
|
|
181
|
+
this.#target.setAttribute('aria-required', 'true');
|
|
167
182
|
} else {
|
|
168
|
-
this.
|
|
183
|
+
this.#target.removeAttribute('aria-required');
|
|
169
184
|
}
|
|
170
185
|
}
|
|
171
186
|
}
|
|
@@ -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 getTabbableElements(this.
|
|
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 });
|