@momentum-design/components 0.129.44 → 0.129.45
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/dist/browser/index.js +200 -200
- package/dist/browser/index.js.map +4 -4
- package/dist/components/dialog/dialog.component.d.ts +1 -1
- package/dist/components/dialog/dialog.component.js +1 -1
- package/dist/components/popover/popover.component.d.ts +1 -1
- package/dist/components/popover/popover.component.js +1 -1
- package/dist/custom-elements.json +15 -4125
- package/dist/utils/dom.d.ts +83 -0
- package/dist/utils/dom.js +164 -0
- package/dist/utils/mixins/{FocusTrapMixin.d.ts → focus/FocusTrapMixin.d.ts} +2 -2
- package/dist/utils/mixins/focus/FocusTrapMixin.js +190 -0
- package/dist/utils/mixins/focus/FocusTrapStack.d.ts +32 -0
- package/dist/utils/mixins/focus/FocusTrapStack.js +69 -0
- package/package.json +1 -1
- package/dist/utils/mixins/FocusTrapMixin.js +0 -418
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
};
|
|
7
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
-
};
|
|
10
|
-
/* eslint-disable no-use-before-define */
|
|
11
|
-
/* eslint-disable max-classes-per-file */
|
|
12
|
-
import { property } from 'lit/decorators.js';
|
|
13
|
-
/**
|
|
14
|
-
* FocusTrapStack manages a stack of active focus traps,
|
|
15
|
-
* ensuring only one focus trap is active at a time.
|
|
16
|
-
*
|
|
17
|
-
* This also makes sure there is only one keydown listener active at a time,
|
|
18
|
-
* which is necessary to handle focus trapping correctly.
|
|
19
|
-
*
|
|
20
|
-
* Handling iFrames is supported, as long as there are focusable elements around the iFrame.
|
|
21
|
-
* Otherwise it will not work as expected.
|
|
22
|
-
*/
|
|
23
|
-
class FocusTrapStack {
|
|
24
|
-
static get stackArray() {
|
|
25
|
-
return Array.from(this.stack);
|
|
26
|
-
}
|
|
27
|
-
static addKeydownListener(keydownListener) {
|
|
28
|
-
this.currentKeydownListener = keydownListener;
|
|
29
|
-
document.addEventListener('keydown', keydownListener);
|
|
30
|
-
}
|
|
31
|
-
static removeKeydownListener() {
|
|
32
|
-
if (this.currentKeydownListener) {
|
|
33
|
-
document.removeEventListener('keydown', this.currentKeydownListener);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Activates a focus trap by adding it to the stack.
|
|
38
|
-
* It deactivates all other traps in the stack to ensure only one trap is active
|
|
39
|
-
*
|
|
40
|
-
* @param trap - The focus trap to activate.
|
|
41
|
-
*/
|
|
42
|
-
static activate(trap) {
|
|
43
|
-
// Deactivate all other traps
|
|
44
|
-
this.stackArray.forEach(activeTrap => {
|
|
45
|
-
if (activeTrap !== trap) {
|
|
46
|
-
activeTrap.setIsFocusTrapActivated(false);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
this.stack.add(trap);
|
|
50
|
-
// remove the current keydown listener if it exists
|
|
51
|
-
// and add a new one for the current trap
|
|
52
|
-
this.removeKeydownListener();
|
|
53
|
-
this.addKeydownListener(trap.handleTabKeydown.bind(trap));
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Deactivates a focus trap by removing it from the stack.
|
|
57
|
-
* Activates the previous trap in the stack if any.
|
|
58
|
-
*
|
|
59
|
-
* @param trap - The focus trap to deactivate.
|
|
60
|
-
*/
|
|
61
|
-
static deactivate(trap) {
|
|
62
|
-
if (!this.stack.has(trap)) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
this.stack.delete(trap);
|
|
66
|
-
this.removeKeydownListener();
|
|
67
|
-
// activate the previous trap in the stack if any
|
|
68
|
-
if (this.stack.size > 0) {
|
|
69
|
-
const lastTrap = this.stackArray.pop();
|
|
70
|
-
if (lastTrap) {
|
|
71
|
-
lastTrap.setIsFocusTrapActivated(true);
|
|
72
|
-
this.addKeydownListener(lastTrap.handleTabKeydown.bind(lastTrap));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
FocusTrapStack.stack = new Set();
|
|
78
|
-
FocusTrapStack.currentKeydownListener = null;
|
|
79
|
-
export const FocusTrapMixin = (superClass) => {
|
|
80
|
-
class FocusTrap extends superClass {
|
|
81
|
-
constructor() {
|
|
82
|
-
super(...arguments);
|
|
83
|
-
/**
|
|
84
|
-
* Determines whether focus should wrap around when reaching the first or last focusable element.
|
|
85
|
-
* If true, focus will cycle from end to start and vice versa.
|
|
86
|
-
*
|
|
87
|
-
* This only applies when `focusTrap` is true.
|
|
88
|
-
* @default true
|
|
89
|
-
*/
|
|
90
|
-
this.shouldFocusTrapWrap = true;
|
|
91
|
-
/** @internal */
|
|
92
|
-
this.focusTrapIndex = -1;
|
|
93
|
-
/** @internal */
|
|
94
|
-
this.focusableElements = [];
|
|
95
|
-
/** @internal */
|
|
96
|
-
this.isFocusTrapActivated = false;
|
|
97
|
-
}
|
|
98
|
-
setIsFocusTrapActivated(isActivated) {
|
|
99
|
-
this.isFocusTrapActivated = isActivated;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Activate the focus trap
|
|
103
|
-
*/
|
|
104
|
-
activateFocusTrap() {
|
|
105
|
-
this.setIsFocusTrapActivated(true);
|
|
106
|
-
FocusTrapStack.activate(this);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Deactivate the focus trap.
|
|
110
|
-
*/
|
|
111
|
-
deactivateFocusTrap() {
|
|
112
|
-
this.setIsFocusTrapActivated(false);
|
|
113
|
-
FocusTrapStack.deactivate(this);
|
|
114
|
-
this.focusTrapIndex = -1;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Checks if the element has no client rectangles (not visible in the viewport).
|
|
118
|
-
*
|
|
119
|
-
* @param element - The element to check.
|
|
120
|
-
* @returns True if the element has no client rectangles.
|
|
121
|
-
*/
|
|
122
|
-
hasNoClientRects(element) {
|
|
123
|
-
return element.getClientRects().length === 0;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Checks if the element has zero dimensions (width and height are both 0).
|
|
127
|
-
*
|
|
128
|
-
* @param element - The element to check.
|
|
129
|
-
* @returns True if the element has zero dimensions.
|
|
130
|
-
*/
|
|
131
|
-
hasZeroDimensions(element) {
|
|
132
|
-
const { width, height } = element.getBoundingClientRect();
|
|
133
|
-
const { offsetWidth, offsetHeight } = element;
|
|
134
|
-
return offsetWidth + offsetHeight + height + width === 0;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Determines if the element is not visible in the DOM.
|
|
138
|
-
*
|
|
139
|
-
* @param element - The element to check.
|
|
140
|
-
* @returns True if the element is not visible.
|
|
141
|
-
*/
|
|
142
|
-
isNotVisible(element) {
|
|
143
|
-
return this.hasZeroDimensions(element) || this.hasNoClientRects(element);
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Checks if the element has inline styles that make it hidden.
|
|
147
|
-
*
|
|
148
|
-
* @param element - The element to check.
|
|
149
|
-
* @returns True if the element has inline styles that make it hidden.
|
|
150
|
-
*/
|
|
151
|
-
hasHiddenStyle(element) {
|
|
152
|
-
const { display, opacity, visibility } = element.style;
|
|
153
|
-
return display === 'none' || opacity === '0' || visibility === 'hidden' || visibility === 'collapse';
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Checks if the element is hidden by a computed style.
|
|
157
|
-
*
|
|
158
|
-
* @param element - The element to check.
|
|
159
|
-
* @returns True if the element is hidden by a computed style.
|
|
160
|
-
*/
|
|
161
|
-
hasComputedHidden(element) {
|
|
162
|
-
const computedStyle = getComputedStyle(element);
|
|
163
|
-
return computedStyle.visibility === 'hidden' || computedStyle.height === '0' || computedStyle.display === 'none';
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Checks if the element is hidden from the user.
|
|
167
|
-
*
|
|
168
|
-
* @param element - The element to check.
|
|
169
|
-
* @returns True if the element is hidden.
|
|
170
|
-
*/
|
|
171
|
-
isHidden(element) {
|
|
172
|
-
return (element.hasAttribute('hidden') ||
|
|
173
|
-
element.getAttribute('aria-hidden') === 'true' ||
|
|
174
|
-
this.hasHiddenStyle(element) ||
|
|
175
|
-
this.isNotVisible(element) ||
|
|
176
|
-
this.hasComputedHidden(element));
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Checks if the element is disabled.
|
|
180
|
-
*
|
|
181
|
-
* @param element - The element to check.
|
|
182
|
-
* @returns True if the element is disabled.
|
|
183
|
-
*/
|
|
184
|
-
isDisabled(element) {
|
|
185
|
-
return element.disabled;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Checks if the element is not tabbable.
|
|
189
|
-
*
|
|
190
|
-
* @param element - The element to check.
|
|
191
|
-
* @returns True if the element is not tabbable.
|
|
192
|
-
*/
|
|
193
|
-
isNotTabbable(element) {
|
|
194
|
-
return element.getAttribute('tabindex') === '-1';
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Checks if the element is interactive.
|
|
198
|
-
*
|
|
199
|
-
* @param element - The element to check.
|
|
200
|
-
* @returns True if the element is interactive.
|
|
201
|
-
*/
|
|
202
|
-
isInteractiveElement(element) {
|
|
203
|
-
const interactiveTags = new Set(['BUTTON', 'DETAILS', 'EMBED', 'IFRAME', 'SELECT', 'TEXTAREA']);
|
|
204
|
-
if (interactiveTags.has(element.tagName)) {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
if (element instanceof HTMLAnchorElement && element.hasAttribute('href')) {
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
if (element instanceof HTMLInputElement && element.type !== 'hidden') {
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
if ((element instanceof HTMLAudioElement || element instanceof HTMLVideoElement) &&
|
|
214
|
-
element.hasAttribute('controls')) {
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
if ((element instanceof HTMLImageElement || element instanceof HTMLObjectElement) &&
|
|
218
|
-
element.hasAttribute('usemap')) {
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
if (element.hasAttribute('tabindex') && element.tabIndex > -1) {
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Checks if the element is focusable.
|
|
228
|
-
*
|
|
229
|
-
* @param element - The element to check.
|
|
230
|
-
* @returns True if the element is focusable.
|
|
231
|
-
*/
|
|
232
|
-
isFocusable(element) {
|
|
233
|
-
if (this.isHidden(element) || this.isNotTabbable(element) || this.isDisabled(element)) {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
return this.isInteractiveElement(element);
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Recursively finds all focusable elements within the given root and its descendants.
|
|
240
|
-
*
|
|
241
|
-
* Make sure this is performant, as it will be called multiple times.
|
|
242
|
-
*
|
|
243
|
-
* @param root - The root element to search for focusable elements.
|
|
244
|
-
* @param matches - The set of focusable elements.
|
|
245
|
-
* @returns The list of focusable elements.
|
|
246
|
-
*/
|
|
247
|
-
findFocusable(root, matches = new Set()) {
|
|
248
|
-
if (root instanceof HTMLElement && this.isFocusable(root)) {
|
|
249
|
-
matches.add(root);
|
|
250
|
-
}
|
|
251
|
-
let children = [];
|
|
252
|
-
if (root.children.length) {
|
|
253
|
-
children = Array.from(root.children);
|
|
254
|
-
}
|
|
255
|
-
else if (root instanceof HTMLElement && root.shadowRoot) {
|
|
256
|
-
children = Array.from(root.shadowRoot.children);
|
|
257
|
-
}
|
|
258
|
-
children.forEach((child) => {
|
|
259
|
-
const element = child;
|
|
260
|
-
if (this.isFocusable(element)) {
|
|
261
|
-
matches.add(element);
|
|
262
|
-
}
|
|
263
|
-
if (element.shadowRoot) {
|
|
264
|
-
this.findFocusable(element.shadowRoot, matches);
|
|
265
|
-
}
|
|
266
|
-
else if (element.tagName === 'SLOT') {
|
|
267
|
-
const assignedNodes = element.assignedElements({ flatten: true });
|
|
268
|
-
assignedNodes.forEach(node => {
|
|
269
|
-
if (node instanceof HTMLElement) {
|
|
270
|
-
this.findFocusable(node, matches);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
this.findFocusable(element, matches);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
return [...matches];
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Updates the list of focusable elements within the component's shadow root.
|
|
282
|
-
*/
|
|
283
|
-
setFocusableElements() {
|
|
284
|
-
if (!this.shadowRoot)
|
|
285
|
-
return;
|
|
286
|
-
this.focusableElements = this.findFocusable(this.shadowRoot, new Set());
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Sets the initial focus within the container.
|
|
290
|
-
*
|
|
291
|
-
* @param elementIndexToReceiveFocus - The index of the preferable element to focus.
|
|
292
|
-
*/
|
|
293
|
-
setInitialFocus(elementIndexToReceiveFocus = 0) {
|
|
294
|
-
this.setFocusableElements();
|
|
295
|
-
if (this.focusableElements.length === 0 || !this.focusTrap) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (this.focusableElements[elementIndexToReceiveFocus]) {
|
|
299
|
-
this.focusTrapIndex = elementIndexToReceiveFocus;
|
|
300
|
-
this.focusableElements[elementIndexToReceiveFocus].focus({ preventScroll: true });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Calculates the next index for the focus trap.
|
|
305
|
-
*
|
|
306
|
-
* @param currentIndex - The current index.
|
|
307
|
-
* @param step - The step to calculate the next index.
|
|
308
|
-
* @returns The next index.
|
|
309
|
-
*/
|
|
310
|
-
calculateNextIndex(currentIndex, step) {
|
|
311
|
-
const { length } = this.focusableElements;
|
|
312
|
-
if (currentIndex === -1) {
|
|
313
|
-
return step > 0 ? 0 : length - 1;
|
|
314
|
-
}
|
|
315
|
-
let nextIndex = currentIndex + step;
|
|
316
|
-
if (this.shouldFocusTrapWrap) {
|
|
317
|
-
if (nextIndex < 0)
|
|
318
|
-
nextIndex = length - 1;
|
|
319
|
-
if (nextIndex >= length)
|
|
320
|
-
nextIndex = 0;
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
if (nextIndex < 0)
|
|
324
|
-
nextIndex = 0;
|
|
325
|
-
if (nextIndex >= length)
|
|
326
|
-
nextIndex = length - 1;
|
|
327
|
-
}
|
|
328
|
-
return nextIndex;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Returns the deepest active element in the shadow DOM.
|
|
332
|
-
*
|
|
333
|
-
* @returns The deepest active element.
|
|
334
|
-
*/
|
|
335
|
-
getDeepActiveElement() {
|
|
336
|
-
var _a;
|
|
337
|
-
let host = document.activeElement || document.body;
|
|
338
|
-
while (host instanceof HTMLElement && ((_a = host.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement)) {
|
|
339
|
-
host = host.shadowRoot.activeElement;
|
|
340
|
-
}
|
|
341
|
-
return host || document.body;
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Finds the index of the active element within the focusable elements.
|
|
345
|
-
*
|
|
346
|
-
* @param activeElement - The active element.
|
|
347
|
-
* @returns The index of the active element.
|
|
348
|
-
*/
|
|
349
|
-
findElement(activeElement) {
|
|
350
|
-
return this.focusableElements.findIndex(element => this.isEqualFocusNode(activeElement, element));
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Checks if the active element is equal to the given element.
|
|
354
|
-
*
|
|
355
|
-
* @param activeElement - The active element.
|
|
356
|
-
* @param element - The element to compare.
|
|
357
|
-
* @returns True if the active element is equal to the given element.
|
|
358
|
-
*/
|
|
359
|
-
isEqualFocusNode(activeElement, element) {
|
|
360
|
-
if (activeElement.nodeType >= 0) {
|
|
361
|
-
return element.isEqualNode(activeElement) && element === activeElement;
|
|
362
|
-
}
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Traps focus within the container.
|
|
367
|
-
*
|
|
368
|
-
* @param direction - The direction of the focus trap.
|
|
369
|
-
* If true, the focus will be trapped in the previous element.
|
|
370
|
-
*/
|
|
371
|
-
trapFocus(event) {
|
|
372
|
-
// calculate the focusable elements
|
|
373
|
-
this.setFocusableElements();
|
|
374
|
-
if (this.focusableElements.length === 0) {
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
const activeElement = this.getDeepActiveElement();
|
|
378
|
-
const activeIndex = this.findElement(activeElement);
|
|
379
|
-
const direction = event.shiftKey;
|
|
380
|
-
if (direction) {
|
|
381
|
-
this.focusTrapIndex = this.calculateNextIndex(activeIndex, -1);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
this.focusTrapIndex = this.calculateNextIndex(activeIndex, 1);
|
|
385
|
-
}
|
|
386
|
-
const nextElement = this.focusableElements[this.focusTrapIndex];
|
|
387
|
-
if (nextElement.tagName === 'IFRAME') {
|
|
388
|
-
// If the next element is an iframe we should not focus it manually
|
|
389
|
-
// but just let the browser handle it.
|
|
390
|
-
// this only works if there are focusable elements around the iframe!
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (nextElement) {
|
|
394
|
-
event.preventDefault();
|
|
395
|
-
nextElement.focus();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Traps focus within the container.
|
|
400
|
-
*
|
|
401
|
-
* @param event - The keyboard event.
|
|
402
|
-
*/
|
|
403
|
-
// @ts-ignore - this is a method which will be called in the stack
|
|
404
|
-
handleTabKeydown(event) {
|
|
405
|
-
if (!this.isFocusTrapActivated) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
if (event.key === 'Tab') {
|
|
409
|
-
this.trapFocus(event);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
__decorate([
|
|
414
|
-
property({ type: Boolean, reflect: true, attribute: 'should-focus-trap-wrap' }),
|
|
415
|
-
__metadata("design:type", Boolean)
|
|
416
|
-
], FocusTrap.prototype, "shouldFocusTrapWrap", void 0);
|
|
417
|
-
return FocusTrap;
|
|
418
|
-
};
|