@vaadin/a11y-base 24.1.0-alpha3 → 24.1.0-alpha5

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": "24.1.0-alpha3",
3
+ "version": "24.1.0-alpha5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@open-wc/dedupe-mixin": "^1.3.0",
34
34
  "@polymer/polymer": "^3.0.0",
35
- "@vaadin/component-base": "24.1.0-alpha3",
35
+ "@vaadin/component-base": "24.1.0-alpha5",
36
36
  "lit": "^2.0.0"
37
37
  },
38
38
  "devDependencies": {
@@ -40,5 +40,5 @@
40
40
  "@vaadin/testing-helpers": "^0.4.0",
41
41
  "sinon": "^13.0.2"
42
42
  },
43
- "gitHead": "077b4a8e8fff063b9eba4581af81f9e152cb852d"
43
+ "gitHead": "1ab6c977fe239d94aac5f39940c1a4722ad4bb63"
44
44
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2023 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;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2023 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
+ * @returns {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
+ * @param {boolean} storeValue whether or not the current value of the attribute should be stored on the map
36
+ * @returns
37
+ */
38
+ function cleanAriaIDReference(target, attr) {
39
+ if (!target) {
40
+ return;
41
+ }
42
+
43
+ target.removeAttribute(attr);
44
+ }
45
+
46
+ /**
47
+ * Storing values of the accessible attributes in a Set inside of the WeakMap.
48
+ *
49
+ * @param {HTMLElement} target
50
+ * @param {string} attr the attribute to be stored
51
+ */
52
+ function storeAriaIDReference(target, attr) {
53
+ if (!target || !attr) {
54
+ return;
55
+ }
56
+ const attributeMap = getAttrMap(attr);
57
+ if (attributeMap.has(target)) {
58
+ return;
59
+ }
60
+ const values = deserializeAttributeValue(target.getAttribute(attr));
61
+ attributeMap.set(target, new Set(values));
62
+ }
63
+
64
+ /**
65
+ * Restores the generated values of the attribute to the given element.
66
+ *
67
+ * @param {HTMLElement} target
68
+ * @param {string} attr
69
+ */
70
+ export function restoreGeneratedAriaIDReference(target, attr) {
71
+ if (!target || !attr) {
72
+ return;
73
+ }
74
+ const attributeMap = getAttrMap(attr);
75
+ const values = attributeMap.get(target);
76
+ if (!values || values.size === 0) {
77
+ target.removeAttribute(attr);
78
+ } else {
79
+ addValueToAttribute(target, attr, serializeAttributeValue(values));
80
+ }
81
+ attributeMap.delete(target);
82
+ }
83
+
84
+ /**
85
+ * Sets a new ID reference for a target element and an ARIA attribute.
86
+ *
87
+ * @typedef {Object} AriaIdReferenceConfig
88
+ * @property {string | null | undefined} newId
89
+ * @property {string | null | undefined} oldId
90
+ * @property {boolean | null | undefined} fromUser
91
+ * @param {HTMLElement} target
92
+ * @param {string} attr
93
+ * @param {AriaIdReferenceConfig | null | undefined} config
94
+ * @param config.newId The new ARIA ID reference to set. If `null`, the attribute is removed,
95
+ * and `config.fromUser` is true, any stored values are restored. If there are stored values
96
+ * and `config.fromUser` is `false`, then `config.newId` is added to the stored values set.
97
+ * @param config.oldId The ARIA ID reference to be removed from the attribute. If there are
98
+ * stored values and `config.fromUser` is `false`, then `config.oldId` is removed from the
99
+ * stored values set.
100
+ * @param config.fromUser Indicates whether the function is called by the user or internally.
101
+ * When `config.fromUser` is called with `true` for the first time, the function will clear
102
+ * and store the attribute value for the given element.
103
+ */
104
+ export function setAriaIDReference(target, attr, config = { newId: null, oldId: null, fromUser: false }) {
105
+ if (!target || !attr) {
106
+ return;
107
+ }
108
+
109
+ const { newId, oldId, fromUser } = config;
110
+
111
+ const attributeMap = getAttrMap(attr);
112
+ const storedValues = attributeMap.get(target);
113
+
114
+ if (!fromUser && !!storedValues) {
115
+ // If there's any stored values, it means the attribute is being handled by the user
116
+ // Replace the "oldId" with "newId" on the stored values set and leave
117
+ storedValues.delete(oldId);
118
+ storedValues.add(newId);
119
+ return;
120
+ }
121
+
122
+ if (fromUser) {
123
+ if (!storedValues) {
124
+ // If it's called from user and there's no stored values for the attribute,
125
+ // then store the current value
126
+ storeAriaIDReference(target, attr);
127
+ } else if (!newId) {
128
+ // If called from user with newId == null, it means the attribute will no longer
129
+ // be in control of the user and the stored values should be restored
130
+ // Removing the entry on the map for this target
131
+ attributeMap.delete(target);
132
+ }
133
+
134
+ // If it's from user, then clear the attribute value before setting newId
135
+ cleanAriaIDReference(target, attr);
136
+ }
137
+
138
+ removeValueFromAttribute(target, attr, oldId);
139
+
140
+ const attributeValue = !newId ? serializeAttributeValue(storedValues) : newId;
141
+ if (attributeValue) {
142
+ addValueToAttribute(target, attr, attributeValue);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Removes the {@link attr | attribute} value of the given {@link target} element.
148
+ * It also stores the current value, if no stored values are present.
149
+ *
150
+ * @param {HTMLElement} target
151
+ * @param {string} attr
152
+ */
153
+ export function removeAriaIDReference(target, attr) {
154
+ storeAriaIDReference(target, attr);
155
+ cleanAriaIDReference(target, attr);
156
+ }
@@ -28,13 +28,20 @@ export class FieldAriaController {
28
28
  */
29
29
  setRequired(required: boolean): void;
30
30
 
31
+ /**
32
+ * Defines the `aria-label` attribute of the target element.
33
+ *
34
+ * To remove the attribute, pass `null` as `label`.
35
+ */
36
+ setAriaLabel(label: string | null): void;
37
+
31
38
  /**
32
39
  * Links the target element with a slotted label element
33
40
  * via the target's attribute `aria-labelledby`.
34
41
  *
35
42
  * To unlink the previous slotted label element, pass `null` as `labelId`.
36
43
  */
37
- setLabelId(labelId: string | null): void;
44
+ setLabelId(labelId: string | null, fromUser: boolean | null): void;
38
45
 
39
46
  /**
40
47
  * Links the target element with a slotted error element via the target's attribute:
@@ -3,7 +3,11 @@
3
3
  * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { addValueToAttribute, removeValueFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
6
+ import {
7
+ removeAriaIDReference,
8
+ restoreGeneratedAriaIDReference,
9
+ setAriaIDReference,
10
+ } from '@vaadin/a11y-base/src/aria-id-reference.js';
7
11
 
8
12
  /**
9
13
  * A controller for managing ARIA attributes for a field element:
@@ -36,6 +40,7 @@ export class FieldAriaController {
36
40
  this.__setLabelIdToAriaAttribute(this.__labelId);
37
41
  this.__setErrorIdToAriaAttribute(this.__errorId);
38
42
  this.__setHelperIdToAriaAttribute(this.__helperId);
43
+ this.setAriaLabel(this.__label);
39
44
  }
40
45
 
41
46
  /**
@@ -50,6 +55,18 @@ export class FieldAriaController {
50
55
  this.__required = required;
51
56
  }
52
57
 
58
+ /**
59
+ * Defines the `aria-label` attribute of the target element.
60
+ *
61
+ * To remove the attribute, pass `null` as `label`.
62
+ *
63
+ * @param {string | null | undefined} label
64
+ */
65
+ setAriaLabel(label) {
66
+ this.__setAriaLabelToAttribute(label);
67
+ this.__label = label;
68
+ }
69
+
53
70
  /**
54
71
  * Links the target element with a slotted label element
55
72
  * via the target's attribute `aria-labelledby`.
@@ -58,9 +75,14 @@ export class FieldAriaController {
58
75
  *
59
76
  * @param {string | null} labelId
60
77
  */
61
- setLabelId(labelId) {
62
- this.__setLabelIdToAriaAttribute(labelId, this.__labelId);
63
- this.__labelId = labelId;
78
+ setLabelId(labelId, fromUser = false) {
79
+ const oldLabelId = fromUser ? this.__labelIdFromUser : this.__labelId;
80
+ this.__setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser);
81
+ if (fromUser) {
82
+ this.__labelIdFromUser = labelId;
83
+ } else {
84
+ this.__labelId = labelId;
85
+ }
64
86
  }
65
87
 
66
88
  /**
@@ -91,13 +113,31 @@ export class FieldAriaController {
91
113
  this.__helperId = helperId;
92
114
  }
93
115
 
116
+ /**
117
+ * @param {string | null | undefined} label
118
+ * @private
119
+ * */
120
+ __setAriaLabelToAttribute(label) {
121
+ if (!this.__target) {
122
+ return;
123
+ }
124
+ if (label) {
125
+ removeAriaIDReference(this.__target, 'aria-labelledby');
126
+ this.__target.setAttribute('aria-label', label);
127
+ } else if (this.__label) {
128
+ restoreGeneratedAriaIDReference(this.__target, 'aria-labelledby');
129
+ this.__target.removeAttribute('aria-label');
130
+ }
131
+ }
132
+
94
133
  /**
95
134
  * @param {string | null | undefined} labelId
96
135
  * @param {string | null | undefined} oldLabelId
136
+ * @param {boolean | null | undefined} fromUser
97
137
  * @private
98
138
  */
99
- __setLabelIdToAriaAttribute(labelId, oldLabelId) {
100
- this.__setAriaAttributeId('aria-labelledby', labelId, oldLabelId);
139
+ __setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser) {
140
+ setAriaIDReference(this.__target, 'aria-labelledby', { newId: labelId, oldId: oldLabelId, fromUser });
101
141
  }
102
142
 
103
143
  /**
@@ -108,11 +148,8 @@ export class FieldAriaController {
108
148
  __setErrorIdToAriaAttribute(errorId, oldErrorId) {
109
149
  // For groups, add all IDs to aria-labelledby rather than aria-describedby -
110
150
  // 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
- }
151
+ const ariaAttribute = this.__isGroupField ? 'aria-labelledby' : 'aria-describedby';
152
+ setAriaIDReference(this.__target, ariaAttribute, { newId: errorId, oldId: oldErrorId, fromUser: false });
116
153
  }
117
154
 
118
155
  /**
@@ -123,11 +160,8 @@ export class FieldAriaController {
123
160
  __setHelperIdToAriaAttribute(helperId, oldHelperId) {
124
161
  // For groups, add all IDs to aria-labelledby rather than aria-describedby -
125
162
  // 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
- }
163
+ const ariaAttribute = this.__isGroupField ? 'aria-labelledby' : 'aria-describedby';
164
+ setAriaIDReference(this.__target, ariaAttribute, { newId: helperId, oldId: oldHelperId, fromUser: false });
131
165
  }
132
166
 
133
167
  /**
@@ -150,23 +184,4 @@ export class FieldAriaController {
150
184
  this.__target.removeAttribute('aria-required');
151
185
  }
152
186
  }
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
187
  }
@@ -4,6 +4,12 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
 
7
+ /**
8
+ * Returns the actually focused element by traversing shadow
9
+ * trees recursively to ensure it's the leaf element.
10
+ */
11
+ export declare function getDeepActiveElement(): Element;
12
+
7
13
  /**
8
14
  * Returns true if the window has received a keydown
9
15
  * event since the last mousedown event.
@@ -26,6 +26,20 @@ window.addEventListener(
26
26
  { capture: true },
27
27
  );
28
28
 
29
+ /**
30
+ * Returns the actually focused element by traversing shadow
31
+ * trees recursively to ensure it's the leaf element.
32
+ *
33
+ * @return {Element}
34
+ */
35
+ export function getDeepActiveElement() {
36
+ let host = document.activeElement || document.body;
37
+ while (host.shadowRoot && host.shadowRoot.activeElement) {
38
+ host = host.shadowRoot.activeElement;
39
+ }
40
+ return host;
41
+ }
42
+
29
43
  /**
30
44
  * Returns true if the window has received a keydown
31
45
  * event since the last mousedown event.