@momentum-design/components 0.129.44 → 0.129.46
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 +280 -280
- package/dist/browser/index.js.map +4 -4
- package/dist/components/button/button.component.d.ts +9 -1
- package/dist/components/button/button.component.js +13 -2
- 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 +2 -2
- package/dist/components/popover/popover.component.js +24 -23
- package/dist/components/text/text.component.d.ts +11 -2
- package/dist/components/text/text.component.js +17 -2
- package/dist/components/tooltip/tooltip.component.d.ts +12 -1
- package/dist/components/tooltip/tooltip.component.js +23 -0
- package/dist/components/tooltip/tooltip.constants.d.ts +1 -0
- package/dist/components/tooltip/tooltip.constants.js +1 -0
- package/dist/custom-elements.json +151 -4125
- package/dist/utils/dom.d.ts +91 -0
- package/dist/utils/dom.js +171 -0
- package/dist/utils/mixins/OverflowMixin.d.ts +7 -0
- package/dist/utils/mixins/OverflowMixin.js +23 -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
package/dist/utils/dom.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OverflowMixinInterface } from './mixins/OverflowMixin';
|
|
1
2
|
/**
|
|
2
3
|
* nodeB precedes nodeA in either a pre-order depth-first traversal of a tree containing both
|
|
3
4
|
* (e.g., as a descendant or preceding sibling or a descendant of a preceding sibling or
|
|
@@ -24,3 +25,93 @@ export declare const isAfter: (nodeA: Element, nodeB: Element) => boolean;
|
|
|
24
25
|
* @see [compareDocumentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition)
|
|
25
26
|
*/
|
|
26
27
|
export declare const isBefore: (nodeA: Element, nodeB: Element) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Checks if the element has no client rectangles (not visible in the viewport).
|
|
30
|
+
*
|
|
31
|
+
* @param element - The element to check.
|
|
32
|
+
* @returns True if the element has no client rectangles.
|
|
33
|
+
*/
|
|
34
|
+
export declare const hasNoClientRects: (element: HTMLElement) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the element has zero dimensions (width and height are both 0).
|
|
37
|
+
*
|
|
38
|
+
* @param element - The element to check.
|
|
39
|
+
* @returns True if the element has zero dimensions.
|
|
40
|
+
*/
|
|
41
|
+
export declare const hasZeroDimensions: (element: HTMLElement) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Determines if the element is not visible in the DOM.
|
|
44
|
+
*
|
|
45
|
+
* @param element - The element to check.
|
|
46
|
+
* @returns True if the element is not visible.
|
|
47
|
+
*/
|
|
48
|
+
export declare const isNotVisible: (element: HTMLElement) => boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Checks if the element has inline styles that make it hidden.
|
|
51
|
+
*
|
|
52
|
+
* @param element - The element to check.
|
|
53
|
+
* @returns True if the element has inline styles that make it hidden.
|
|
54
|
+
*/
|
|
55
|
+
export declare const hasHiddenStyle: (element: HTMLElement) => boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Checks if the element is hidden by a computed style.
|
|
58
|
+
*
|
|
59
|
+
* @param element - The element to check.
|
|
60
|
+
* @returns True if the element is hidden by a computed style.
|
|
61
|
+
*/
|
|
62
|
+
export declare const hasComputedHidden: (element: HTMLElement) => boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Checks if the element is hidden from the user.
|
|
65
|
+
*
|
|
66
|
+
* @param element - The element to check.
|
|
67
|
+
* @returns True if the element is hidden.
|
|
68
|
+
*/
|
|
69
|
+
export declare const isHidden: (element: HTMLElement) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Checks if the element is disabled.
|
|
72
|
+
*
|
|
73
|
+
* @param element - The element to check.
|
|
74
|
+
* @returns True if the element is disabled.
|
|
75
|
+
*/
|
|
76
|
+
export declare const isDisabled: (element: any) => any;
|
|
77
|
+
/**
|
|
78
|
+
* Checks if the element is not tabbable.
|
|
79
|
+
*
|
|
80
|
+
* @param element - The element to check.
|
|
81
|
+
* @returns True if the element is not tabbable.
|
|
82
|
+
*/
|
|
83
|
+
export declare const isTabbable: (element: HTMLElement) => boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Checks if the element is interactive.
|
|
86
|
+
*
|
|
87
|
+
* @param element - The element to check.
|
|
88
|
+
* @returns True if the element is interactive.
|
|
89
|
+
*/
|
|
90
|
+
export declare const isInteractiveElement: (element: HTMLElement) => boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Checks if the element is focusable.
|
|
93
|
+
*
|
|
94
|
+
* @param element - The element to check.
|
|
95
|
+
* @returns True if the element is focusable.
|
|
96
|
+
*/
|
|
97
|
+
export declare const isFocusable: (element: HTMLElement) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Recursively finds all focusable elements within the given root and its descendants.
|
|
100
|
+
*
|
|
101
|
+
* Make sure this is performant, as it will be called multiple times.
|
|
102
|
+
*
|
|
103
|
+
* @param root - The root element to search for focusable elements.
|
|
104
|
+
* @returns The list of focusable elements.
|
|
105
|
+
*/
|
|
106
|
+
export declare const findFocusable: (root: ShadowRoot | HTMLElement | null) => HTMLElement[];
|
|
107
|
+
/**
|
|
108
|
+
* Get the active element from the DOM, including shadow DOMs
|
|
109
|
+
*/
|
|
110
|
+
export declare const getDomActiveElement: (root?: Document) => Element | null;
|
|
111
|
+
/**
|
|
112
|
+
* Type guard to check if an element inherits the OverflowMixin.
|
|
113
|
+
*
|
|
114
|
+
* @param element - The element to check
|
|
115
|
+
* @returns True if the element has the OverflowMixin methods
|
|
116
|
+
*/
|
|
117
|
+
export declare const hasOverflowMixin: <T extends HTMLElement>(element: T) => element is T & OverflowMixinInterface;
|
package/dist/utils/dom.js
CHANGED
|
@@ -25,3 +25,174 @@ export const isAfter = (nodeA, nodeB) => !!(nodeA.compareDocumentPosition(nodeB)
|
|
|
25
25
|
* @see [compareDocumentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition)
|
|
26
26
|
*/
|
|
27
27
|
export const isBefore = (nodeA, nodeB) => !!(nodeA.compareDocumentPosition(nodeB) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
28
|
+
/**
|
|
29
|
+
* Checks if the element has no client rectangles (not visible in the viewport).
|
|
30
|
+
*
|
|
31
|
+
* @param element - The element to check.
|
|
32
|
+
* @returns True if the element has no client rectangles.
|
|
33
|
+
*/
|
|
34
|
+
export const hasNoClientRects = (element) => element.getClientRects().length === 0;
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the element has zero dimensions (width and height are both 0).
|
|
37
|
+
*
|
|
38
|
+
* @param element - The element to check.
|
|
39
|
+
* @returns True if the element has zero dimensions.
|
|
40
|
+
*/
|
|
41
|
+
export const hasZeroDimensions = (element) => {
|
|
42
|
+
const { width, height } = element.getBoundingClientRect();
|
|
43
|
+
const { offsetWidth, offsetHeight } = element;
|
|
44
|
+
return offsetWidth + offsetHeight + height + width === 0;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Determines if the element is not visible in the DOM.
|
|
48
|
+
*
|
|
49
|
+
* @param element - The element to check.
|
|
50
|
+
* @returns True if the element is not visible.
|
|
51
|
+
*/
|
|
52
|
+
export const isNotVisible = (element) => hasZeroDimensions(element) || hasNoClientRects(element);
|
|
53
|
+
/**
|
|
54
|
+
* Checks if the element has inline styles that make it hidden.
|
|
55
|
+
*
|
|
56
|
+
* @param element - The element to check.
|
|
57
|
+
* @returns True if the element has inline styles that make it hidden.
|
|
58
|
+
*/
|
|
59
|
+
export const hasHiddenStyle = (element) => {
|
|
60
|
+
const { display, opacity, visibility } = element.style;
|
|
61
|
+
return display === 'none' || opacity === '0' || visibility === 'hidden' || visibility === 'collapse';
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Checks if the element is hidden by a computed style.
|
|
65
|
+
*
|
|
66
|
+
* @param element - The element to check.
|
|
67
|
+
* @returns True if the element is hidden by a computed style.
|
|
68
|
+
*/
|
|
69
|
+
export const hasComputedHidden = (element) => {
|
|
70
|
+
const computedStyle = getComputedStyle(element);
|
|
71
|
+
return computedStyle.visibility === 'hidden' || computedStyle.height === '0' || computedStyle.display === 'none';
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Checks if the element is hidden from the user.
|
|
75
|
+
*
|
|
76
|
+
* @param element - The element to check.
|
|
77
|
+
* @returns True if the element is hidden.
|
|
78
|
+
*/
|
|
79
|
+
export const isHidden = (element) => element.hasAttribute('hidden') ||
|
|
80
|
+
element.getAttribute('aria-hidden') === 'true' ||
|
|
81
|
+
hasHiddenStyle(element) ||
|
|
82
|
+
isNotVisible(element) ||
|
|
83
|
+
hasComputedHidden(element);
|
|
84
|
+
/**
|
|
85
|
+
* Checks if the element is disabled.
|
|
86
|
+
*
|
|
87
|
+
* @param element - The element to check.
|
|
88
|
+
* @returns True if the element is disabled.
|
|
89
|
+
*/
|
|
90
|
+
export const isDisabled = (element) => element.disabled;
|
|
91
|
+
/**
|
|
92
|
+
* Checks if the element is not tabbable.
|
|
93
|
+
*
|
|
94
|
+
* @param element - The element to check.
|
|
95
|
+
* @returns True if the element is not tabbable.
|
|
96
|
+
*/
|
|
97
|
+
export const isTabbable = (element) => element.getAttribute('tabindex') !== '-1';
|
|
98
|
+
/**
|
|
99
|
+
* Checks if the element is interactive.
|
|
100
|
+
*
|
|
101
|
+
* @param element - The element to check.
|
|
102
|
+
* @returns True if the element is interactive.
|
|
103
|
+
*/
|
|
104
|
+
export const isInteractiveElement = (element) => {
|
|
105
|
+
const interactiveTags = new Set(['BUTTON', 'DETAILS', 'EMBED', 'IFRAME', 'SELECT', 'TEXTAREA']);
|
|
106
|
+
if (interactiveTags.has(element.tagName)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (element instanceof HTMLAnchorElement && element.hasAttribute('href')) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (element instanceof HTMLInputElement && element.type !== 'hidden') {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if ((element instanceof HTMLAudioElement || element instanceof HTMLVideoElement) &&
|
|
116
|
+
element.hasAttribute('controls')) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if ((element instanceof HTMLImageElement || element instanceof HTMLObjectElement) && element.hasAttribute('usemap')) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (element.hasAttribute('tabindex') && element.tabIndex > -1) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Checks if the element is focusable.
|
|
129
|
+
*
|
|
130
|
+
* @param element - The element to check.
|
|
131
|
+
* @returns True if the element is focusable.
|
|
132
|
+
*/
|
|
133
|
+
export const isFocusable = (element) => !isDisabled(element) && isTabbable(element) && !isHidden(element) && isInteractiveElement(element);
|
|
134
|
+
/**
|
|
135
|
+
* Recursively finds all focusable elements within the given root and its descendants.
|
|
136
|
+
*
|
|
137
|
+
* Make sure this is performant, as it will be called multiple times.
|
|
138
|
+
*
|
|
139
|
+
* @param root - The root element to search for focusable elements.
|
|
140
|
+
* @returns The list of focusable elements.
|
|
141
|
+
*/
|
|
142
|
+
export const findFocusable = (root) => {
|
|
143
|
+
if (!root) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const matches = new Set();
|
|
147
|
+
const finder = (root) => {
|
|
148
|
+
if (root instanceof HTMLElement && isFocusable(root)) {
|
|
149
|
+
matches.add(root);
|
|
150
|
+
}
|
|
151
|
+
let children = [];
|
|
152
|
+
if (root instanceof HTMLElement && root.shadowRoot) {
|
|
153
|
+
children = Array.from(root.shadowRoot.children);
|
|
154
|
+
}
|
|
155
|
+
else if (root.children.length) {
|
|
156
|
+
children = Array.from(root.children);
|
|
157
|
+
}
|
|
158
|
+
children.forEach((child) => {
|
|
159
|
+
const element = child;
|
|
160
|
+
if (isFocusable(element)) {
|
|
161
|
+
matches.add(element);
|
|
162
|
+
}
|
|
163
|
+
if (element.shadowRoot) {
|
|
164
|
+
finder(element.shadowRoot);
|
|
165
|
+
}
|
|
166
|
+
else if (element.tagName === 'SLOT') {
|
|
167
|
+
const assignedNodes = element.assignedElements({ flatten: true });
|
|
168
|
+
assignedNodes.forEach(node => {
|
|
169
|
+
if (node instanceof HTMLElement) {
|
|
170
|
+
finder(node);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
finder(element);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
finder(root);
|
|
180
|
+
return [...matches];
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Get the active element from the DOM, including shadow DOMs
|
|
184
|
+
*/
|
|
185
|
+
export const getDomActiveElement = (root = document) => {
|
|
186
|
+
var _a;
|
|
187
|
+
let { activeElement } = root;
|
|
188
|
+
while ((_a = activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement)
|
|
189
|
+
activeElement = activeElement.shadowRoot.activeElement;
|
|
190
|
+
return activeElement;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Type guard to check if an element inherits the OverflowMixin.
|
|
194
|
+
*
|
|
195
|
+
* @param element - The element to check
|
|
196
|
+
* @returns True if the element has the OverflowMixin methods
|
|
197
|
+
*/
|
|
198
|
+
export const hasOverflowMixin = (element) => 'isWidthOverflowing' in element && typeof element.isWidthOverflowing === 'function';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { Constructor } from './index.types';
|
|
3
|
+
export declare abstract class OverflowMixinInterface {
|
|
4
|
+
protected get overflowElement(): HTMLElement;
|
|
5
|
+
isWidthOverflowing(): boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const OverflowMixin: <T extends Constructor<LitElement>>(superClass: T) => Constructor<OverflowMixinInterface> & T;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const OverflowMixin = (superClass) => {
|
|
2
|
+
class InnerMixinClass extends superClass {
|
|
3
|
+
/**
|
|
4
|
+
* Gets the element whose overflow will be monitored.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
get overflowElement() {
|
|
9
|
+
return this;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Determines if the content of the overflow element is overflowing its width.
|
|
13
|
+
*
|
|
14
|
+
* @returns True if the scroll width of the overflow element is greater than its client width.
|
|
15
|
+
*/
|
|
16
|
+
isWidthOverflowing() {
|
|
17
|
+
const el = this.overflowElement;
|
|
18
|
+
return el.scrollWidth > el.clientWidth;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Cast return type to your mixin's interface intersected with the superClass type
|
|
22
|
+
return InnerMixinClass;
|
|
23
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Component } from '
|
|
2
|
-
import type { Constructor } from '
|
|
1
|
+
import type { Component } from '../../../models';
|
|
2
|
+
import type { Constructor } from '../index.types';
|
|
3
3
|
export declare abstract class FocusTrapClassInterface {
|
|
4
4
|
protected abstract focusTrap: boolean;
|
|
5
5
|
setInitialFocus(elementIndexToReceiveFocus?: number): void;
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
import { findFocusable } from '../../dom';
|
|
14
|
+
import { FocusTrapStack } from './FocusTrapStack';
|
|
15
|
+
export const FocusTrapMixin = (superClass) => {
|
|
16
|
+
class FocusTrap extends superClass {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
/**
|
|
20
|
+
* Determines whether focus should wrap around when reaching the first or last focusable element.
|
|
21
|
+
* If true, focus will cycle from end to start and vice versa.
|
|
22
|
+
*
|
|
23
|
+
* This only applies when `focusTrap` is true.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
this.shouldFocusTrapWrap = true;
|
|
27
|
+
/** @internal */
|
|
28
|
+
this.focusTrapIndex = -1;
|
|
29
|
+
/** @internal */
|
|
30
|
+
this.focusableElements = [];
|
|
31
|
+
/** @internal */
|
|
32
|
+
this.isFocusTrapActivated = false;
|
|
33
|
+
}
|
|
34
|
+
setIsFocusTrapActivated(isActivated) {
|
|
35
|
+
this.isFocusTrapActivated = isActivated;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Activate the focus trap
|
|
39
|
+
*/
|
|
40
|
+
activateFocusTrap() {
|
|
41
|
+
this.setIsFocusTrapActivated(true);
|
|
42
|
+
FocusTrapStack.activate(this);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Deactivate the focus trap.
|
|
46
|
+
*/
|
|
47
|
+
deactivateFocusTrap() {
|
|
48
|
+
this.setIsFocusTrapActivated(false);
|
|
49
|
+
FocusTrapStack.deactivate(this);
|
|
50
|
+
this.focusTrapIndex = -1;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Updates the list of focusable elements within the component's shadow root.
|
|
54
|
+
*/
|
|
55
|
+
setFocusableElements() {
|
|
56
|
+
if (!this.shadowRoot)
|
|
57
|
+
return;
|
|
58
|
+
this.focusableElements = findFocusable(this.shadowRoot);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sets the initial focus within the container.
|
|
62
|
+
*
|
|
63
|
+
* @param elementIndexToReceiveFocus - The index of the preferable element to focus.
|
|
64
|
+
*/
|
|
65
|
+
setInitialFocus(elementIndexToReceiveFocus = 0) {
|
|
66
|
+
this.setFocusableElements();
|
|
67
|
+
if (this.focusableElements.length === 0 || !this.focusTrap) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (this.focusableElements[elementIndexToReceiveFocus]) {
|
|
71
|
+
this.focusTrapIndex = elementIndexToReceiveFocus;
|
|
72
|
+
this.focusableElements[elementIndexToReceiveFocus].focus({ preventScroll: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Calculates the next index for the focus trap.
|
|
77
|
+
*
|
|
78
|
+
* @param currentIndex - The current index.
|
|
79
|
+
* @param step - The step to calculate the next index.
|
|
80
|
+
* @returns The next index.
|
|
81
|
+
*/
|
|
82
|
+
calculateNextIndex(currentIndex, step) {
|
|
83
|
+
const { length } = this.focusableElements;
|
|
84
|
+
if (currentIndex === -1) {
|
|
85
|
+
return step > 0 ? 0 : length - 1;
|
|
86
|
+
}
|
|
87
|
+
let nextIndex = currentIndex + step;
|
|
88
|
+
if (this.shouldFocusTrapWrap) {
|
|
89
|
+
if (nextIndex < 0)
|
|
90
|
+
nextIndex = length - 1;
|
|
91
|
+
if (nextIndex >= length)
|
|
92
|
+
nextIndex = 0;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (nextIndex < 0)
|
|
96
|
+
nextIndex = 0;
|
|
97
|
+
if (nextIndex >= length)
|
|
98
|
+
nextIndex = length - 1;
|
|
99
|
+
}
|
|
100
|
+
return nextIndex;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Returns the deepest active element in the shadow DOM.
|
|
104
|
+
*
|
|
105
|
+
* @returns The deepest active element.
|
|
106
|
+
*/
|
|
107
|
+
getDeepActiveElement() {
|
|
108
|
+
var _a;
|
|
109
|
+
let host = document.activeElement || document.body;
|
|
110
|
+
while (host instanceof HTMLElement && ((_a = host.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement)) {
|
|
111
|
+
host = host.shadowRoot.activeElement;
|
|
112
|
+
}
|
|
113
|
+
return host || document.body;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Finds the index of the active element within the focusable elements.
|
|
117
|
+
*
|
|
118
|
+
* @param activeElement - The active element.
|
|
119
|
+
* @returns The index of the active element.
|
|
120
|
+
*/
|
|
121
|
+
findElement(activeElement) {
|
|
122
|
+
return this.focusableElements.findIndex(element => this.isEqualFocusNode(activeElement, element));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Checks if the active element is equal to the given element.
|
|
126
|
+
*
|
|
127
|
+
* @param activeElement - The active element.
|
|
128
|
+
* @param element - The element to compare.
|
|
129
|
+
* @returns True if the active element is equal to the given element.
|
|
130
|
+
*/
|
|
131
|
+
isEqualFocusNode(activeElement, element) {
|
|
132
|
+
if (activeElement.nodeType >= 0) {
|
|
133
|
+
return element.isEqualNode(activeElement) && element === activeElement;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Traps focus within the container.
|
|
139
|
+
*
|
|
140
|
+
* @param direction - The direction of the focus trap.
|
|
141
|
+
* If true, the focus will be trapped in the previous element.
|
|
142
|
+
*/
|
|
143
|
+
trapFocus(event) {
|
|
144
|
+
// calculate the focusable elements
|
|
145
|
+
this.setFocusableElements();
|
|
146
|
+
if (this.focusableElements.length === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const activeElement = this.getDeepActiveElement();
|
|
150
|
+
const activeIndex = this.findElement(activeElement);
|
|
151
|
+
const direction = event.shiftKey;
|
|
152
|
+
if (direction) {
|
|
153
|
+
this.focusTrapIndex = this.calculateNextIndex(activeIndex, -1);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.focusTrapIndex = this.calculateNextIndex(activeIndex, 1);
|
|
157
|
+
}
|
|
158
|
+
const nextElement = this.focusableElements[this.focusTrapIndex];
|
|
159
|
+
if (nextElement.tagName === 'IFRAME') {
|
|
160
|
+
// If the next element is an iframe we should not focus it manually
|
|
161
|
+
// but just let the browser handle it.
|
|
162
|
+
// this only works if there are focusable elements around the iframe!
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (nextElement) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
nextElement.focus();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Traps focus within the container.
|
|
172
|
+
*
|
|
173
|
+
* @param event - The keyboard event.
|
|
174
|
+
*/
|
|
175
|
+
// @ts-ignore - this is a method which will be called in the stack
|
|
176
|
+
handleTabKeydown(event) {
|
|
177
|
+
if (!this.isFocusTrapActivated) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (event.key === 'Tab') {
|
|
181
|
+
this.trapFocus(event);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
__decorate([
|
|
186
|
+
property({ type: Boolean, reflect: true, attribute: 'should-focus-trap-wrap' }),
|
|
187
|
+
__metadata("design:type", Boolean)
|
|
188
|
+
], FocusTrap.prototype, "shouldFocusTrapWrap", void 0);
|
|
189
|
+
return FocusTrap;
|
|
190
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FocusTrapStack manages a stack of active focus traps,
|
|
3
|
+
* ensuring only one focus trap is active at a time.
|
|
4
|
+
*
|
|
5
|
+
* This also makes sure there is only one keydown listener active at a time,
|
|
6
|
+
* which is necessary to handle focus trapping correctly.
|
|
7
|
+
*
|
|
8
|
+
* Handling iFrames is supported, as long as there are focusable elements around the iFrame.
|
|
9
|
+
* Otherwise it will not work as expected.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FocusTrapStack {
|
|
12
|
+
private static stack;
|
|
13
|
+
static get stackArray(): any[];
|
|
14
|
+
static getActiveTrap(): any;
|
|
15
|
+
private static currentKeydownListener;
|
|
16
|
+
private static addKeydownListener;
|
|
17
|
+
private static removeKeydownListener;
|
|
18
|
+
/**
|
|
19
|
+
* Activates a focus trap by adding it to the stack.
|
|
20
|
+
* It deactivates all other traps in the stack to ensure only one trap is active
|
|
21
|
+
*
|
|
22
|
+
* @param trap - The focus trap to activate.
|
|
23
|
+
*/
|
|
24
|
+
static activate(trap: any): void;
|
|
25
|
+
/**
|
|
26
|
+
* Deactivates a focus trap by removing it from the stack.
|
|
27
|
+
* Activates the previous trap in the stack if any.
|
|
28
|
+
*
|
|
29
|
+
* @param trap - The focus trap to deactivate.
|
|
30
|
+
*/
|
|
31
|
+
static deactivate(trap: any): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FocusTrapStack manages a stack of active focus traps,
|
|
3
|
+
* ensuring only one focus trap is active at a time.
|
|
4
|
+
*
|
|
5
|
+
* This also makes sure there is only one keydown listener active at a time,
|
|
6
|
+
* which is necessary to handle focus trapping correctly.
|
|
7
|
+
*
|
|
8
|
+
* Handling iFrames is supported, as long as there are focusable elements around the iFrame.
|
|
9
|
+
* Otherwise it will not work as expected.
|
|
10
|
+
*/
|
|
11
|
+
export class FocusTrapStack {
|
|
12
|
+
static get stackArray() {
|
|
13
|
+
return Array.from(this.stack);
|
|
14
|
+
}
|
|
15
|
+
static getActiveTrap() {
|
|
16
|
+
return this.stackArray.at(-1);
|
|
17
|
+
}
|
|
18
|
+
static addKeydownListener(keydownListener) {
|
|
19
|
+
this.currentKeydownListener = keydownListener;
|
|
20
|
+
document.addEventListener('keydown', keydownListener);
|
|
21
|
+
}
|
|
22
|
+
static removeKeydownListener() {
|
|
23
|
+
if (this.currentKeydownListener) {
|
|
24
|
+
document.removeEventListener('keydown', this.currentKeydownListener);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Activates a focus trap by adding it to the stack.
|
|
29
|
+
* It deactivates all other traps in the stack to ensure only one trap is active
|
|
30
|
+
*
|
|
31
|
+
* @param trap - The focus trap to activate.
|
|
32
|
+
*/
|
|
33
|
+
static activate(trap) {
|
|
34
|
+
// Deactivate all other traps
|
|
35
|
+
this.stackArray.forEach(activeTrap => {
|
|
36
|
+
if (activeTrap !== trap) {
|
|
37
|
+
activeTrap.setIsFocusTrapActivated(false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
this.stack.add(trap);
|
|
41
|
+
// remove the current keydown listener if it exists
|
|
42
|
+
// and add a new one for the current trap
|
|
43
|
+
this.removeKeydownListener();
|
|
44
|
+
this.addKeydownListener(trap.handleTabKeydown.bind(trap));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deactivates a focus trap by removing it from the stack.
|
|
48
|
+
* Activates the previous trap in the stack if any.
|
|
49
|
+
*
|
|
50
|
+
* @param trap - The focus trap to deactivate.
|
|
51
|
+
*/
|
|
52
|
+
static deactivate(trap) {
|
|
53
|
+
if (!this.stack.has(trap)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.stack.delete(trap);
|
|
57
|
+
this.removeKeydownListener();
|
|
58
|
+
// activate the previous trap in the stack if any
|
|
59
|
+
if (this.stack.size > 0) {
|
|
60
|
+
const lastTrap = this.stackArray.pop();
|
|
61
|
+
if (lastTrap) {
|
|
62
|
+
lastTrap.setIsFocusTrapActivated(true);
|
|
63
|
+
this.addKeydownListener(lastTrap.handleTabKeydown.bind(lastTrap));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
FocusTrapStack.stack = new Set();
|
|
69
|
+
FocusTrapStack.currentKeydownListener = null;
|