@momentum-design/components 0.134.8 → 0.134.10

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.
@@ -2,24 +2,43 @@ import { type EventName } from '@lit/react';
2
2
  import Component from '../../components/spatialnavigationprovider';
3
3
  import type { Events } from '../../components/spatialnavigationprovider/spatialnavigationprovider.types';
4
4
  /**
5
- * This component manages focus using spatial navigation and provides context for child components.
6
- *
7
- * Place it at the root of the application.
5
+ * Spatial navigation focus manager
8
6
  *
9
7
  * [Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among
10
8
  * elements on a 2D plane, common on TVs and game consoles with remotes or gamepads.
11
9
  *
10
+ * It should have only one instance and it should placed at the root of the application.
11
+ *
12
12
  * ## Focus management
13
13
  *
14
14
  * The provider listens to keyboard events and moves focus among elements based on arrow key input.
15
15
  * You can influence or override this behavior.
16
16
  *
17
- * Note: The algorithm is distance-based, so the UI should be designed so focusable elements are
18
- * predictably reachable. Relative element positions should remain stable; responsive layouts can
19
- * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
20
- * behavior in Storybook when resizing. See the "Limitations" section.
17
+ * ### Steps
18
+ *
19
+ * Spatial navigation goes trough the following steps after each keydown:
20
+ *
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
+ * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
25
+ * prevented.
26
+ * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
27
+ * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
28
+ * the key event itself. If `navbeforeprocess` event is prevented, stop here.
29
+ * - If the component did not handle `keydown`, it calculate the next focusable item
30
+ * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
31
+ * - Otherwise calculate the next focused item based on the direction and distances.
32
+ * - If there is no next item, it emits `navnotarget` event
33
+ * - Otherwise emit `navbeforefocus`,
34
+ * - If this event prevented, nothing happens
35
+ * - Otherwise the focus moves to the next element
21
36
  *
22
- * ### Automatic
37
+ * ### Determine next focus
38
+ *
39
+ * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
40
+ *
41
+ * #### Calculated focus
23
42
  *
24
43
  * By default, the next focus target is computed from element positions:
25
44
  *
@@ -33,9 +52,17 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
33
52
  * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
34
53
  * (e.g., `tabindex="-1"`).
35
54
  *
36
- * ### Overwrite next element
55
+ * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
56
+ * are focusable.
57
+ *
58
+ * Note: The algorithm is distance-based, so the UI should be designed to focusable elements are
59
+ * predictably reachable. Relative element positions should remain stable; responsive layouts can
60
+ * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
61
+ * behavior in Storybook when resizing. See the "Limitations" section.
62
+ *
63
+ * #### Overwrite next element
37
64
  *
38
- * Override automatic navigation by adding one of these attributes to a focusable element:
65
+ * Override calculated navigation by adding one of these attributes to a focusable element:
39
66
  *
40
67
  * - `data-spatial-up`
41
68
  * - `data-spatial-down`
@@ -44,7 +71,7 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
44
71
  *
45
72
  * Each attribute value must be the id of the element to focus when the corresponding key is pressed.
46
73
  *
47
- * ### Element internal navigation
74
+ * #### Element internal navigation
48
75
  *
49
76
  * Complex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves
50
77
  * focus internally on Down until the last item, after which Down should fall back to provider navigation.
@@ -76,14 +103,15 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
76
103
  *
77
104
  * Supported data attributes:
78
105
  *
79
- * | Attribute | Value | Default | Description |
80
- * |------------------------|-------------|---------|-------------------------------------------------------------------------------------|
81
- * | `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |
82
- * | `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |
83
- * | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
84
- * | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
85
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
86
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
106
+ * | Attribute | Value | Default | Description |
107
+ * |--------------------------|---------------------------|---------|-------------------------------------------------------------------------------|
108
+ * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
109
+ * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
110
+ * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
111
+ * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
112
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
113
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
114
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
87
115
  *
88
116
  * ## Event emitting order
89
117
  *
@@ -3,24 +3,43 @@ import { createComponent } from '@lit/react';
3
3
  import Component from '../../components/spatialnavigationprovider';
4
4
  import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavigationprovider.constants';
5
5
  /**
6
- * This component manages focus using spatial navigation and provides context for child components.
7
- *
8
- * Place it at the root of the application.
6
+ * Spatial navigation focus manager
9
7
  *
10
8
  * [Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among
11
9
  * elements on a 2D plane, common on TVs and game consoles with remotes or gamepads.
12
10
  *
11
+ * It should have only one instance and it should placed at the root of the application.
12
+ *
13
13
  * ## Focus management
14
14
  *
15
15
  * The provider listens to keyboard events and moves focus among elements based on arrow key input.
16
16
  * You can influence or override this behavior.
17
17
  *
18
- * Note: The algorithm is distance-based, so the UI should be designed so focusable elements are
19
- * predictably reachable. Relative element positions should remain stable; responsive layouts can
20
- * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
21
- * behavior in Storybook when resizing. See the "Limitations" section.
18
+ * ### Steps
19
+ *
20
+ * Spatial navigation goes trough the following steps after each keydown:
21
+ *
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
+ * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
26
+ * prevented.
27
+ * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
28
+ * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
29
+ * the key event itself. If `navbeforeprocess` event is prevented, stop here.
30
+ * - If the component did not handle `keydown`, it calculate the next focusable item
31
+ * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
32
+ * - Otherwise calculate the next focused item based on the direction and distances.
33
+ * - If there is no next item, it emits `navnotarget` event
34
+ * - Otherwise emit `navbeforefocus`,
35
+ * - If this event prevented, nothing happens
36
+ * - Otherwise the focus moves to the next element
22
37
  *
23
- * ### Automatic
38
+ * ### Determine next focus
39
+ *
40
+ * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
41
+ *
42
+ * #### Calculated focus
24
43
  *
25
44
  * By default, the next focus target is computed from element positions:
26
45
  *
@@ -34,9 +53,17 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
34
53
  * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
35
54
  * (e.g., `tabindex="-1"`).
36
55
  *
37
- * ### Overwrite next element
56
+ * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
57
+ * are focusable.
58
+ *
59
+ * Note: The algorithm is distance-based, so the UI should be designed to focusable elements are
60
+ * predictably reachable. Relative element positions should remain stable; responsive layouts can
61
+ * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
62
+ * behavior in Storybook when resizing. See the "Limitations" section.
63
+ *
64
+ * #### Overwrite next element
38
65
  *
39
- * Override automatic navigation by adding one of these attributes to a focusable element:
66
+ * Override calculated navigation by adding one of these attributes to a focusable element:
40
67
  *
41
68
  * - `data-spatial-up`
42
69
  * - `data-spatial-down`
@@ -45,7 +72,7 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
45
72
  *
46
73
  * Each attribute value must be the id of the element to focus when the corresponding key is pressed.
47
74
  *
48
- * ### Element internal navigation
75
+ * #### Element internal navigation
49
76
  *
50
77
  * Complex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves
51
78
  * focus internally on Down until the last item, after which Down should fall back to provider navigation.
@@ -77,14 +104,15 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
77
104
  *
78
105
  * Supported data attributes:
79
106
  *
80
- * | Attribute | Value | Default | Description |
81
- * |------------------------|-------------|---------|-------------------------------------------------------------------------------------|
82
- * | `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |
83
- * | `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |
84
- * | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
85
- * | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
86
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
87
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
107
+ * | Attribute | Value | Default | Description |
108
+ * |--------------------------|---------------------------|---------|-------------------------------------------------------------------------------|
109
+ * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
110
+ * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
111
+ * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
112
+ * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
113
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
114
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
115
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
88
116
  *
89
117
  * ## Event emitting order
90
118
  *
@@ -3,10 +3,14 @@ import type { OverflowMixinInterface } from './mixins/OverflowMixin';
3
3
  * Options for finding focusable elements.
4
4
  */
