@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,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
|
|
7
|
+
import { timeOut } from '@vaadin/component-base/src/async.js';
|
|
8
|
+
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
|
9
|
+
import { getNormalizedScrollLeft, setNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
|
|
10
|
+
import { KeyboardDirectionMixin } from './keyboard-direction-mixin.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A mixin for list elements, facilitating navigation and selection of items.
|
|
14
|
+
*
|
|
15
|
+
* @polymerMixin
|
|
16
|
+
* @mixes KeyboardDirectionMixin
|
|
17
|
+
*/
|
|
18
|
+
export const ListMixin = (superClass) =>
|
|
19
|
+
class ListMixinClass extends KeyboardDirectionMixin(superClass) {
|
|
20
|
+
static get properties() {
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* Used for mixin detection because `instanceof` does not work with mixins.
|
|
24
|
+
* @type {boolean}
|
|
25
|
+
*/
|
|
26
|
+
_hasVaadinListMixin: {
|
|
27
|
+
value: true,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* If true, the user cannot interact with this element.
|
|
32
|
+
* When the element is disabled, the selected item is
|
|
33
|
+
* not updated when `selected` property is changed.
|
|
34
|
+
*/
|
|
35
|
+
disabled: {
|
|
36
|
+
type: Boolean,
|
|
37
|
+
value: false,
|
|
38
|
+
reflectToAttribute: true,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The index of the item selected in the items array.
|
|
43
|
+
* Note: Not updated when used in `multiple` selection mode.
|
|
44
|
+
*/
|
|
45
|
+
selected: {
|
|
46
|
+
type: Number,
|
|
47
|
+
reflectToAttribute: true,
|
|
48
|
+
notify: true,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Define how items are disposed in the dom.
|
|
53
|
+
* Possible values are: `horizontal|vertical`.
|
|
54
|
+
* It also changes navigation keys from left/right to up/down.
|
|
55
|
+
* @type {!ListOrientation}
|
|
56
|
+
*/
|
|
57
|
+
orientation: {
|
|
58
|
+
type: String,
|
|
59
|
+
reflectToAttribute: true,
|
|
60
|
+
value: '',
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The list of items from which a selection can be made.
|
|
65
|
+
* It is populated from the elements passed to the light DOM,
|
|
66
|
+
* and updated dynamically when adding or removing items.
|
|
67
|
+
*
|
|
68
|
+
* The item elements must implement `Vaadin.ItemMixin`.
|
|
69
|
+
*
|
|
70
|
+
* Note: unlike `<vaadin-combo-box>`, this property is read-only,
|
|
71
|
+
* so if you want to provide items by iterating array of data,
|
|
72
|
+
* you have to use `dom-repeat` and place it to the light DOM.
|
|
73
|
+
* @type {!Array<!Element> | undefined}
|
|
74
|
+
*/
|
|
75
|
+
items: {
|
|
76
|
+
type: Array,
|
|
77
|
+
readOnly: true,
|
|
78
|
+
notify: true,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The search buffer for the keyboard selection feature.
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
_searchBuf: {
|
|
86
|
+
type: String,
|
|
87
|
+
value: '',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static get observers() {
|
|
93
|
+
return ['_enhanceItems(items, orientation, selected, disabled)'];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @return {boolean}
|
|
98
|
+
* @protected
|
|
99
|
+
*/
|
|
100
|
+
get _isRTL() {
|
|
101
|
+
return !this._vertical && this.getAttribute('dir') === 'rtl';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @return {!HTMLElement}
|
|
106
|
+
* @protected
|
|
107
|
+
*/
|
|
108
|
+
get _scrollerElement() {
|
|
109
|
+
// Returning scroller element of the component
|
|
110
|
+
console.warn(`Please implement the '_scrollerElement' property in <${this.localName}>`);
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @return {boolean}
|
|
116
|
+
* @protected
|
|
117
|
+
*/
|
|
118
|
+
get _vertical() {
|
|
119
|
+
return this.orientation !== 'horizontal';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
focus() {
|
|
123
|
+
// In initialization (e.g vaadin-select) observer might not been run yet.
|
|
124
|
+
if (this._observer) {
|
|
125
|
+
this._observer.flush();
|
|
126
|
+
}
|
|
127
|
+
const firstItem = this.querySelector('[tabindex="0"]') || (this.items ? this.items[0] : null);
|
|
128
|
+
this._focusItem(firstItem);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @protected */
|
|
132
|
+
ready() {
|
|
133
|
+
super.ready();
|
|
134
|
+
|
|
135
|
+
this.addEventListener('click', (e) => this._onClick(e));
|
|
136
|
+
|
|
137
|
+
this._observer = new FlattenedNodesObserver(this, () => {
|
|
138
|
+
this._setItems(this._filterItems(FlattenedNodesObserver.getFlattenedNodes(this)));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Override method inherited from `KeyboardDirectionMixin`
|
|
144
|
+
* to use the stored list of item elements.
|
|
145
|
+
*
|
|
146
|
+
* @return {Element[]}
|
|
147
|
+
* @protected
|
|
148
|
+
* @override
|
|
149
|
+
*/
|
|
150
|
+
_getItems() {
|
|
151
|
+
return this.items;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @private */
|
|
155
|
+
_enhanceItems(items, orientation, selected, disabled) {
|
|
156
|
+
if (!disabled) {
|
|
157
|
+
if (items) {
|
|
158
|
+
this.setAttribute('aria-orientation', orientation || 'vertical');
|
|
159
|
+
items.forEach((item) => {
|
|
160
|
+
if (orientation) {
|
|
161
|
+
item.setAttribute('orientation', orientation);
|
|
162
|
+
} else {
|
|
163
|
+
item.removeAttribute('orientation');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this._setFocusable(selected || 0);
|
|
168
|
+
|
|
169
|
+
const itemToSelect = items[selected];
|
|
170
|
+
items.forEach((item) => {
|
|
171
|
+
item.selected = item === itemToSelect;
|
|
172
|
+
});
|
|
173
|
+
if (itemToSelect && !itemToSelect.disabled) {
|
|
174
|
+
this._scrollToItem(selected);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {!Array<!Element>} array
|
|
182
|
+
* @return {!Array<!Element>}
|
|
183
|
+
* @protected
|
|
184
|
+
*/
|
|
185
|
+
_filterItems(array) {
|
|
186
|
+
return array.filter((e) => e._hasVaadinItemMixin);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {!MouseEvent} event
|
|
191
|
+
* @protected
|
|
192
|
+
*/
|
|
193
|
+
_onClick(event) {
|
|
194
|
+
if (event.metaKey || event.shiftKey || event.ctrlKey || event.defaultPrevented) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const item = this._filterItems(event.composedPath())[0];
|
|
199
|
+
let idx;
|
|
200
|
+
if (item && !item.disabled && (idx = this.items.indexOf(item)) >= 0) {
|
|
201
|
+
this.selected = idx;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {number} currentIdx
|
|
207
|
+
* @param {string} key
|
|
208
|
+
* @return {number}
|
|
209
|
+
* @protected
|
|
210
|
+
*/
|
|
211
|
+
_searchKey(currentIdx, key) {
|
|
212
|
+
this._searchReset = Debouncer.debounce(this._searchReset, timeOut.after(500), () => {
|
|
213
|
+
this._searchBuf = '';
|
|
214
|
+
});
|
|
215
|
+
this._searchBuf += key.toLowerCase();
|
|
216
|
+
|
|
217
|
+
if (!this.items.some((item) => this.__isMatchingKey(item))) {
|
|
218
|
+
this._searchBuf = key.toLowerCase();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const idx = this._searchBuf.length === 1 ? currentIdx + 1 : currentIdx;
|
|
222
|
+
return this._getAvailableIndex(
|
|
223
|
+
this.items,
|
|
224
|
+
idx,
|
|
225
|
+
1,
|
|
226
|
+
(item) => this.__isMatchingKey(item) && getComputedStyle(item).display !== 'none',
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @private */
|
|
231
|
+
__isMatchingKey(item) {
|
|
232
|
+
return item.textContent
|
|
233
|
+
.replace(/[^\p{L}\p{Nd}]/gu, '')
|
|
234
|
+
.toLowerCase()
|
|
235
|
+
.startsWith(this._searchBuf);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Override an event listener from `KeyboardMixin`
|
|
240
|
+
* to search items by key.
|
|
241
|
+
*
|
|
242
|
+
* @param {!KeyboardEvent} event
|
|
243
|
+
* @protected
|
|
244
|
+
* @override
|
|
245
|
+
*/
|
|
246
|
+
_onKeyDown(event) {
|
|
247
|
+
if (event.metaKey || event.ctrlKey) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const key = event.key;
|
|
252
|
+
|
|
253
|
+
const currentIdx = this.items.indexOf(this.focused);
|
|
254
|
+
if (/[a-zA-Z0-9]/u.test(key) && key.length === 1) {
|
|
255
|
+
const idx = this._searchKey(currentIdx, key);
|
|
256
|
+
if (idx >= 0) {
|
|
257
|
+
this._focus(idx);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
super._onKeyDown(event);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {!Element} item
|
|
267
|
+
* @return {boolean}
|
|
268
|
+
* @protected
|
|
269
|
+
*/
|
|
270
|
+
_isItemHidden(item) {
|
|
271
|
+
return getComputedStyle(item).display === 'none';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @param {number} idx
|
|
276
|
+
* @protected
|
|
277
|
+
*/
|
|
278
|
+
_setFocusable(idx) {
|
|
279
|
+
idx = this._getAvailableIndex(this.items, idx, 1);
|
|
280
|
+
const item = this.items[idx];
|
|
281
|
+
this.items.forEach((e) => {
|
|
282
|
+
e.tabIndex = e === item ? 0 : -1;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {number} idx
|
|
288
|
+
* @protected
|
|
289
|
+
*/
|
|
290
|
+
_focus(idx) {
|
|
291
|
+
this.items.forEach((e, index) => {
|
|
292
|
+
e.focused = index === idx;
|
|
293
|
+
});
|
|
294
|
+
this._setFocusable(idx);
|
|
295
|
+
this._scrollToItem(idx);
|
|
296
|
+
super._focus(idx);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Scroll the container to have the next item by the edge of the viewport.
|
|
301
|
+
* @param {number} idx
|
|
302
|
+
* @protected
|
|
303
|
+
*/
|
|
304
|
+
_scrollToItem(idx) {
|
|
305
|
+
const item = this.items[idx];
|
|
306
|
+
if (!item) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const props = this._vertical ? ['top', 'bottom'] : this._isRTL ? ['right', 'left'] : ['left', 'right'];
|
|
311
|
+
|
|
312
|
+
const scrollerRect = this._scrollerElement.getBoundingClientRect();
|
|
313
|
+
const nextItemRect = (this.items[idx + 1] || item).getBoundingClientRect();
|
|
314
|
+
const prevItemRect = (this.items[idx - 1] || item).getBoundingClientRect();
|
|
315
|
+
|
|
316
|
+
let scrollDistance = 0;
|
|
317
|
+
if (
|
|
318
|
+
(!this._isRTL && nextItemRect[props[1]] >= scrollerRect[props[1]]) ||
|
|
319
|
+
(this._isRTL && nextItemRect[props[1]] <= scrollerRect[props[1]])
|
|
320
|
+
) {
|
|
321
|
+
scrollDistance = nextItemRect[props[1]] - scrollerRect[props[1]];
|
|
322
|
+
} else if (
|
|
323
|
+
(!this._isRTL && prevItemRect[props[0]] <= scrollerRect[props[0]]) ||
|
|
324
|
+
(this._isRTL && prevItemRect[props[0]] >= scrollerRect[props[0]])
|
|
325
|
+
) {
|
|
326
|
+
scrollDistance = prevItemRect[props[0]] - scrollerRect[props[0]];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this._scroll(scrollDistance);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {number} pixels
|
|
334
|
+
* @protected
|
|
335
|
+
*/
|
|
336
|
+
_scroll(pixels) {
|
|
337
|
+
if (this._vertical) {
|
|
338
|
+
this._scrollerElement.scrollTop += pixels;
|
|
339
|
+
} else {
|
|
340
|
+
const dir = this.getAttribute('dir') || 'ltr';
|
|
341
|
+
const scrollLeft = getNormalizedScrollLeft(this._scrollerElement, dir) + pixels;
|
|
342
|
+
setNormalizedScrollLeft(this._scrollerElement, dir, scrollLeft);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Fired when the selection is changed.
|
|
348
|
+
* Not fired when used in `multiple` selection mode.
|
|
349
|
+
*
|
|
350
|
+
* @event selected-changed
|
|
351
|
+
* @param {Object} detail
|
|
352
|
+
* @param {Object} detail.value the index of the item selected in the items array.
|
|
353
|
+
*/
|
|
354
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import type { DisabledMixinClass } from './disabled-mixin.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A mixin to toggle the `tabindex` attribute.
|
|
11
|
+
*
|
|
12
|
+
* The attribute is set to -1 whenever the user disables the element
|
|
13
|
+
* and restored with the last known value once the element is enabled.
|
|
14
|
+
*/
|
|
15
|
+
export declare function TabindexMixin<T extends Constructor<HTMLElement>>(
|
|
16
|
+
base: T,
|
|
17
|
+
): Constructor<DisabledMixinClass> & Constructor<TabindexMixinClass> & T;
|
|
18
|
+
|
|
19
|
+
export declare class TabindexMixinClass {
|
|
20
|
+
/**
|
|
21
|
+
* Indicates whether the element can be focused and where it participates in sequential keyboard navigation.
|
|
22
|
+
*/
|
|
23
|
+
tabindex: number | null | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stores the last known tabindex since the element has been disabled.
|
|
27
|
+
*/
|
|
28
|
+
protected _lastTabIndex: number | null | undefined;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* When the user has changed tabindex while the element is disabled,
|
|
32
|
+
* the observer reverts tabindex to -1 and rather saves the new tabindex value to apply it later.
|
|
33
|
+
* The new value will be applied as soon as the element becomes enabled.
|
|
34
|
+
*/
|
|
35
|
+
protected _tabindexChanged(tabindex: number | null | undefined): void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { DisabledMixin } from './disabled-mixin.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A mixin to toggle the `tabindex` attribute.
|
|
10
|
+
*
|
|
11
|
+
* The attribute is set to -1 whenever the user disables the element
|
|
12
|
+
* and restored with the last known value once the element is enabled.
|
|
13
|
+
*
|
|
14
|
+
* @polymerMixin
|
|
15
|
+
* @mixes DisabledMixin
|
|
16
|
+
*/
|
|
17
|
+
export const TabindexMixin = (superclass) =>
|
|
18
|
+
class TabindexMixinClass extends DisabledMixin(superclass) {
|
|
19
|
+
static get properties() {
|
|
20
|
+
return {
|
|
21
|
+
/**
|
|
22
|
+
* Indicates whether the element can be focused and where it participates in sequential keyboard navigation.
|
|
23
|
+
*
|
|
24
|
+
* @protected
|
|
25
|
+
*/
|
|
26
|
+
tabindex: {
|
|
27
|
+
type: Number,
|
|
28
|
+
reflectToAttribute: true,
|
|
29
|
+
observer: '_tabindexChanged',
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stores the last known tabindex since the element has been disabled.
|
|
34
|
+
*
|
|
35
|
+
* @protected
|
|
36
|
+
*/
|
|
37
|
+
_lastTabIndex: {
|
|
38
|
+
type: Number,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* When the element gets disabled, the observer saves the last known tabindex
|
|
45
|
+
* and makes the element not focusable by setting tabindex to -1.
|
|
46
|
+
* As soon as the element gets enabled, the observer restores the last known tabindex
|
|
47
|
+
* so that the element can be focusable again.
|
|
48
|
+
*
|
|
49
|
+
* @protected
|
|
50
|
+
* @override
|
|
51
|
+
*/
|
|
52
|
+
_disabledChanged(disabled, oldDisabled) {
|
|
53
|
+
super._disabledChanged(disabled, oldDisabled);
|
|
54
|
+
|
|
55
|
+
if (disabled) {
|
|
56
|
+
if (this.tabindex !== undefined) {
|
|
57
|
+
this._lastTabIndex = this.tabindex;
|
|
58
|
+
}
|
|
59
|
+
this.tabindex = -1;
|
|
60
|
+
} else if (oldDisabled) {
|
|
61
|
+
this.tabindex = this._lastTabIndex;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* When the user has changed tabindex while the element is disabled,
|
|
67
|
+
* the observer reverts tabindex to -1 and rather saves the new tabindex value to apply it later.
|
|
68
|
+
* The new value will be applied as soon as the element becomes enabled.
|
|
69
|
+
*
|
|
70
|
+
* @protected
|
|
71
|
+
*/
|
|
72
|
+
_tabindexChanged(tabindex) {
|
|
73
|
+
if (this.disabled && tabindex !== -1) {
|
|
74
|
+
this._lastTabIndex = tabindex;
|
|
75
|
+
this.tabindex = -1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|