@momentum-design/components 0.129.43 → 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 +394 -394
- package/dist/browser/index.js.map +4 -4
- package/dist/components/accordionbutton/accordionbutton.component.d.ts +1 -1
- package/dist/components/accordionbutton/accordionbutton.component.js +4 -3
- package/dist/components/buttonsimple/buttonsimple.component.d.ts +1 -1
- package/dist/components/buttonsimple/buttonsimple.component.js +8 -6
- package/dist/components/cardcheckbox/cardcheckbox.component.d.ts +1 -1
- package/dist/components/cardcheckbox/cardcheckbox.component.js +7 -5
- package/dist/components/cardradio/cardradio.component.d.ts +1 -1
- package/dist/components/cardradio/cardradio.component.js +8 -7
- package/dist/components/checkbox/checkbox.component.d.ts +2 -1
- package/dist/components/checkbox/checkbox.component.js +7 -4
- package/dist/components/combobox/combobox.component.d.ts +1 -1
- package/dist/components/combobox/combobox.component.js +11 -11
- package/dist/components/dialog/dialog.component.d.ts +1 -1
- package/dist/components/dialog/dialog.component.js +1 -1
- package/dist/components/input/input.component.d.ts +1 -1
- package/dist/components/input/input.component.js +3 -3
- package/dist/components/list/list.component.d.ts +1 -1
- package/dist/components/listbox/listbox.component.d.ts +1 -1
- package/dist/components/listitem/listitem.component.d.ts +1 -1
- package/dist/components/listitem/listitem.component.js +11 -5
- package/dist/components/menubar/menubar.component.d.ts +2 -2
- package/dist/components/menubar/menubar.component.js +10 -20
- package/dist/components/menuitem/menuitem.component.js +3 -3
- package/dist/components/menupopover/menupopover.component.d.ts +0 -10
- package/dist/components/menupopover/menupopover.component.js +15 -36
- package/dist/components/popover/popover.component.d.ts +1 -1
- package/dist/components/popover/popover.component.js +4 -3
- package/dist/components/radio/radio.component.d.ts +1 -1
- package/dist/components/radio/radio.component.js +8 -7
- package/dist/components/searchfield/searchfield.component.d.ts +3 -0
- package/dist/components/searchfield/searchfield.component.js +18 -5
- package/dist/components/searchpopover/searchpopover.component.js +3 -4
- package/dist/components/select/select.component.d.ts +1 -1
- package/dist/components/select/select.component.js +9 -8
- package/dist/components/slider/slider.component.d.ts +2 -1
- package/dist/components/slider/slider.component.js +4 -3
- package/dist/components/stepperitem/stepperitem.component.d.ts +1 -1
- package/dist/components/stepperitem/stepperitem.component.js +8 -6
- package/dist/components/textarea/textarea.component.d.ts +1 -1
- package/dist/components/textarea/textarea.component.js +5 -4
- package/dist/components/toggle/toggle.component.d.ts +1 -1
- package/dist/components/toggle/toggle.component.js +5 -4
- package/dist/components/virtualizedlist/virtualizedlist.component.d.ts +1 -1
- package/dist/components/virtualizedlist/virtualizedlist.component.js +7 -6
- package/dist/custom-elements.json +1276 -4245
- package/dist/react/checkbox/index.d.ts +1 -0
- package/dist/react/checkbox/index.js +1 -0
- package/dist/utils/dom.d.ts +83 -0
- package/dist/utils/dom.js +164 -0
- package/dist/utils/mixins/KeyToActionMixin.d.ts +69 -0
- package/dist/utils/mixins/KeyToActionMixin.js +92 -0
- package/dist/utils/mixins/ListNavigationMixin.d.ts +2 -1
- package/dist/utils/mixins/ListNavigationMixin.js +10 -33
- 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
|
@@ -11,6 +11,7 @@ import type { Events } from '../../components/checkbox/checkbox.types';
|
|
|
11
11
|
* **Note:** This component internally renders a native checkbox input element with custom styling.
|
|
12
12
|
*
|
|
13
13
|
* ## When to use
|
|
14
|
+
*
|
|
14
15
|
* Use checkboxes when users can select multiple options from a list, or when a single checkbox represents a binary choice (e.g., agreeing to terms).
|
|
15
16
|
*
|
|
16
17
|
* ## Accessibility
|
|
@@ -12,6 +12,7 @@ import { TAG_NAME } from '../../components/checkbox/checkbox.constants';
|
|
|
12
12
|
* **Note:** This component internally renders a native checkbox input element with custom styling.
|
|
13
13
|
*
|
|
14
14
|
* ## When to use
|
|
15
|
+
*
|
|
15
16
|
* Use checkboxes when users can select multiple options from a list, or when a single checkbox represents a binary choice (e.g., agreeing to terms).
|
|
16
17
|
*
|
|
17
18
|
* ## Accessibility
|
package/dist/utils/dom.d.ts
CHANGED
|
@@ -24,3 +24,86 @@ export declare const isAfter: (nodeA: Element, nodeB: Element) => boolean;
|
|
|
24
24
|
* @see [compareDocumentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition)
|
|
25
25
|
*/
|
|
26
26
|
export declare const isBefore: (nodeA: Element, nodeB: Element) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Checks if the element has no client rectangles (not visible in the viewport).
|
|
29
|
+
*
|
|
30
|
+
* @param element - The element to check.
|
|
31
|
+
* @returns True if the element has no client rectangles.
|
|
32
|
+
*/
|
|
33
|
+
export declare const hasNoClientRects: (element: HTMLElement) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Checks if the element has zero dimensions (width and height are both 0).
|
|
36
|
+
*
|
|
37
|
+
* @param element - The element to check.
|
|
38
|
+
* @returns True if the element has zero dimensions.
|
|
39
|
+
*/
|
|
40
|
+
export declare const hasZeroDimensions: (element: HTMLElement) => boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Determines if the element is not visible in the DOM.
|
|
43
|
+
*
|
|
44
|
+
* @param element - The element to check.
|
|
45
|
+
* @returns True if the element is not visible.
|
|
46
|
+
*/
|
|
47
|
+
export declare const isNotVisible: (element: HTMLElement) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Checks if the element has inline styles that make it hidden.
|
|
50
|
+
*
|
|
51
|
+
* @param element - The element to check.
|
|
52
|
+
* @returns True if the element has inline styles that make it hidden.
|
|
53
|
+
*/
|
|
54
|
+
export declare const hasHiddenStyle: (element: HTMLElement) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Checks if the element is hidden by a computed style.
|
|
57
|
+
*
|
|
58
|
+
* @param element - The element to check.
|
|
59
|
+
* @returns True if the element is hidden by a computed style.
|
|
60
|
+
*/
|
|
61
|
+
export declare const hasComputedHidden: (element: HTMLElement) => boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Checks if the element is hidden from the user.
|
|
64
|
+
*
|
|
65
|
+
* @param element - The element to check.
|
|
66
|
+
* @returns True if the element is hidden.
|
|
67
|
+
*/
|
|
68
|
+
export declare const isHidden: (element: HTMLElement) => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Checks if the element is disabled.
|
|
71
|
+
*
|
|
72
|
+
* @param element - The element to check.
|
|
73
|
+
* @returns True if the element is disabled.
|
|
74
|
+
*/
|
|
75
|
+
export declare const isDisabled: (element: any) => any;
|
|
76
|
+
/**
|
|
77
|
+
* Checks if the element is not tabbable.
|
|
78
|
+
*
|
|
79
|
+
* @param element - The element to check.
|
|
80
|
+
* @returns True if the element is not tabbable.
|
|
81
|
+
*/
|
|
82
|
+
export declare const isTabbable: (element: HTMLElement) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Checks if the element is interactive.
|
|
85
|
+
*
|
|
86
|
+
* @param element - The element to check.
|
|
87
|
+
* @returns True if the element is interactive.
|
|
88
|
+
*/
|
|
89
|
+
export declare const isInteractiveElement: (element: HTMLElement) => boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Checks if the element is focusable.
|
|
92
|
+
*
|
|
93
|
+
* @param element - The element to check.
|
|
94
|
+
* @returns True if the element is focusable.
|
|
95
|
+
*/
|
|
96
|
+
export declare const isFocusable: (element: HTMLElement) => boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Recursively finds all focusable elements within the given root and its descendants.
|
|
99
|
+
*
|
|
100
|
+
* Make sure this is performant, as it will be called multiple times.
|
|
101
|
+
*
|
|
102
|
+
* @param root - The root element to search for focusable elements.
|
|
103
|
+
* @returns The list of focusable elements.
|
|
104
|
+
*/
|
|
105
|
+
export declare const findFocusable: (root: ShadowRoot | HTMLElement | null) => HTMLElement[];
|
|
106
|
+
/**
|
|
107
|
+
* Get the active element from the DOM, including shadow DOMs
|
|
108
|
+
*/
|
|
109
|
+
export declare const getDomActiveElement: (root?: Document) => Element | null;
|
package/dist/utils/dom.js
CHANGED
|
@@ -25,3 +25,167 @@ 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
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { Constructor } from './index.types';
|
|
3
|
+
export declare const ACTIONS: {
|
|
4
|
+
/** Action key, e.g., Enter */
|
|
5
|
+
readonly ENTER: "enter";
|
|
6
|
+
/** Back key, e.g., Escape/Back/Cancel */
|
|
7
|
+
readonly ESCAPE: "escape";
|
|
8
|
+
/** Navigation key up */
|
|
9
|
+
readonly UP: "up";
|
|
10
|
+
/** Navigation key down */
|
|
11
|
+
readonly DOWN: "down";
|
|
12
|
+
/** Navigation key left */
|
|
13
|
+
readonly LEFT: "left";
|
|
14
|
+
/** Navigation key right */
|
|
15
|
+
readonly RIGHT: "right";
|
|
16
|
+
/** Space key, some actions and scrolling */
|
|
17
|
+
readonly SPACE: "space";
|
|
18
|
+
/** Tab key */
|
|
19
|
+
readonly TAB: "tab";
|
|
20
|
+
/** Home key */
|
|
21
|
+
readonly HOME: "home";
|
|
22
|
+
/** End key */
|
|
23
|
+
readonly END: "end";
|
|
24
|
+
};
|
|
25
|
+
export type Actions = (typeof ACTIONS)[keyof typeof ACTIONS];
|
|
26
|
+
export interface KeyToActionInterface {
|
|
27
|
+
/**
|
|
28
|
+
* Returns a (abstract) action for the given keyboard event based on the current spatial navigation context
|
|
29
|
+
*
|
|
30
|
+
* @param evt - The keyboard event
|
|
31
|
+
* @param applyWritingDirection - For right-to-left writing direction, left/right actions are swapped if set to true
|
|
32
|
+
* @returns The mapped key or `undefined` if no mapping exists
|
|
33
|
+
*/
|
|
34
|
+
getActionForKeyEvent(evt: KeyboardEvent, applyWritingDirection?: boolean): Actions | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Mixin to provide abstract key mapping for navigation and actions keys.
|
|
38
|
+
*
|
|
39
|
+
* Instead of using hardcoded key names this mixin provides a way to map keys to abstract actions
|
|
40
|
+
* and use different key mappings based on context.
|
|
41
|
+
*
|
|
42
|
+
* All components should implement this mixin if it handles keyboard events for navigation or actions,
|
|
43
|
+
* e.g. buttons, lists, popups, etc.
|
|
44
|
+
*
|
|
45
|
+
* Navigation keys mapped directly:
|
|
46
|
+
* - 'up'
|
|
47
|
+
* - 'down'
|
|
48
|
+
* - 'left'
|
|
49
|
+
* - 'right'
|
|
50
|
+
* - 'tab'
|
|
51
|
+
* - 'home'
|
|
52
|
+
* - 'end'
|
|
53
|
+
*
|
|
54
|
+
* Action keys:
|
|
55
|
+
* - 'action' (Enter key)
|
|
56
|
+
* - 'abort' (Escape/Back key)
|
|
57
|
+
*
|
|
58
|
+
* Special keys:
|
|
59
|
+
* - 'space' (Space key)
|
|
60
|
+
*
|
|
61
|
+
* Space is separated from action keys as it is
|
|
62
|
+
* - not always trigger the same action as the enter key
|
|
63
|
+
* - not every platform has a space key equivalent for example on a TV remote or gamepad
|
|
64
|
+
* - often used for scrolling as well.
|
|
65
|
+
*
|
|
66
|
+
* From the above lists only 'up', 'down', 'left', 'right', 'action' and 'abort' are mandatory to implement,
|
|
67
|
+
* because those are essential for spatial navigation and basic actions and all platforms have equivalents for those.
|
|
68
|
+
*/
|
|
69
|
+
export declare const KeyToActionMixin: <T extends Constructor<LitElement>>(superClass: T) => Constructor<KeyToActionInterface> & T;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { KEYS } from '../keys';
|
|
2
|
+
export const ACTIONS = {
|
|
3
|
+
/** Action key, e.g., Enter */
|
|
4
|
+
ENTER: 'enter',
|
|
5
|
+
/** Back key, e.g., Escape/Back/Cancel */
|
|
6
|
+
ESCAPE: 'escape',
|
|
7
|
+
/** Navigation key up */
|
|
8
|
+
UP: 'up',
|
|
9
|
+
/** Navigation key down */
|
|
10
|
+
DOWN: 'down',
|
|
11
|
+
/** Navigation key left */
|
|
12
|
+
LEFT: 'left',
|
|
13
|
+
/** Navigation key right */
|
|
14
|
+
RIGHT: 'right',
|
|
15
|
+
/** Space key, some actions and scrolling */
|
|
16
|
+
SPACE: 'space',
|
|
17
|
+
/** Tab key */
|
|
18
|
+
TAB: 'tab',
|
|
19
|
+
/** Home key */
|
|
20
|
+
HOME: 'home',
|
|
21
|
+
/** End key */
|
|
22
|
+
END: 'end',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Default key to action mapping if no spatial navigation context is available
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_KEY_TO_ACTION = {
|
|
28
|
+
[KEYS.ARROW_UP]: ACTIONS.UP,
|
|
29
|
+
[KEYS.ARROW_DOWN]: ACTIONS.DOWN,
|
|
30
|
+
[KEYS.ARROW_LEFT]: ACTIONS.LEFT,
|
|
31
|
+
[KEYS.ARROW_RIGHT]: ACTIONS.RIGHT,
|
|
32
|
+
[KEYS.ENTER]: ACTIONS.ENTER,
|
|
33
|
+
[KEYS.SPACE]: ACTIONS.SPACE,
|
|
34
|
+
[KEYS.ESCAPE]: ACTIONS.ESCAPE,
|
|
35
|
+
[KEYS.TAB]: ACTIONS.TAB,
|
|
36
|
+
[KEYS.HOME]: ACTIONS.HOME,
|
|
37
|
+
[KEYS.END]: ACTIONS.END,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Mixin to provide abstract key mapping for navigation and actions keys.
|
|
41
|
+
*
|
|
42
|
+
* Instead of using hardcoded key names this mixin provides a way to map keys to abstract actions
|
|
43
|
+
* and use different key mappings based on context.
|
|
44
|
+
*
|
|
45
|
+
* All components should implement this mixin if it handles keyboard events for navigation or actions,
|
|
46
|
+
* e.g. buttons, lists, popups, etc.
|
|
47
|
+
*
|
|
48
|
+
* Navigation keys mapped directly:
|
|
49
|
+
* - 'up'
|
|
50
|
+
* - 'down'
|
|
51
|
+
* - 'left'
|
|
52
|
+
* - 'right'
|
|
53
|
+
* - 'tab'
|
|
54
|
+
* - 'home'
|
|
55
|
+
* - 'end'
|
|
56
|
+
*
|
|
57
|
+
* Action keys:
|
|
58
|
+
* - 'action' (Enter key)
|
|
59
|
+
* - 'abort' (Escape/Back key)
|
|
60
|
+
*
|
|
61
|
+
* Special keys:
|
|
62
|
+
* - 'space' (Space key)
|
|
63
|
+
*
|
|
64
|
+
* Space is separated from action keys as it is
|
|
65
|
+
* - not always trigger the same action as the enter key
|
|
66
|
+
* - not every platform has a space key equivalent for example on a TV remote or gamepad
|
|
67
|
+
* - often used for scrolling as well.
|
|
68
|
+
*
|
|
69
|
+
* From the above lists only 'up', 'down', 'left', 'right', 'action' and 'abort' are mandatory to implement,
|
|
70
|
+
* because those are essential for spatial navigation and basic actions and all platforms have equivalents for those.
|
|
71
|
+
*/
|
|
72
|
+
export const KeyToActionMixin = (superClass) => {
|
|
73
|
+
class InnerMixinClass extends superClass {
|
|
74
|
+
/** @see KeyToActionInterface.getMappedKeyFromEvent */
|
|
75
|
+
getActionForKeyEvent(evt, applyWritingDirection = false) {
|
|
76
|
+
const mapping = DEFAULT_KEY_TO_ACTION;
|
|
77
|
+
const key = mapping[evt.key];
|
|
78
|
+
if (applyWritingDirection) {
|
|
79
|
+
const isRtl = window.getComputedStyle(this).direction === 'rtl';
|
|
80
|
+
if (!isRtl)
|
|
81
|
+
return key;
|
|
82
|
+
if (key === ACTIONS.LEFT)
|
|
83
|
+
return ACTIONS.RIGHT;
|
|
84
|
+
if (key === ACTIONS.RIGHT)
|
|
85
|
+
return ACTIONS.LEFT;
|
|
86
|
+
}
|
|
87
|
+
return key;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Cast return type to your mixin's interface intersected with the superClass type
|
|
91
|
+
return InnerMixinClass;
|
|
92
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Component } from '../../models';
|
|
2
2
|
import type { BaseArray } from '../virtualIndexArray';
|
|
3
3
|
import type { Constructor } from './index.types';
|
|
4
|
+
import { KeyToActionInterface } from './KeyToActionMixin';
|
|
4
5
|
export declare abstract class ListNavigationMixinInterface {
|
|
5
6
|
protected loop: 'true' | 'false';
|
|
6
7
|
protected propagateAllKeyEvents: boolean;
|
|
@@ -29,4 +30,4 @@ export declare abstract class ListNavigationMixinInterface {
|
|
|
29
30
|
* ```
|
|
30
31
|
* @param superClass - The class to extend with the mixin.
|
|
31
32
|
*/
|
|
32
|
-
export declare const ListNavigationMixin: <T extends Constructor<Component>>(superClass: T) => Constructor<Component & ListNavigationMixinInterface> & T;
|
|
33
|
+
export declare const ListNavigationMixin: <T extends Constructor<Component>>(superClass: T) => Constructor<Component & ListNavigationMixinInterface & KeyToActionInterface> & T;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ACTIONS, KeyToActionMixin } from './KeyToActionMixin';
|
|
2
2
|
/**
|
|
3
3
|
* This mixin extends the passed class with list like navigation capabilities.
|
|
4
4
|
*
|
|
@@ -18,7 +18,7 @@ import { KEYS } from '../keys';
|
|
|
18
18
|
* @param superClass - The class to extend with the mixin.
|
|
19
19
|
*/
|
|
20
20
|
export const ListNavigationMixin = (superClass) => {
|
|
21
|
-
class InnerMixinClass extends superClass {
|
|
21
|
+
class InnerMixinClass extends KeyToActionMixin(superClass) {
|
|
22
22
|
constructor(...rest) {
|
|
23
23
|
super(...rest);
|
|
24
24
|
/**
|
|
@@ -92,10 +92,9 @@ export const ListNavigationMixin = (superClass) => {
|
|
|
92
92
|
* @internal
|
|
93
93
|
*/
|
|
94
94
|
handleNavigationKeyDown(event) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if (!keysToHandle.has(targetKey)) {
|
|
95
|
+
const action = this.getActionForKeyEvent(event, true);
|
|
96
|
+
const actionsToHandle = new Set([ACTIONS.DOWN, ACTIONS.UP, ACTIONS.HOME, ACTIONS.END]);
|
|
97
|
+
if (!action || !actionsToHandle.has(action)) {
|
|
99
98
|
return;
|
|
100
99
|
}
|
|
101
100
|
const target = event.target;
|
|
@@ -103,25 +102,25 @@ export const ListNavigationMixin = (superClass) => {
|
|
|
103
102
|
if (currentIndex === -1)
|
|
104
103
|
return;
|
|
105
104
|
this.resetTabIndexes(currentIndex);
|
|
106
|
-
switch (
|
|
107
|
-
case
|
|
105
|
+
switch (action) {
|
|
106
|
+
case ACTIONS.HOME: {
|
|
108
107
|
// Move focus to the first item
|
|
109
108
|
this.resetTabIndexAndSetFocus(0, currentIndex);
|
|
110
109
|
break;
|
|
111
110
|
}
|
|
112
|
-
case
|
|
111
|
+
case ACTIONS.END: {
|
|
113
112
|
// Move focus to the last item
|
|
114
113
|
this.resetTabIndexAndSetFocus(this.navItems.length - 1, currentIndex);
|
|
115
114
|
break;
|
|
116
115
|
}
|
|
117
|
-
case
|
|
116
|
+
case ACTIONS.DOWN: {
|
|
118
117
|
// Move focus to the next item
|
|
119
118
|
const eolIndex = this.shouldLoop() ? 0 : currentIndex;
|
|
120
119
|
const newIndex = currentIndex + 1 === this.navItems.length ? eolIndex : currentIndex + 1;
|
|
121
120
|
this.resetTabIndexAndSetFocus(newIndex, currentIndex);
|
|
122
121
|
break;
|
|
123
122
|
}
|
|
124
|
-
case
|
|
123
|
+
case ACTIONS.UP: {
|
|
125
124
|
// Move focus to the prev item
|
|
126
125
|
const eolIndex = this.shouldLoop() ? this.navItems.length - 1 : currentIndex;
|
|
127
126
|
const newIndex = currentIndex - 1 === -1 ? eolIndex : currentIndex - 1;
|
|
@@ -204,28 +203,6 @@ export const ListNavigationMixin = (superClass) => {
|
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
}
|
|
207
|
-
/**
|
|
208
|
-
* Resolves the key pressed by the user based on the direction of the layout.
|
|
209
|
-
* This method is used to handle keyboard navigation in a right-to-left (RTL) layout.
|
|
210
|
-
* It checks if the layout is RTL and adjusts the arrow keys accordingly.
|
|
211
|
-
* For example, in RTL, the left arrow key behaves like the right arrow key and vice versa.
|
|
212
|
-
*
|
|
213
|
-
* @param key - The key pressed by the user.
|
|
214
|
-
* @param isRtl - A boolean indicating if the layout is right-to-left (RTL).
|
|
215
|
-
* @returns - The resolved key based on the direction.
|
|
216
|
-
*/
|
|
217
|
-
resolveDirectionKey(key, isRtl) {
|
|
218
|
-
if (!isRtl)
|
|
219
|
-
return key;
|
|
220
|
-
switch (key) {
|
|
221
|
-
case KEYS.ARROW_LEFT:
|
|
222
|
-
return KEYS.ARROW_RIGHT;
|
|
223
|
-
case KEYS.ARROW_RIGHT:
|
|
224
|
-
return KEYS.ARROW_LEFT;
|
|
225
|
-
default:
|
|
226
|
-
return key;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
206
|
shouldLoop() {
|
|
230
207
|
return this.loop !== 'false';
|
|
231
208
|
}
|
|
@@ -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;
|