@momentum-design/components 0.134.16 → 0.134.17

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.
@@ -16,30 +16,30 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
16
16
  *
17
17
  * ### Steps
18
18
  *
19
- * Spatial navigation goes trough the following steps after each keydown:
19
+ * Spatial navigation goes through the following steps after each keydown:
20
20
  *
21
21
  * 1. Handle `keydown` in the capture phase.
22
- * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
23
- * provider own `keydown` handler (see step 3).
24
- * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not
25
- * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the
22
+ * - When the active element has a `data-spatial-{direction}` attribute, then prevent all component navigation and call the
23
+ * provider's own `keydown` handler (see step 3).
24
+ * - When the active element's parent is scrollable and it is not fully visible in the given direction, and it does not
25
+ * have a `data-spatial-noscroll` attribute, prevent all navigation and scroll in the given direction half-size of the
26
26
  * scroll view.
27
27
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
28
28
  * prevented.
29
29
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
30
- * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
30
+ * - If a key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
31
31
  * the key event itself. If `navbeforeprocess` event is prevented, stop here.
32
- * - If the component did not handle `keydown`, it calculate the next focusable item
33
- * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
32
+ * - If the component did not handle `keydown`, it calculates the next focusable item
33
+ * - if the active element has a `data-spatial-{direction}` attribute, it will try to focus the element with the id.
34
34
  * - Otherwise calculate the next focused item based on the direction and distances.
35
35
  * - If there is no next item, it emits `navnotarget` event
36
36
  * - Otherwise emit `navbeforefocus`,
37
- * - If this event prevented, nothing happens
37
+ * - If this event is prevented, nothing happens
38
38
  * - Otherwise the focus moves to the next element
39
39
  *
40
40
  * ### Determine next focus
41
41
  *
42
- * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
42
+ * The provider uses multiple ways to determine the next focused element. The order defined in the "Steps" section.
43
43
  *
44
44
  * #### Calculated focus
45
45
  *
@@ -50,9 +50,9 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
50
50
  * 3. Compute distances from the current element to candidates using the W3C "find the shortest
51
51
  * distance" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance
52
52
  * 4. If no candidates are found, repeat from step 1, skipping areas already checked.
53
- * 5. Focus the closest candidate.
53
+ * 5. Focus on the closest candidate.
54
54
  *
55
- * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
55
+ * Elements with `data-spatial-focusable` are treated as focusable even if they do otherwise not be
56
56
  * (e.g., `tabindex="-1"`).
57
57
  *
58
58
  * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
@@ -63,7 +63,7 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
63
63
  * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
64
64
  * behavior in Storybook when resizing. See the "Limitations" section.
65
65
  *
66
- * #### Overwrite next element
66
+ * #### Overwrite the next element
67
67
  *
68
68
  * Override calculated navigation by adding one of these attributes to a focusable element:
69
69
  *
@@ -106,17 +106,17 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
106
106
  *
107
107
  * Supported data attributes:
108
108
  *
109
- * | Attribute | Value | Default | Description |
110
- * |------------------------------|-------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------|
111
- * | `data-spatial-left` | empty string / id / selector | N/A | Prevent native navigation in Left direction and focus element if exists |
112
- * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction and focus element if exists |
113
- * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in Right direction and focus element if exists |
114
- * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction and focus element if exists |
115
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
116
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
117
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
118
- * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
119
- * | `data-spatial-scroll-parent` | N/A | N/A | When the focusable item in not a direct child of the scrollable aria use this attribute to mark scrollable area element | |
109
+ * | Attribute | Value | Default | Description |
110
+ * |------------------------------|-------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------|
111
+ * | `data-spatial-left` | empty string / id / selector | N/A | Prevent native navigation in the Left direction, focus it if it's focusable otherwise limit the search in the selected container. |
112
+ * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction, focus it if it's focusable otherwise limit the search in the selected container. |
113
+ * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in the Right direction, focus it if it's focusable otherwise limit the search in the selected container. |
114
+ * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction, focus it if it's focusable otherwise limit the search in the selected container. |
115
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
116
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
117
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
118
+ * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
119
+ * | `data-spatial-scroll-parent` | N/A | N/A | When the focusable item in not a direct child of the scrollable aria use this attribute to mark scrollable area element |
120
120
  *