5
5
  type FindFocusableOptions = {
6
- /** Elements to exclude from the search. */
6
+ /** Elements to include (and its subtree) in the search. */
7
+ includeElements?: HTMLElement[];
8
+ /** Elements to exclude (and its subtree) from the search. */
7
9
  excludedElements?: HTMLElement[];
8
10
  /** Selectors to include in the search. */
9
11
  includeSelectors?: string[];
12
+ /** Selectors to exclude from the search. */
13
+ excludeSelectors?: string[];
10
14
  /**
11
15
  * When true, elements with `tabindex="-1"` and their subtrees are excluded from the search.
12
16
  * This supports composite widget patterns (e.g., roving tabindex in lists) where
package/dist/utils/dom.js CHANGED
@@ -166,16 +166,18 @@ export const isFocusable = (element) => !isDisabled(element) && isTabbable(eleme
166
166
  * @returns The list of focusable elements.
167
167
  */
168
168
  export const findFocusable = (root, options = {}) => {
169
- var _a, _b, _c;
169
+ var _a, _b, _c, _d, _e;
170
170
  if (!root) {
171
171
  return [];
172
172
  }
173
173
  const excludesSet = new Set((_a = options === null || options === void 0 ? void 0 : options.excludedElements) !== null && _a !== void 0 ? _a : []);
174
174
  const includeSelectors = (_b = options === null || options === void 0 ? void 0 : options.includeSelectors) !== null && _b !== void 0 ? _b : [];
175
- const stopAtNonTabbable = (_c = options === null || options === void 0 ? void 0 : options.stopAtNonTabbable) !== null && _c !== void 0 ? _c : false;
176
- const matches = new Set();
175
+ const excludeSelectors = (_c = options === null || options === void 0 ? void 0 : options.excludeSelectors) !== null && _c !== void 0 ? _c : [];
176
+ const stopAtNonTabbable = (_d = options === null || options === void 0 ? void 0 : options.stopAtNonTabbable) !== null && _d !== void 0 ? _d : false;
177
+ const matches = new Set((_e = options.includeElements) !== null && _e !== void 0 ? _e : []);
177
178
  const focusableCheck = (element) => {
178
- if (!(element instanceof HTMLSlotElement) && (isHidden(element) || isDisabled(element))) {
179
+ if (!(element instanceof HTMLSlotElement) &&
180
+ (isHidden(element) || isDisabled(element) || isMatchAny(element, excludeSelectors))) {
179
181
  return 'stop';
180
182
  }
181
183
  if (stopAtNonTabbable && !(element instanceof HTMLSlotElement) && element.getAttribute('tabindex') === '-1') {
@@ -194,7 +196,7 @@ export const findFocusable = (root, options = {}) => {
194
196
  : 'continue';
195
197
  };
196
198
  const finder = (root) => {
197
- if (excludesSet.has(root)) {
199
+ if (excludesSet.has(root) || (root instanceof HTMLElement && isMatchAny(root, excludeSelectors))) {
198
200
  return;
199
201
  }
200
202
  if (root instanceof HTMLElement && focusableCheck(root) === 'focusable') {
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.8",
4
+ "version": "0.134.10",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=8.0.0"