@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,260 @@
|
|
|
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
|
+
// We consider the keyboard to be active if the window has received a keydown
|
|
8
|
+
// event since the last mousedown event.
|
|
9
|
+
let keyboardActive = false;
|
|
10
|
+
|
|
11
|
+
// Listen for top-level keydown and mousedown events.
|
|
12
|
+
// Use capture phase so we detect events even if they're handled.
|
|
13
|
+
window.addEventListener(
|
|
14
|
+
'keydown',
|
|
15
|
+
() => {
|
|
16
|
+
keyboardActive = true;
|
|
17
|
+
},
|
|
18
|
+
{ capture: true },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
window.addEventListener(
|
|
22
|
+
'mousedown',
|
|
23
|
+
() => {
|
|
24
|
+
keyboardActive = false;
|
|
25
|
+
},
|
|
26
|
+
{ capture: true },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if the window has received a keydown
|
|
31
|
+
* event since the last mousedown event.
|
|
32
|
+
*
|
|
33
|
+
* @return {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function isKeyboardActive() {
|
|
36
|
+
return keyboardActive;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if the element is hidden directly with `display: none` or `visibility: hidden`,
|
|
41
|
+
* false otherwise.
|
|
42
|
+
*
|
|
43
|
+
* The method doesn't traverse the element's ancestors, it only checks for the CSS properties
|
|
44
|
+
* set directly to or inherited by the element.
|
|
45
|
+
*
|
|
46
|
+
* @param {HTMLElement} element
|
|
47
|
+
* @return {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function isElementHiddenDirectly(element) {
|
|
50
|
+
// Check inline style first to save a re-flow.
|
|
51
|
+
const style = element.style;
|
|
52
|
+
if (style.visibility === 'hidden' || style.display === 'none') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const computedStyle = window.getComputedStyle(element);
|
|
57
|
+
if (computedStyle.visibility === 'hidden' || computedStyle.display === 'none') {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns if element `a` has lower tab order compared to element `b`
|
|
66
|
+
* (both elements are assumed to be focusable and tabbable).
|
|
67
|
+
* Elements with tabindex = 0 have lower tab order compared to elements
|
|
68
|
+
* with tabindex > 0.
|
|
69
|
+
* If both have same tabindex, it returns false.
|
|
70
|
+
*
|
|
71
|
+
* @param {HTMLElement} a
|
|
72
|
+
* @param {HTMLElement} b
|
|
73
|
+
* @return {boolean}
|
|
74
|
+
*/
|
|
75
|
+
function hasLowerTabOrder(a, b) {
|
|
76
|
+
// Normalize tabIndexes
|
|
77
|
+
// e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
|
|
78
|
+
const ati = Math.max(a.tabIndex, 0);
|
|
79
|
+
const bti = Math.max(b.tabIndex, 0);
|
|
80
|
+
return ati === 0 || bti === 0 ? bti > ati : ati > bti;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merge sort iterator, merges the two arrays into one, sorted by tabindex.
|
|
85
|
+
*
|
|
86
|
+
* @param {HTMLElement[]} left
|
|
87
|
+
* @param {HTMLElement[]} right
|
|
88
|
+
* @return {HTMLElement[]}
|
|
89
|
+
*/
|
|
90
|
+
function mergeSortByTabIndex(left, right) {
|
|
91
|
+
const result = [];
|
|
92
|
+
while (left.length > 0 && right.length > 0) {
|
|
93
|
+
if (hasLowerTabOrder(left[0], right[0])) {
|
|
94
|
+
result.push(right.shift());
|
|
95
|
+
} else {
|
|
96
|
+
result.push(left.shift());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result.concat(left, right);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sorts an array of elements by tabindex. Returns a new array.
|
|
105
|
+
*
|
|
106
|
+
* @param {HTMLElement[]} elements
|
|
107
|
+
* @return {HTMLElement[]}
|
|
108
|
+
*/
|
|
109
|
+
function sortElementsByTabIndex(elements) {
|
|
110
|
+
// Implement a merge sort as Array.prototype.sort does a non-stable sort
|
|
111
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
|
|
112
|
+
const len = elements.length;
|
|
113
|
+
if (len < 2) {
|
|
114
|
+
return elements;
|
|
115
|
+
}
|
|
116
|
+
const pivot = Math.ceil(len / 2);
|
|
117
|
+
const left = sortElementsByTabIndex(elements.slice(0, pivot));
|
|
118
|
+
const right = sortElementsByTabIndex(elements.slice(pivot));
|
|
119
|
+
|
|
120
|
+
return mergeSortByTabIndex(left, right);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns true if the element is hidden, false otherwise.
|
|
125
|
+
*
|
|
126
|
+
* An element is treated as hidden when any of the following conditions are met:
|
|
127
|
+
* - the element itself or one of its ancestors has `display: none`.
|
|
128
|
+
* - the element has or inherits `visibility: hidden`.
|
|
129
|
+
*
|
|
130
|
+
* @param {HTMLElement} element
|
|
131
|
+
* @return {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isElementHidden(element) {
|
|
134
|
+
// `offsetParent` is `null` when the element itself
|
|
135
|
+
// or one of its ancestors is hidden with `display: none`.
|
|
136
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
|
137
|
+
if (element.offsetParent === null) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return isElementHiddenDirectly(element);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns true if the element is focusable, otherwise false.
|
|
146
|
+
*
|
|
147
|
+
* The list of focusable elements is taken from http://stackoverflow.com/a/1600194/4228703.
|
|
148
|
+
* However, there isn't a definite list, it's up to the browser.
|
|
149
|
+
* The only standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html,
|
|
150
|
+
* according to which the only elements that have a `focus()` method are:
|
|
151
|
+
* - HTMLInputElement
|
|
152
|
+
* - HTMLSelectElement
|
|
153
|
+
* - HTMLTextAreaElement
|
|
154
|
+
* - HTMLAnchorElement
|
|
155
|
+
*
|
|
156
|
+
* This notably omits HTMLButtonElement and HTMLAreaElement.
|
|
157
|
+
* Referring to these tests with tabbables in different browsers
|
|
158
|
+
* http://allyjs.io/data-tables/focusable.html
|
|
159
|
+
*
|
|
160
|
+
* @param {HTMLElement} element
|
|
161
|
+
* @return {boolean}
|
|
162
|
+
*/
|
|
163
|
+
export function isElementFocusable(element) {
|
|
164
|
+
// The element cannot be focused if its `tabindex` attribute is set to `-1`.
|
|
165
|
+
if (element.matches('[tabindex="-1"]')) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Elements that cannot be focused if they have a `disabled` attribute.
|
|
170
|
+
if (element.matches('input, select, textarea, button, object')) {
|
|
171
|
+
return element.matches(':not([disabled])');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Elements that can be focused even if they have a `disabled` attribute.
|
|
175
|
+
return element.matches('a[href], area[href], iframe, [tabindex], [contentEditable]');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns true if the element is focused, false otherwise.
|
|
180
|
+
*
|
|
181
|
+
* @param {HTMLElement} element
|
|
182
|
+
* @return {boolean}
|
|
183
|
+
*/
|
|
184
|
+
export function isElementFocused(element) {
|
|
185
|
+
return element.getRootNode().activeElement === element;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns the normalized element tabindex. If not focusable, returns -1.
|
|
190
|
+
* It checks for the attribute "tabindex" instead of the element property
|
|
191
|
+
* `tabIndex` since browsers assign different values to it.
|
|
192
|
+
* e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
|
|
193
|
+
*
|
|
194
|
+
* @param {HTMLElement} element
|
|
195
|
+
* @return {number}
|
|
196
|
+
*/
|
|
197
|
+
function normalizeTabIndex(element) {
|
|
198
|
+
if (!isElementFocusable(element)) {
|
|
199
|
+
return -1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const tabIndex = element.getAttribute('tabindex') || 0;
|
|
203
|
+
return Number(tabIndex);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Searches for nodes that are tabbable and adds them to the `result` array.
|
|
208
|
+
* Returns if the `result` array needs to be sorted by tabindex.
|
|
209
|
+
*
|
|
210
|
+
* @param {Node} node The starting point for the search; added to `result` if tabbable.
|
|
211
|
+
* @param {HTMLElement[]} result
|
|
212
|
+
* @return {boolean}
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
function collectFocusableNodes(node, result) {
|
|
216
|
+
if (node.nodeType !== Node.ELEMENT_NODE || isElementHiddenDirectly(node)) {
|
|
217
|
+
// Don't traverse children if the node is not an HTML element or not visible.
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const element = /** @type {HTMLElement} */ (node);
|
|
222
|
+
const tabIndex = normalizeTabIndex(element);
|
|
223
|
+
let needsSort = tabIndex > 0;
|
|
224
|
+
if (tabIndex >= 0) {
|
|
225
|
+
result.push(element);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let children = [];
|
|
229
|
+
if (element.localName === 'slot') {
|
|
230
|
+
children = element.assignedNodes({ flatten: true });
|
|
231
|
+
} else {
|
|
232
|
+
// Use shadow root if possible, will check for distributed nodes.
|
|
233
|
+
children = (element.shadowRoot || element).children;
|
|
234
|
+
}
|
|
235
|
+
[...children].forEach((child) => {
|
|
236
|
+
// Ensure method is always invoked to collect focusable children.
|
|
237
|
+
needsSort = collectFocusableNodes(child, result) || needsSort;
|
|
238
|
+
});
|
|
239
|
+
return needsSort;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns a tab-ordered array of focusable elements for a root element.
|
|
244
|
+
* The resulting array will include the root element if it is focusable.
|
|
245
|
+
*
|
|
246
|
+
* The method traverses nodes in shadow DOM trees too if any.
|
|
247
|
+
*
|
|
248
|
+
* @param {HTMLElement} element
|
|
249
|
+
* @return {HTMLElement[]}
|
|
250
|
+
*/
|
|
251
|
+
export function getFocusableElements(element) {
|
|
252
|
+
const focusableElements = [];
|
|
253
|
+
const needsSortByTabIndex = collectFocusableNodes(element, focusableElements);
|
|
254
|
+
// If there is at least one element with tabindex > 0,
|
|
255
|
+
// we need to sort the final array by tabindex.
|
|
256
|
+
if (needsSortByTabIndex) {
|
|
257
|
+
return sortElementsByTabIndex(focusableElements);
|
|
258
|
+
}
|
|
259
|
+
return focusableElements;
|
|
260
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2022 - 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 { KeyboardMixinClass } from './keyboard-mixin.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A mixin for navigating items with keyboard.
|
|
11
|
+
*/
|
|
12
|
+
export declare function KeyboardDirectionMixin<T extends Constructor<HTMLElement>>(
|
|
13
|
+
base: T,
|
|
14
|
+
): Constructor<KeyboardDirectionMixinClass> & Constructor<KeyboardMixinClass> & T;
|
|
15
|
+
|
|
16
|
+
export declare class KeyboardDirectionMixinClass {
|
|
17
|
+
protected readonly focused: Element | null;
|
|
18
|
+
|
|
19
|
+
protected readonly _vertical: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns index of the next item that satisfies the given condition,
|
|
23
|
+
* based on the index of the current item and a numeric increment.
|
|
24
|
+
*/
|
|
25
|
+
protected _getAvailableIndex(
|
|
26
|
+
items: Element[],
|
|
27
|
+
index: number,
|
|
28
|
+
increment: number,
|
|
29
|
+
condition: (item: Element) => boolean,
|
|
30
|
+
): number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Focus the item at given index. Override this method to add custom logic.
|
|
34
|
+
*/
|
|
35
|
+
protected _focus(index: number, navigating: boolean): void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Focus the given item. Override this method to add custom logic.
|
|
39
|
+
*/
|
|
40
|
+
protected _focusItem(item: Element, navigating: boolean): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2022 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { isElementFocused, isElementHidden } from './focus-utils.js';
|
|
7
|
+
import { KeyboardMixin } from './keyboard-mixin.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A mixin for navigating items with keyboard.
|
|
11
|
+
*
|
|
12
|
+
* @polymerMixin
|
|
13
|
+
* @mixes KeyboardMixin
|
|
14
|
+
*/
|
|
15
|
+
export const KeyboardDirectionMixin = (superclass) =>
|
|
16
|
+
class KeyboardDirectionMixinClass extends KeyboardMixin(superclass) {
|
|
17
|
+
/**
|
|
18
|
+
* @return {Element | null}
|
|
19
|
+
* @protected
|
|
20
|
+
*/
|
|
21
|
+
get focused() {
|
|
22
|
+
return (this._getItems() || []).find(isElementFocused);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @return {boolean}
|
|
27
|
+
* @protected
|
|
28
|
+
*/
|
|
29
|
+
get _vertical() {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @protected */
|
|
34
|
+
focus() {
|
|
35
|
+
const items = this._getItems();
|
|
36
|
+
if (Array.isArray(items)) {
|
|
37
|
+
const idx = this._getAvailableIndex(items, 0, null, (item) => !isElementHidden(item));
|
|
38
|
+
if (idx >= 0) {
|
|
39
|
+
items[idx].focus();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the list of items participating in keyboard navigation.
|
|
46
|
+
* By default, it treats all the light DOM children as items.
|
|
47
|
+
* Override this method to provide custom list of elements.
|
|
48
|
+
*
|
|
49
|
+
* @return {Element[]}
|
|
50
|
+
* @protected
|
|
51
|
+
*/
|
|
52
|
+
_getItems() {
|
|
53
|
+
return Array.from(this.children);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Override an event listener from `KeyboardMixin`.
|
|
58
|
+
*
|
|
59
|
+
* @param {!KeyboardEvent} event
|
|
60
|
+
* @protected
|
|
61
|
+
* @override
|
|
62
|
+
*/
|
|
63
|
+
_onKeyDown(event) {
|
|
64
|
+
super._onKeyDown(event);
|
|
65
|
+
|
|
66
|
+
if (event.metaKey || event.ctrlKey) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { key } = event;
|
|
71
|
+
const items = this._getItems() || [];
|
|
72
|
+
const currentIdx = items.indexOf(this.focused);
|
|
73
|
+
|
|
74
|
+
let idx;
|
|
75
|
+
let increment;
|
|
76
|
+
|
|
77
|
+
const isRTL = !this._vertical && this.getAttribute('dir') === 'rtl';
|
|
78
|
+
const dirIncrement = isRTL ? -1 : 1;
|
|
79
|
+
|
|
80
|
+
if (this.__isPrevKey(key)) {
|
|
81
|
+
increment = -dirIncrement;
|
|
82
|
+
idx = currentIdx - dirIncrement;
|
|
83
|
+
} else if (this.__isNextKey(key)) {
|
|
84
|
+
increment = dirIncrement;
|
|
85
|
+
idx = currentIdx + dirIncrement;
|
|
86
|
+
} else if (key === 'Home') {
|
|
87
|
+
increment = 1;
|
|
88
|
+
idx = 0;
|
|
89
|
+
} else if (key === 'End') {
|
|
90
|
+
increment = -1;
|
|
91
|
+
idx = items.length - 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
idx = this._getAvailableIndex(items, idx, increment, (item) => !isElementHidden(item));
|
|
95
|
+
|
|
96
|
+
if (idx >= 0) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
this._focus(idx, true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} key
|
|
104
|
+
* @return {boolean}
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
__isPrevKey(key) {
|
|
108
|
+
return this._vertical ? key === 'ArrowUp' : key === 'ArrowLeft';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} key
|
|
113
|
+
* @return {boolean}
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
__isNextKey(key) {
|
|
117
|
+
return this._vertical ? key === 'ArrowDown' : key === 'ArrowRight';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Focus the item at given index. Override this method to add custom logic.
|
|
122
|
+
*
|
|
123
|
+
* @param {number} index
|
|
124
|
+
* @param {boolean} navigating
|
|
125
|
+
* @protected
|
|
126
|
+
*/
|
|
127
|
+
_focus(index, navigating = false) {
|
|
128
|
+
const items = this._getItems();
|
|
129
|
+
|
|
130
|
+
this._focusItem(items[index], navigating);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Focus the given item. Override this method to add custom logic.
|
|
135
|
+
*
|
|
136
|
+
* @param {Element} item
|
|
137
|
+
* @param {boolean} navigating
|
|
138
|
+
* @protected
|
|
139
|
+
*/
|
|
140
|
+
_focusItem(item) {
|
|
141
|
+
if (item) {
|
|
142
|
+
item.focus();
|
|
143
|
+
|
|
144
|
+
// Generally, the items are expected to implement `FocusMixin`
|
|
145
|
+
// that would set this attribute based on the `keydown` event.
|
|
146
|
+
// We set it manually to handle programmatic focus() calls.
|
|
147
|
+
item.setAttribute('focus-ring', '');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns index of the next item that satisfies the given condition,
|
|
153
|
+
* based on the index of the current item and a numeric increment.
|
|
154
|
+
*
|
|
155
|
+
* @param {Element[]} items - array of items to iterate over
|
|
156
|
+
* @param {number} index - index of the current item
|
|
157
|
+
* @param {number} increment - numeric increment, can be either 1 or -1
|
|
158
|
+
* @param {Function} condition - function used to check the item
|
|
159
|
+
* @return {number}
|
|
160
|
+
* @protected
|
|
161
|
+
*/
|
|
162
|
+
_getAvailableIndex(items, index, increment, condition) {
|
|
163
|
+
const totalItems = items.length;
|
|
164
|
+
let idx = index;
|
|
165
|
+
for (let i = 0; typeof idx === 'number' && i < totalItems; i += 1, idx += increment || 1) {
|
|
166
|
+
if (idx < 0) {
|
|
167
|
+
idx = totalItems - 1;
|
|
168
|
+
} else if (idx >= totalItems) {
|
|
169
|
+
idx = 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const item = items[idx];
|
|
173
|
+
|
|
174
|
+
if (!item.hasAttribute('disabled') && this.__isMatchingItem(item, condition)) {
|
|
175
|
+
return idx;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns true if the item matches condition.
|
|
183
|
+
*
|
|
184
|
+
* @param {Element} item - item to check
|
|
185
|
+
* @param {Function} condition - function used to check the item
|
|
186
|
+
* @return {number}
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
__isMatchingItem(item, condition) {
|
|
190
|
+
return typeof condition === 'function' ? condition(item) : true;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
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 that manages keyboard handling.
|
|
10
|
+
* The mixin subscribes to the keyboard events while an actual implementation
|
|
11
|
+
* for the event handlers is left to the client (a component or another mixin).
|
|
12
|
+
*/
|
|
13
|
+
export declare function KeyboardMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<KeyboardMixinClass> & T;
|
|
14
|
+
|
|
15
|
+
export declare class KeyboardMixinClass {
|
|
16
|
+
/**
|
|
17
|
+
* A handler for the "Enter" key. By default, it does nothing.
|
|
18
|
+
* Override the method to implement your own behavior.
|
|
19
|
+
*/
|
|
20
|
+
protected _onEnter(event: KeyboardEvent): void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A handler for the "Escape" key. By default, it does nothing.
|
|
24
|
+
* Override the method to implement your own behavior.
|
|
25
|
+
*/
|
|
26
|
+
protected _onEscape(event: KeyboardEvent): void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A handler for the `keydown` event. By default, it calls
|
|
30
|
+
* separate methods for handling "Enter" and "Escape" keys.
|
|
31
|
+
* Override the method to implement your own behavior.
|
|
32
|
+
*/
|
|
33
|
+
protected _onKeyDown(event: KeyboardEvent): void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A handler for the `keyup` event. By default, it does nothing.
|
|
37
|
+
* Override the method to implement your own behavior.
|
|
38
|
+
*/
|
|
39
|
+
protected _onKeyUp(event: KeyboardEvent): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A mixin that manages keyboard handling.
|
|
10
|
+
* The mixin subscribes to the keyboard events while an actual implementation
|
|
11
|
+
* for the event handlers is left to the client (a component or another mixin).
|
|
12
|
+
*
|
|
13
|
+
* @polymerMixin
|
|
14
|
+
*/
|
|
15
|
+
export const KeyboardMixin = dedupingMixin(
|
|
16
|
+
(superclass) =>
|
|
17
|
+
class KeyboardMixinClass extends superclass {
|
|
18
|
+
/** @protected */
|
|
19
|
+
ready() {
|
|
20
|
+
super.ready();
|
|
21
|
+
|
|
22
|
+
this.addEventListener('keydown', (event) => {
|
|
23
|
+
this._onKeyDown(event);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.addEventListener('keyup', (event) => {
|
|
27
|
+
this._onKeyUp(event);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A handler for the `keydown` event. By default, it calls
|
|
33
|
+
* separate methods for handling "Enter" and "Escape" keys.
|
|
34
|
+
* Override the method to implement your own behavior.
|
|
35
|
+
*
|
|
36
|
+
* @param {KeyboardEvent} event
|
|
37
|
+
* @protected
|
|
38
|
+
*/
|
|
39
|
+
_onKeyDown(event) {
|
|
40
|
+
switch (event.key) {
|
|
41
|
+
case 'Enter':
|
|
42
|
+
this._onEnter(event);
|
|
43
|
+
break;
|
|
44
|
+
case 'Escape':
|
|
45
|
+
this._onEscape(event);
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A handler for the `keyup` event. By default, it does nothing.
|
|
54
|
+
* Override the method to implement your own behavior.
|
|
55
|
+
*
|
|
56
|
+
* @param {KeyboardEvent} _event
|
|
57
|
+
* @protected
|
|
58
|
+
*/
|
|
59
|
+
_onKeyUp(_event) {
|
|
60
|
+
// To be implemented.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A handler for the "Enter" key. By default, it does nothing.
|
|
65
|
+
* Override the method to implement your own behavior.
|
|
66
|
+
*
|
|
67
|
+
* @param {KeyboardEvent} _event
|
|
68
|
+
* @protected
|
|
69
|
+
*/
|
|
70
|
+
_onEnter(_event) {
|
|
71
|
+
// To be implemented.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A handler for the "Escape" key. By default, it does nothing.
|
|
76
|
+
* Override the method to implement your own behavior.
|
|
77
|
+
*
|
|
78
|
+
* @param {KeyboardEvent} _event
|
|
79
|
+
* @protected
|
|
80
|
+
*/
|
|
81
|
+
_onEscape(_event) {
|
|
82
|
+
// To be implemented.
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
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 type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { KeyboardDirectionMixinClass } from './keyboard-direction-mixin.js';
|
|
8
|
+
import type { KeyboardMixinClass } from './keyboard-mixin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A mixin for list elements, facilitating navigation and selection of items.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ListMixin<T extends Constructor<HTMLElement>>(
|
|
14
|
+
base: T,
|
|
15
|
+
): Constructor<KeyboardDirectionMixinClass> & Constructor<KeyboardMixinClass> & Constructor<ListMixinClass> & T;
|
|
16
|
+
|
|
17
|
+
export declare class ListMixinClass {
|
|
18
|
+
/**
|
|
19
|
+
* Used for mixin detection because `instanceof` does not work with mixins.
|
|
20
|
+
*/
|
|
21
|
+
_hasVaadinListMixin: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* If true, the user cannot interact with this element.
|
|
25
|
+
* When the element is disabled, the selected item is
|
|
26
|
+
* not updated when `selected` property is changed.
|
|
27
|
+
*/
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The index of the item selected in the items array.
|
|
32
|
+
* Note: Not updated when used in `multiple` selection mode.
|
|
33
|
+
*/
|
|
34
|
+
selected: number | null | undefined;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Define how items are disposed in the dom.
|
|
38
|
+
* Possible values are: `horizontal|vertical`.
|
|
39
|
+
* It also changes navigation keys from left/right to up/down.
|
|
40
|
+
*/
|
|
41
|
+
orientation: 'horizontal' | 'vertical';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The list of items from which a selection can be made.
|
|
45
|
+
* It is populated from the elements passed to the light DOM,
|
|
46
|
+
* and updated dynamically when adding or removing items.
|
|
47
|
+
*
|
|
48
|
+
* The item elements must implement `Vaadin.ItemMixin`.
|
|
49
|
+
*
|
|
50
|
+
* Note: unlike `<vaadin-combo-box>`, this property is read-only,
|
|
51
|
+
* so if you want to provide items by iterating array of data,
|
|
52
|
+
* you have to use `dom-repeat` and place it to the light DOM.
|
|
53
|
+
*/
|
|
54
|
+
readonly items: Element[] | undefined;
|
|
55
|
+
|
|
56
|
+
protected readonly _scrollerElement: HTMLElement;
|
|
57
|
+
}
|