121
121
  * ## Event emitting order
122
122
  *
@@ -155,7 +155,7 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
155
155
  *
156
156
  * ## Platform specific behaviors
157
157
  *
158
- * Consider remote/gamepad constraints. Often focus alone is not enough and users press Enter to "enter" an interactive mode:
158
+ * Consider remote/gamepad constraints. Often focus alone is not enough, and users press Enter to "enter" an interactive mode:
159
159
  * - Select: Enter opens options rather than arrow keys opening a popover.
160
160
  * - Text inputs: see the next section.
161
161
  * - Slider: Enter to start adjusting, arrow keys to change value, Enter/Escape to stop.
@@ -183,7 +183,7 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
183
183
  * - Escape - Escape
184
184
  *
185
185
  * With wrapper: wraps the component in a 3x3 grid with surrounding buttons for testing.
186
- * Without wrapper: renders the component alone.
186
+ * Without a wrapper: renders the component alone.
187
187
  *
188
188
  * ### Visual debugger
189
189
  *
@@ -17,30 +17,30 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
17
17
  *
18
18
  * ### Steps
19
19
  *
20
- * Spatial navigation goes trough the following steps after each keydown:
20
+ * Spatial navigation goes through the following steps after each keydown:
21
21
  *
22
22
  * 1. Handle `keydown` in the capture phase.
23
- * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
24
- * provider own `keydown` handler (see step 3).
25
- * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not
26
- * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the
23
+ * - When the active element has a `data-spatial-{direction}` attribute, then prevent all component navigation and call the
24
+ * provider's own `keydown` handler (see step 3).
25
+ * - When the active element's parent is scrollable and it is not fully visible in the given direction, and it does not
26
+ * have a `data-spatial-noscroll` attribute, prevent all navigation and scroll in the given direction half-size of the
27
27
  * scroll view.
28
28
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
29
29
  * prevented.
30
30
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
31
- * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
31
+ * - If a key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
32
32
  * the key event itself. If `navbeforeprocess` event is prevented, stop here.
33
- * - If the component did not handle `keydown`, it calculate the next focusable item
34
- * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
33
+ * - If the component did not handle `keydown`, it calculates the next focusable item
34
+ * - if the active element has a `data-spatial-{direction}` attribute, it will try to focus the element with the id.
35
35
  * - Otherwise calculate the next focused item based on the direction and distances.
36
36
  * - If there is no next item, it emits `navnotarget` event
37
37
  * - Otherwise emit `navbeforefocus`,
38
- * - If this event prevented, nothing happens
38
+ * - If this event is prevented, nothing happens
39
39
  * - Otherwise the focus moves to the next element
40
40
  *
41
41
  * ### Determine next focus
42
42
  *
43
- * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
43
+ * The provider uses multiple ways to determine the next focused element. The order defined in the "Steps" section.
44
44
  *
45
45
  * #### Calculated focus
46
46
  *
@@ -51,9 +51,9 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
51
51
  * 3. Compute distances from the current element to candidates using the W3C "find the shortest
52
52
  * distance" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance
53
53
  * 4. If no candidates are found, repeat from step 1, skipping areas already checked.
54
- * 5. Focus the closest candidate.
54
+ * 5. Focus on the closest candidate.
55
55
  *
56
- * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
56
+ * Elements with `data-spatial-focusable` are treated as focusable even if they do otherwise not be
57
57
  * (e.g., `tabindex="-1"`).
58
58
  *
59
59
  * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
@@ -64,7 +64,7 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
64
64
  * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
65
65
  * behavior in Storybook when resizing. See the "Limitations" section.
66
66
  *
67
- * #### Overwrite next element
67
+ * #### Overwrite the next element
68
68
  *
69
69
  * Override calculated navigation by adding one of these attributes to a focusable element:
70
70
  *
@@ -107,17 +107,17 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
107
107
  *
108
108
  * Supported data attributes:
109
109
  *
