@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,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 Anton Korzunov
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview
|
|
9
|
+
*
|
|
10
|
+
* This module includes JS code copied from the `aria-hidden` package:
|
|
11
|
+
* https://github.com/theKashey/aria-hidden/blob/master/src/index.ts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @type {WeakMap<Element, number>} */
|
|
15
|
+
let counterMap = new WeakMap();
|
|
16
|
+
|
|
17
|
+
/** @type {WeakMap<Element, boolean>} */
|
|
18
|
+
let uncontrolledNodes = new WeakMap();
|
|
19
|
+
|
|
20
|
+
/** @type {Record<string, WeakMap<Element, number>>} */
|
|
21
|
+
let markerMap = {};
|
|
22
|
+
|
|
23
|
+
/** @type {number} */
|
|
24
|
+
let lockCount = 0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Element | Shadow} node
|
|
28
|
+
* @return {Element | null}
|
|
29
|
+
*/
|
|
30
|
+
const unwrapHost = (node) => (node ? node.host || unwrapHost(node.parentNode) : null);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {?Node} node
|
|
34
|
+
* @return {boolean}
|
|
35
|
+
*/
|
|
36
|
+
const isElement = (node) => node && node.nodeType === Node.ELEMENT_NODE;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {...unknown} args
|
|
40
|
+
*/
|
|
41
|
+
const logError = (...args) => {
|
|
42
|
+
console.error(`Error: ${args.join(' ')}. Skip setting aria-hidden.`);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {HTMLElement} parent
|
|
47
|
+
* @param {Element[]} targets
|
|
48
|
+
* @return {Element[]}
|
|
49
|
+
*/
|
|
50
|
+
const correctTargets = (parent, targets) => {
|
|
51
|
+
if (!isElement(parent)) {
|
|
52
|
+
logError(parent, 'is not a valid element');
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return targets
|
|
57
|
+
.map((target) => {
|
|
58
|
+
if (!isElement(target)) {
|
|
59
|
+
logError(target, 'is not a valid element');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (parent.contains(target)) {
|
|
64
|
+
return target;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const correctedTarget = unwrapHost(target);
|
|
68
|
+
if (correctedTarget && parent.contains(correctedTarget)) {
|
|
69
|
+
return correctedTarget;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logError(target, 'is not contained inside', parent);
|
|
73
|
+
return null;
|
|
74
|
+
})
|
|
75
|
+
.filter((x) => Boolean(x));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Marks everything except given node(or nodes) as aria-hidden
|
|
80
|
+
* @param {Element | Element[]} originalTarget - elements to keep on the page
|
|
81
|
+
* @param {HTMLElement} [parentNode] - top element, defaults to document.body
|
|
82
|
+
* @param {String} [markerName] - a special attribute to mark every node
|
|
83
|
+
* @param {String} [controlAttribute] - html Attribute to control
|
|
84
|
+
* @return {Function}
|
|
85
|
+
*/
|
|
86
|
+
const applyAttributeToOthers = (originalTarget, parentNode, markerName, controlAttribute) => {
|
|
87
|
+
const targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
|
|
88
|
+
|
|
89
|
+
if (!markerMap[markerName]) {
|
|
90
|
+
markerMap[markerName] = new WeakMap();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const markerCounter = markerMap[markerName];
|
|
94
|
+
|
|
95
|
+
/** @type {Element[]} */
|
|
96
|
+
const hiddenNodes = [];
|
|
97
|
+
|
|
98
|
+
/** @type {Set<Node>} */
|
|
99
|
+
const elementsToKeep = new Set();
|
|
100
|
+
|
|
101
|
+
/** @type {Set<Node>} */
|
|
102
|
+
const elementsToStop = new Set(targets);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {?Node} el
|
|
106
|
+
*/
|
|
107
|
+
const keep = (el) => {
|
|
108
|
+
if (!el || elementsToKeep.has(el)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
elementsToKeep.add(el);
|
|
113
|
+
keep(el.parentNode);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
targets.forEach(keep);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {?Node} el
|
|
120
|
+
*/
|
|
121
|
+
const deep = (parent) => {
|
|
122
|
+
if (!parent || elementsToStop.has(parent)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[...parent.children].forEach((node) => {
|
|
127
|
+
// Skip elements that don't need to be hidden
|
|
128
|
+
if (['template', 'script', 'style'].includes(node.localName)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (elementsToKeep.has(node)) {
|
|
133
|
+
deep(node);
|
|
134
|
+
} else {
|
|
135
|
+
const attr = node.getAttribute(controlAttribute);
|
|
136
|
+
const alreadyHidden = attr !== null && attr !== 'false';
|
|
137
|
+
const counterValue = (counterMap.get(node) || 0) + 1;
|
|
138
|
+
const markerValue = (markerCounter.get(node) || 0) + 1;
|
|
139
|
+
|
|
140
|
+
counterMap.set(node, counterValue);
|
|
141
|
+
markerCounter.set(node, markerValue);
|
|
142
|
+
hiddenNodes.push(node);
|
|
143
|
+
|
|
144
|
+
if (counterValue === 1 && alreadyHidden) {
|
|
145
|
+
uncontrolledNodes.set(node, true);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (markerValue === 1) {
|
|
149
|
+
node.setAttribute(markerName, 'true');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!alreadyHidden) {
|
|
153
|
+
node.setAttribute(controlAttribute, 'true');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
deep(parentNode);
|
|
160
|
+
|
|
161
|
+
elementsToKeep.clear();
|
|
162
|
+
|
|
163
|
+
lockCount += 1;
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
hiddenNodes.forEach((node) => {
|
|
167
|
+
const counterValue = counterMap.get(node) - 1;
|
|
168
|
+
const markerValue = markerCounter.get(node) - 1;
|
|
169
|
+
|
|
170
|
+
counterMap.set(node, counterValue);
|
|
171
|
+
markerCounter.set(node, markerValue);
|
|
172
|
+
|
|
173
|
+
if (!counterValue) {
|
|
174
|
+
if (uncontrolledNodes.has(node)) {
|
|
175
|
+
uncontrolledNodes.delete(node);
|
|
176
|
+
} else {
|
|
177
|
+
node.removeAttribute(controlAttribute);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!markerValue) {
|
|
182
|
+
node.removeAttribute(markerName);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
lockCount -= 1;
|
|
187
|
+
|
|
188
|
+
if (!lockCount) {
|
|
189
|
+
// clear
|
|
190
|
+
counterMap = new WeakMap();
|
|
191
|
+
counterMap = new WeakMap();
|
|
192
|
+
uncontrolledNodes = new WeakMap();
|
|
193
|
+
markerMap = {};
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Marks everything except given node(or nodes) as aria-hidden
|
|
200
|
+
* @param {Element | Element[]} originalTarget - elements to keep on the page
|
|
201
|
+
* @param {HTMLElement} [parentNode] - top element, defaults to document.body
|
|
202
|
+
* @param {String} [markerName] - a special attribute to mark every node
|
|
203
|
+
* @return {Function} undo command
|
|
204
|
+
*/
|
|
205
|
+
export const hideOthers = (originalTarget, parentNode = document.body, markerName = 'data-aria-hidden') => {
|
|
206
|
+
const targets = Array.from(Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
|
|
207
|
+
|
|
208
|
+
if (parentNode) {
|
|
209
|
+
// We should not hide ariaLive elements - https://github.com/theKashey/aria-hidden/issues/10
|
|
210
|
+
targets.push(...Array.from(parentNode.querySelectorAll('[aria-live]')));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return applyAttributeToOthers(targets, parentNode, markerName, 'aria-hidden');
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Marks everything except given node(or nodes) as inert
|
|
218
|
+
* @param {Element | Element[]} originalTarget - elements to keep on the page
|
|
219
|
+
* @param {HTMLElement} [parentNode] - top element, defaults to document.body
|
|
220
|
+
* @param {String} [markerName] - a special attribute to mark every node
|
|
221
|
+
* @return {Function} undo command
|
|
222
|
+
*/
|
|
223
|
+
export const inertOthers = (originalTarget, parentNode = document.body, markerName = 'data-inert-ed') => {
|
|
224
|
+
return applyAttributeToOthers(originalTarget, parentNode, markerName, 'inert');
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @return if current browser supports inert
|
|
229
|
+
*/
|
|
230
|
+
export const supportsInert = 'inert' in HTMLElement.prototype;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Automatic function to "suppress" DOM elements - _hide_ or _inert_ in the best possible way
|
|
234
|
+
* @param {Element | Element[]} originalTarget - elements to keep on the page
|
|
235
|
+
* @param {HTMLElement} [parentNode] - top element, defaults to document.body
|
|
236
|
+
* @param {String} [markerName] - a special attribute to mark every node
|
|
237
|
+
* @return {Function} undo command
|
|
238
|
+
*/
|
|
239
|
+
export const suppressOthers = (originalTarget, parentNode, markerName) =>
|
|
240
|
+
(supportsInert ? inertOthers : hideOthers)(originalTarget, parentNode, markerName);
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { ReactiveController } from 'lit';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A controller for handling modal state on the elements with `dialog` and `alertdialog` role.
|
|
10
|
+
* See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal
|
|
11
|
+
*/
|
|
12
|
+
export class AriaModalController implements ReactiveController {
|
|
13
|
+
/**
|
|
14
|
+
* The controller host element.
|
|
15
|
+
*/
|
|
16
|
+
host: HTMLElement;
|
|
17
|
+
|
|
18
|
+
constructor(node: HTMLElement);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Make the controller host element modal by trapping focus inside it and hiding
|
|
22
|
+
* other elements from screen readers using `aria-hidden="true"` appropriately.
|
|
23
|
+
*
|
|
24
|
+
* The method name is chosen to align with the one provided by native `<dialog>`:
|
|
25
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal
|
|
26
|
+
*/
|
|
27
|
+
showModal(): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Exit modal state: release focus and remove `aria-hidden` from other elements
|
|
31
|
+
* unless there are any other underlying elements that are also shown as modal.
|
|
32
|
+
*/
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { hideOthers } from './aria-hidden.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A controller for handling modal state on the elements with `dialog` and `alertdialog` role.
|
|
10
|
+
* See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal
|
|
11
|
+
*
|
|
12
|
+
* Note, the actual `role` and `aria-modal` attributes are supposed to be handled by the
|
|
13
|
+
* consumer web component. This is done in to ensure the controller only does one thing.
|
|
14
|
+
*/
|
|
15
|
+
export class AriaModalController {
|
|
16
|
+
/**
|
|
17
|
+
* @param {HTMLElement} host
|
|
18
|
+
*/
|
|
19
|
+
constructor(host) {
|
|
20
|
+
/**
|
|
21
|
+
* The controller host element.
|
|
22
|
+
*
|
|
23
|
+
* @type {HTMLElement}
|
|
24
|
+
*/
|
|
25
|
+
this.host = host;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Make the controller host modal by hiding other elements from screen readers
|
|
30
|
+
* using `aria-hidden` attribute (can be replaced with `inert` in the future).
|
|
31
|
+
*
|
|
32
|
+
* The method name is chosen to align with the one provided by native `<dialog>`:
|
|
33
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal
|
|
34
|
+
*/
|
|
35
|
+
showModal() {
|
|
36
|
+
this.__showOthers = hideOthers(this.host);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Remove `aria-hidden` from other elements unless there are any other
|
|
41
|
+
* controller hosts on the page activated by using `showModal()` call.
|
|
42
|
+
*/
|
|
43
|
+
close() {
|
|
44
|
+
if (this.__showOthers) {
|
|
45
|
+
this.__showOthers();
|
|
46
|
+
this.__showOthers = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
import type { FocusMixinClass } from './focus-mixin.js';
|
|
9
|
+
import type { TabindexMixinClass } from './tabindex-mixin.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A mixin to forward focus to an element in the light DOM.
|
|
13
|
+
*/
|
|
14
|
+
export declare function DelegateFocusMixin<T extends Constructor<HTMLElement>>(
|
|
15
|
+
base: T,
|
|
16
|
+
): Constructor<DelegateFocusMixinClass> &
|
|
17
|
+
Constructor<DisabledMixinClass> &
|
|
18
|
+
Constructor<FocusMixinClass> &
|
|
19
|
+
Constructor<TabindexMixinClass> &
|
|
20
|
+
T;
|
|
21
|
+
|
|
22
|
+
export declare class DelegateFocusMixinClass {
|
|
23
|
+
/**
|
|
24
|
+
* Specify that this control should have input focus when the page loads.
|
|
25
|
+
*/
|
|
26
|
+
autofocus: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A reference to the focusable element controlled by the mixin.
|
|
30
|
+
* It can be an input, textarea, button or any element with tabindex > -1.
|
|
31
|
+
*
|
|
32
|
+
* Any component implementing this mixin is expected to provide it
|
|
33
|
+
* by using `this._setFocusElement(input)` Polymer API.
|
|
34
|
+
*/
|
|
35
|
+
readonly focusElement: HTMLElement | null | undefined;
|
|
36
|
+
|
|
37
|
+
protected _addFocusListeners(element: HTMLElement): void;
|
|
38
|
+
|
|
39
|
+
protected _removeFocusListeners(element: HTMLElement): void;
|
|
40
|
+
|
|
41
|
+
protected _focusElementChanged(element: HTMLElement, oldElement: HTMLElement): void;
|
|
42
|
+
|
|
43
|
+
protected _onBlur(event: FocusEvent): void;
|
|
44
|
+
|
|
45
|
+
protected _onFocus(event: FocusEvent): void;
|
|
46
|
+
|
|
47
|
+
protected _setFocusElement(element: HTMLElement): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
import { FocusMixin } from './focus-mixin.js';
|
|
8
|
+
import { TabindexMixin } from './tabindex-mixin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A mixin to forward focus to an element in the light DOM.
|
|
12
|
+
*
|
|
13
|
+
* @polymerMixin
|
|
14
|
+
* @mixes FocusMixin
|
|
15
|
+
* @mixes TabindexMixin
|
|
16
|
+
*/
|
|
17
|
+
export const DelegateFocusMixin = dedupingMixin(
|
|
18
|
+
(superclass) =>
|
|
19
|
+
class DelegateFocusMixinClass extends FocusMixin(TabindexMixin(superclass)) {
|
|
20
|
+
static get properties() {
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* Specify that this control should have input focus when the page loads.
|
|
24
|
+
*/
|
|
25
|
+
autofocus: {
|
|
26
|
+
type: Boolean,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A reference to the focusable element controlled by the mixin.
|
|
31
|
+
* It can be an input, textarea, button or any element with tabindex > -1.
|
|
32
|
+
*
|
|
33
|
+
* Any component implementing this mixin is expected to provide it
|
|
34
|
+
* by using `this._setFocusElement(input)` Polymer API.
|
|
35
|
+
*
|
|
36
|
+
* Toggling `tabindex` attribute on the host element propagates its value to `focusElement`.
|
|
37
|
+
*
|
|
38
|
+
* @protected
|
|
39
|
+
* @type {!HTMLElement}
|
|
40
|
+
*/
|
|
41
|
+
focusElement: {
|
|
42
|
+
type: Object,
|
|
43
|
+
readOnly: true,
|
|
44
|
+
observer: '_focusElementChanged',
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Override the property from `TabIndexMixin`
|
|
49
|
+
* to ensure the `tabindex` attribute of the focus element
|
|
50
|
+
* will be restored to `0` after re-enabling the element.
|
|
51
|
+
*
|
|
52
|
+
* @protected
|
|
53
|
+
* @override
|
|
54
|
+
*/
|
|
55
|
+
_lastTabIndex: {
|
|
56
|
+
value: 0,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
super();
|
|
63
|
+
|
|
64
|
+
this._boundOnBlur = this._onBlur.bind(this);
|
|
65
|
+
this._boundOnFocus = this._onFocus.bind(this);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @protected */
|
|
69
|
+
ready() {
|
|
70
|
+
super.ready();
|
|
71
|
+
|
|
72
|
+
if (this.autofocus && !this.disabled) {
|
|
73
|
+
requestAnimationFrame(() => {
|
|
74
|
+
this.focus();
|
|
75
|
+
this.setAttribute('focus-ring', '');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @protected
|
|
82
|
+
* @override
|
|
83
|
+
*/
|
|
84
|
+
focus() {
|
|
85
|
+
if (!this.focusElement || this.disabled) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.focusElement.focus();
|
|
90
|
+
this._setFocused(true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @protected
|
|
95
|
+
* @override
|
|
96
|
+
*/
|
|
97
|
+
blur() {
|
|
98
|
+
if (!this.focusElement) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.focusElement.blur();
|
|
102
|
+
this._setFocused(false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @protected
|
|
107
|
+
* @override
|
|
108
|
+
*/
|
|
109
|
+
click() {
|
|
110
|
+
if (this.focusElement && !this.disabled) {
|
|
111
|
+
this.focusElement.click();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @protected */
|
|
116
|
+
_focusElementChanged(element, oldElement) {
|
|
117
|
+
if (element) {
|
|
118
|
+
element.disabled = this.disabled;
|
|
119
|
+
this._addFocusListeners(element);
|
|
120
|
+
this.__forwardTabIndex(this.tabindex);
|
|
121
|
+
} else if (oldElement) {
|
|
122
|
+
this._removeFocusListeners(oldElement);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {HTMLElement} element
|
|
128
|
+
* @protected
|
|
129
|
+
*/
|
|
130
|
+
_addFocusListeners(element) {
|
|
131
|
+
element.addEventListener('blur', this._boundOnBlur);
|
|
132
|
+
element.addEventListener('focus', this._boundOnFocus);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {HTMLElement} element
|
|
137
|
+
* @protected
|
|
138
|
+
*/
|
|
139
|
+
_removeFocusListeners(element) {
|
|
140
|
+
element.removeEventListener('blur', this._boundOnBlur);
|
|
141
|
+
element.removeEventListener('focus', this._boundOnFocus);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Focus event does not bubble, so we dispatch it manually
|
|
146
|
+
* on the host element to support adding focus listeners
|
|
147
|
+
* when the focusable element is placed in light DOM.
|
|
148
|
+
* @param {FocusEvent} event
|
|
149
|
+
* @protected
|
|
150
|
+
*/
|
|
151
|
+
_onFocus(event) {
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
this.dispatchEvent(new Event('focus'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Blur event does not bubble, so we dispatch it manually
|
|
158
|
+
* on the host element to support adding blur listeners
|
|
159
|
+
* when the focusable element is placed in light DOM.
|
|
160
|
+
* @param {FocusEvent} event
|
|
161
|
+
* @protected
|
|
162
|
+
*/
|
|
163
|
+
_onBlur(event) {
|
|
164
|
+
event.stopPropagation();
|
|
165
|
+
this.dispatchEvent(new Event('blur'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {Event} event
|
|
170
|
+
* @return {boolean}
|
|
171
|
+
* @protected
|
|
172
|
+
* @override
|
|
173
|
+
*/
|
|
174
|
+
_shouldSetFocus(event) {
|
|
175
|
+
return event.target === this.focusElement;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {boolean} disabled
|
|
180
|
+
* @param {boolean} oldDisabled
|
|
181
|
+
* @protected
|
|
182
|
+
* @override
|
|
183
|
+
*/
|
|
184
|
+
_disabledChanged(disabled, oldDisabled) {
|
|
185
|
+
super._disabledChanged(disabled, oldDisabled);
|
|
186
|
+
|
|
187
|
+
if (this.focusElement) {
|
|
188
|
+
this.focusElement.disabled = disabled;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (disabled) {
|
|
192
|
+
this.blur();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Override an observer from `TabindexMixin`.
|
|
198
|
+
* Do not call super to remove tabindex attribute
|
|
199
|
+
* from the host after it has been forwarded.
|
|
200
|
+
* @param {string} tabindex
|
|
201
|
+
* @protected
|
|
202
|
+
* @override
|
|
203
|
+
*/
|
|
204
|
+
_tabindexChanged(tabindex) {
|
|
205
|
+
this.__forwardTabIndex(tabindex);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @private */
|
|
209
|
+
__forwardTabIndex(tabindex) {
|
|
210
|
+
if (tabindex !== undefined && this.focusElement) {
|
|
211
|
+
this.focusElement.tabIndex = tabindex;
|
|
212
|
+
|
|
213
|
+
// Preserve tabindex="-1" on the host element
|
|
214
|
+
if (tabindex !== -1) {
|
|
215
|
+
this.tabindex = undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (this.disabled && tabindex) {
|
|
220
|
+
// If tabindex attribute was changed while component was disabled
|
|
221
|
+
if (tabindex !== -1) {
|
|
222
|
+
this._lastTabIndex = tabindex;
|
|
223
|
+
}
|
|
224
|
+
this.tabindex = undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
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 to provide disabled property for field components.
|
|
10
|
+
*/
|
|
11
|
+
export declare function DisabledMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<DisabledMixinClass> & T;
|
|
12
|
+
|
|
13
|
+
export declare class DisabledMixinClass {
|
|
14
|
+
/**
|
|
15
|
+
* If true, the user cannot interact with this element.
|
|
16
|
+
*/
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
|
|
19
|
+
protected _disabledChanged(disabled: boolean, oldDisabled: boolean): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 to provide disabled property for field components.
|
|
10
|
+
*
|
|
11
|
+
* @polymerMixin
|
|
12
|
+
*/
|
|
13
|
+
export const DisabledMixin = dedupingMixin(
|
|
14
|
+
(superclass) =>
|
|
15
|
+
class DisabledMixinClass extends superclass {
|
|
16
|
+
static get properties() {
|
|
17
|
+
return {
|
|
18
|
+
/**
|
|
19
|
+
* If true, the user cannot interact with this element.
|
|
20
|
+
*/
|
|
21
|
+
disabled: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
value: false,
|
|
24
|
+
observer: '_disabledChanged',
|
|
25
|
+
reflectToAttribute: true,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {boolean} disabled
|
|
32
|
+
* @protected
|
|
33
|
+
*/
|
|
34
|
+
_disabledChanged(disabled) {
|
|
35
|
+
this._setAriaDisabled(disabled);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {boolean} disabled
|
|
40
|
+
* @protected
|
|
41
|
+
*/
|
|
42
|
+
_setAriaDisabled(disabled) {
|
|
43
|
+
if (disabled) {
|
|
44
|
+
this.setAttribute('aria-disabled', 'true');
|
|
45
|
+
} else {
|
|
46
|
+
this.removeAttribute('aria-disabled');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Overrides the default element `click` method in order to prevent
|
|
52
|
+
* firing the `click` event when the element is disabled.
|
|
53
|
+
* @protected
|
|
54
|
+
* @override
|
|
55
|
+
*/
|
|
56
|
+
click() {
|
|
57
|
+
if (!this.disabled) {
|
|
58
|
+
super.click();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
);
|