@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.
- package/dist/browser/index.js +317 -317
- package/dist/browser/index.js.map +3 -3
- package/dist/components/spatialnavigationprovider/spatialnavigationprovider.component.d.ts +62 -20
- package/dist/components/spatialnavigationprovider/spatialnavigationprovider.component.js +100 -30
- package/dist/custom-elements.json +74 -12
- package/dist/react/spatialnavigationprovider/index.d.ts +47 -19
- package/dist/react/spatialnavigationprovider/index.js +47 -19
- package/dist/utils/dom.d.ts +5 -1
- package/dist/utils/dom.js +7 -5
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
* ###
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
80
|
-
*
|
|
81
|
-
* | `data-spatial-left`
|
|
82
|
-
* | `data-spatial-up`
|
|
83
|
-
* | `data-spatial-right`
|
|
84
|
-
* | `data-spatial-down`
|
|
85
|
-
* | `data-spatial-go-back`
|
|
86
|
-
* | `data-spatial-focusable` | N/A
|
|
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
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
* ###
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
81
|
-
*
|
|
82
|
-
* | `data-spatial-left`
|
|
83
|
-
* | `data-spatial-up`
|
|
84
|
-
* | `data-spatial-right`
|
|
85
|
-
* | `data-spatial-down`
|
|
86
|
-
* | `data-spatial-go-back`
|
|
87
|
-
* | `data-spatial-focusable` | N/A
|
|
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
|
*
|
package/dist/utils/dom.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
176
|
-
const
|
|
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) &&
|
|
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') {
|