110
- * | Attribute | Value | Default | Description |
111
- * |------------------------------|-------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------|
112
- * | `data-spatial-left` | empty string / id / selector | N/A | Prevent native navigation in Left direction and focus element if exists |
113
- * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction and focus element if exists |
114
- * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in Right direction and focus element if exists |
115
- * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction and focus element if exists |
116
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
117
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
118
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
119
- * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
120
- * | `data-spatial-scroll-parent` | N/A | N/A | When the focusable item in not a direct child of the scrollable aria use this attribute to mark scrollable area element | |
110
+ * | Attribute | Value | Default | Description |
111
+ * |------------------------------|-------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------|
112
+ * | `data-spatial-left` | empty string / id / selector | N/A | Prevent native navigation in the Left direction, focus it if it's focusable otherwise limit the search in the selected container. |
113
+ * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction, focus it if it's focusable otherwise limit the search in the selected container. |
114
+ * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in the Right direction, focus it if it's focusable otherwise limit the search in the selected container. |
115
+ * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction, focus it if it's focusable otherwise limit the search in the selected container. |
116
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
117
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
118
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
119
+ * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
120
+ * | `data-spatial-scroll-parent` | N/A | N/A | When the focusable item in not a direct child of the scrollable aria use this attribute to mark scrollable area element |
121
121
  *
122
122
  * ## Event emitting order
123
123
  *
@@ -156,7 +156,7 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
156
156
  *
157
157
  * ## Platform specific behaviors
158
158
  *
159
- * Consider remote/gamepad constraints. Often focus alone is not enough and users press Enter to "enter" an interactive mode:
159
+ * Consider remote/gamepad constraints. Often focus alone is not enough, and users press Enter to "enter" an interactive mode:
160
160
  * - Select: Enter opens options rather than arrow keys opening a popover.
161
161
  * - Text inputs: see the next section.
162
162
  * - Slider: Enter to start adjusting, arrow keys to change value, Enter/Escape to stop.
@@ -184,7 +184,7 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
184
184
  * - Escape - Escape
185
185
  *
186
186
  * With wrapper: wraps the component in a 3x3 grid with surrounding buttons for testing.
187
- * Without wrapper: renders the component alone.
187
+ * Without a wrapper: renders the component alone.
188
188
  *
189
189
  * ### Visual debugger
190
190
  *
@@ -1,10 +1,9 @@
1
1
  import type { OverflowMixinInterface } from './mixins/OverflowMixin';
2
+ type FindFocusableCommands = 'focusable' | 'continue' | 'stop';
2
3
  /**
3
4
  * Options for finding focusable elements.
4
5
  */
5
6
  type FindFocusableOptions = {
6
- /** Elements to include (and its subtree) in the search. */
7
- includeElements?: HTMLElement[];
8
7
  /** Elements to exclude (and its subtree) from the search. */
9
8
  excludedElements?: HTMLElement[];
10
9
  /** Selectors to include in the search. */
@@ -17,6 +16,14 @@ type FindFocusableOptions = {
17
16
  * inactive items and their children should not be reachable via Tab navigation.
18
17
  */
19
18
  stopAtNonTabbable?: boolean;
19
+ /**
20
+ * When it is provided, it is executed after the default checked run.
21
+ * I get the result from the default checked run and can modify it.
22
+ *
23
+ * @param element - The element to check.
24
+ * @param result - The result of the default checked run.
25
+ */
26
+ customFocusableCheck?: (element: HTMLElement, result: FindFocusableCommands) => FindFocusableCommands;
20
27
  };
21
28
  /**
22
29
  * nodeB precedes nodeA in either a pre-order depth-first traversal of a tree containing both
package/dist/utils/dom.js CHANGED
@@ -173,7 +173,7 @@ export const isFocusable = (element) => !isDisabled(element) && isTabbable(eleme
173
173
  * @returns The list of focusable elements.
174
174
  */
175
175
  export const findFocusable = (root, options = {}) => {
176
- var _a, _b, _c, _d, _e;
176
+ var _a, _b, _c, _d;
177
177
  if (!root) {
178
178
  return [];
179
179
  }
@@ -181,12 +181,14 @@ export const findFocusable = (root, options = {}) => {
181
181
  const includeSelectors = (_b = options === null || options === void 0 ? void 0 : options.includeSelectors) !== null && _b !== void 0 ? _b : [];
182
182
  const excludeSelectors = (_c = options === null || options === void 0 ? void 0 : options.excludeSelectors) !== null && _c !== void 0 ? _c : [];
183
183
  const stopAtNonTabbable = (_d = options === null || options === void 0 ? void 0 : options.stopAtNonTabbable) !== null && _d !== void 0 ? _d : false;
184
- const matches = new Set((_e = options.includeElements) !== null && _e !== void 0 ? _e : []);
185
- const focusableCheck = (element) => {
186
- if (!(element instanceof HTMLSlotElement) &&
187
- (isHidden(element) || isDisabled(element) || isMatchAny(element, excludeSelectors))) {
184
+ const matches = new Set([]);
185
+ const internalFocusableCheck = (element) => {
186
+ // Excluded
187
+ if (excludesSet.has(root) || isMatchAny(element, excludeSelectors))
188
+ return 'stop';
189
+ // Unreachable
190
+ if (isHidden(element) || isDisabled(element))
188
191
  return 'stop';
189
- }
190
192
  if (stopAtNonTabbable && !(element instanceof HTMLSlotElement) && element.getAttribute('tabindex') === '-1') {
191
193
  // AI-Assisted
192
194
  // Do not stop traversal for elements inside a shadow root with delegatesFocus: true.
@@ -202,49 +204,30 @@ export const findFocusable = (root, options = {}) => {
202
204
  ? 'focusable'
203
205
  : 'continue';
204
206
  };
207
+ const focusableCheck = options.customFocusableCheck
208
+ ? (el) => options.customFocusableCheck(el, internalFocusableCheck(el))
209
+ : internalFocusableCheck;
205
210
  const finder = (root) => {
206
- if (excludesSet.has(root) || (root instanceof HTMLElement && isMatchAny(root, excludeSelectors))) {
207
- return;
208
- }
209
- if (root instanceof HTMLElement && focusableCheck(root) === 'focusable') {
210
- matches.add(root);
211
+ if (root instanceof HTMLElement) {
212
+ const isFocusableResult = focusableCheck(root);
213
+ if (isFocusableResult === 'focusable') {
214
+ matches.add(root);
215
+ }
216
+ if (isFocusableResult === 'stop') {
217
+ return;
218
+ }
211
219
  }
212
220
  let children = [];
213
- if (root instanceof HTMLElement && root.shadowRoot) {
221
+ if (root instanceof HTMLSlotElement) {
222
+ children = root.assignedElements({ flatten: true });
223
+ }
224
+ else if (root instanceof HTMLElement && root.shadowRoot) {
214
225
  children = Array.from(root.shadowRoot.children);
215
226
  }
216
227
  else if (root.children.length) {
217
228
  children = Array.from(root.children);
218
229
  }
219
- children.forEach((child) => {
220
- const element = child;
221
- const isFocusableResult = focusableCheck(element);
222
- if (isFocusableResult === 'focusable') {
223
- matches.add(element);
224
- }
225
- if (isFocusableResult === 'stop') {
226
- return;
227
- }
228
- if (element.shadowRoot) {
229
- finder(element.shadowRoot);
230
- }
231
- else if (element.tagName === 'SLOT') {
232
- const assignedNodes = element.assignedElements({ flatten: true });
233
- assignedNodes.forEach(node => {
234
- if (node instanceof HTMLElement) {
235
- // When stopAtNonTabbable is enabled, skip non-tabbable slotted elements and their
236
- // subtrees to support composite widget patterns (e.g., roving tabindex in lists)
237
- if (stopAtNonTabbable && !(node instanceof HTMLSlotElement) && node.getAttribute('tabindex') === '-1') {
238
- return;
239
- }
240
- finder(node);
241
- }
242
- });
243
- }
244
- else {
245
- finder(element);
246
- }
247
- });
230
+ children.forEach(finder);
248
231
  };
249
232
  finder(root);
250
233
  return [...matches];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@momentum-design/components",
3
3
  "packageManager": "yarn@3.2.4",
4
- "version": "0.134.16",
4
+ "version": "0.134.17",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=8.0.0"