@momentum-design/components 0.134.7 → 0.134.9
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 +5 -1
- package/dist/components/spatialnavigationprovider/spatialnavigationprovider.component.js +6 -1
- package/dist/components/tooltip/tooltip.component.d.ts +20 -3
- package/dist/components/tooltip/tooltip.component.js +53 -22
- package/dist/custom-elements.json +34 -13
- package/dist/react/spatialnavigationprovider/index.d.ts +5 -1
- package/dist/react/spatialnavigationprovider/index.js +5 -1
- package/dist/utils/dom.d.ts +5 -1
- package/dist/utils/dom.js +7 -5
- package/package.json +1 -1
|
@@ -32,6 +32,9 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
|
|
|
32
32
|
* Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
|
|
33
33
|
* (e.g., `tabindex="-1"`).
|
|
34
34
|
*
|
|
35
|
+
* Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
|
|
36
|
+
* are focusable.
|
|
37
|
+
*
|
|
35
38
|
* ### Overwrite next element
|
|
36
39
|
*
|
|
37
40
|
* Override automatic navigation by adding one of these attributes to a focusable element:
|
|
@@ -82,7 +85,8 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
|
|
|
82
85
|
* | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
|
|
83
86
|
* | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
|
|
84
87
|
* | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
|
|
85
|
-
* | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`)
|
|
88
|
+
* | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
|
|
89
|
+
* | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
|
|
86
90
|
*
|
|
87
91
|
* ## Event emitting order
|
|
88
92
|
*
|
|
@@ -48,6 +48,9 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
|
|
|
48
48
|
* Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
|
|
49
49
|
* (e.g., `tabindex="-1"`).
|
|
50
50
|
*
|
|
51
|
+
* Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
|
|
52
|
+
* are focusable.
|
|
53
|
+
*
|
|
51
54
|
* ### Overwrite next element
|
|
52
55
|
*
|
|
53
56
|
* Override automatic navigation by adding one of these attributes to a focusable element:
|
|
@@ -98,7 +101,8 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
|
|
|
98
101
|
* | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
|
|
99
102
|
* | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
|
|
100
103
|
* | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
|
|
101
|
-
* | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`)
|
|
104
|
+
* | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
|
|
105
|
+
* | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
|
|
102
106
|
*
|
|
103
107
|
* ## Event emitting order
|
|
104
108
|
*
|
|
@@ -423,6 +427,7 @@ class SpatialNavigationProvider extends Provider {
|
|
|
423
427
|
focusableElements.push(...findFocusable(el, {
|
|
424
428
|
excludedElements: checkedFocusArea ? [checkedFocusArea] : undefined,
|
|
425
429
|
includeSelectors: ['[data-spatial-focusable]'],
|
|
430
|
+
excludeSelectors: ['[data-spatial-exclude]'],
|
|
426
431
|
}));
|
|
427
432
|
const result = this.focusNextInFocusableAria(focusableElements, direction);
|
|
428
433
|
// If there is a focusable element found, or reached the active trap or the root, stop searching
|
|
@@ -58,9 +58,26 @@ declare class Tooltip extends Popover {
|
|
|
58
58
|
*/
|
|
59
59
|
private setTooltipType;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Cached auto-generated id so that if the consumer (or a React wrapper that re-passes props
|
|
62
|
+
* on every render) clears the id we restore the same value instead of minting a fresh UUID
|
|
63
|
+
* on every cycle.
|
|
62
64
|
*/
|
|
63
|
-
private
|
|
65
|
+
private autoGeneratedId;
|
|
66
|
+
/**
|
|
67
|
+
* Ensures the tooltip has a non-empty id. Called from `willUpdate` so the assignment is folded
|
|
68
|
+
* into the in-flight update cycle (no extra render pass, no risk of an update loop).
|
|
69
|
+
*
|
|
70
|
+
* - If the consumer provides an explicit id, we drop the cached auto id so a future clear can
|
|
71
|
+
* regenerate a fresh one.
|
|
72
|
+
* - If the id is empty, we reuse the cached auto id when present, otherwise generate one.
|
|
73
|
+
*/
|
|
74
|
+
private ensureId;
|
|
75
|
+
/**
|
|
76
|
+
* Writes the appropriate aria attribute on the trigger element based on the current tooltipType.
|
|
77
|
+
* Used when only the id changed (tooltipType-driven aria updates are handled in
|
|
78
|
+
* `onTooltipTypeUpdated`).
|
|
79
|
+
*/
|
|
80
|
+
private syncTriggerAriaForId;
|
|
64
81
|
/**
|
|
65
82
|
* Updates the placement attribute if it is not a valid placement.
|
|
66
83
|
* Overriding the default from Popover
|
|
@@ -72,7 +89,7 @@ declare class Tooltip extends Popover {
|
|
|
72
89
|
*/
|
|
73
90
|
private onTooltipTypeUpdated;
|
|
74
91
|
protected isOpenUpdated(oldValue: boolean, newValue: boolean): Promise<void>;
|
|
75
|
-
|
|
92
|
+
protected willUpdate(changedProperties: PropertyValues<this>): void;
|
|
76
93
|
show(): void;
|
|
77
94
|
static styles: Array<CSSResult>;
|
|
78
95
|
}
|
|
@@ -63,6 +63,12 @@ class Tooltip extends Popover {
|
|
|
63
63
|
* @default false
|
|
64
64
|
*/
|
|
65
65
|
this.onlyShowWhenTriggerOverflows = DEFAULTS.ONLY_SHOW_WHEN_TRIGGER_OVERFLOWS;
|
|
66
|
+
/**
|
|
67
|
+
* Cached auto-generated id so that if the consumer (or a React wrapper that re-passes props
|
|
68
|
+
* on every render) clears the id we restore the same value instead of minting a fresh UUID
|
|
69
|
+
* on every cycle.
|
|
70
|
+
*/
|
|
71
|
+
this.autoGeneratedId = null;
|
|
66
72
|
}
|
|
67
73
|
connectedCallback() {
|
|
68
74
|
super.connectedCallback();
|
|
@@ -99,26 +105,42 @@ class Tooltip extends Popover {
|
|
|
99
105
|
this.setAttribute('tooltip-type', Object.values(TOOLTIP_TYPES).includes(type) ? type : DEFAULTS.TOOLTIP_TYPE);
|
|
100
106
|
}
|
|
101
107
|
/**
|
|
102
|
-
*
|
|
108
|
+
* Ensures the tooltip has a non-empty id. Called from `willUpdate` so the assignment is folded
|
|
109
|
+
* into the in-flight update cycle (no extra render pass, no risk of an update loop).
|
|
110
|
+
*
|
|
111
|
+
* - If the consumer provides an explicit id, we drop the cached auto id so a future clear can
|
|
112
|
+
* regenerate a fresh one.
|
|
113
|
+
* - If the id is empty, we reuse the cached auto id when present, otherwise generate one.
|
|
103
114
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
await this.updateComplete;
|
|
110
|
-
// Update the aria props on the trigger component to updated tooltip id.
|
|
111
|
-
if (this.triggerElement) {
|
|
112
|
-
switch (this.tooltipType) {
|
|
113
|
-
case TOOLTIP_TYPES.DESCRIPTION:
|
|
114
|
-
this.triggerElement.setAttribute('aria-describedby', this.id);
|
|
115
|
-
break;
|
|
116
|
-
case TOOLTIP_TYPES.LABEL:
|
|
117
|
-
this.triggerElement.setAttribute('aria-labelledby', this.id);
|
|
118
|
-
break;
|
|
119
|
-
default:
|
|
120
|
-
break;
|
|
115
|
+
ensureId() {
|
|
116
|
+
if (this.id.length > 0) {
|
|
117
|
+
if (this.autoGeneratedId && this.id !== this.autoGeneratedId) {
|
|
118
|
+
this.autoGeneratedId = null;
|
|
121
119
|
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!this.autoGeneratedId) {
|
|
123
|
+
this.autoGeneratedId = `mdc-tooltip-${uuidv4()}`;
|
|
124
|
+
}
|
|
125
|
+
this.id = this.autoGeneratedId;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Writes the appropriate aria attribute on the trigger element based on the current tooltipType.
|
|
129
|
+
* Used when only the id changed (tooltipType-driven aria updates are handled in
|
|
130
|
+
* `onTooltipTypeUpdated`).
|
|
131
|
+
*/
|
|
132
|
+
syncTriggerAriaForId() {
|
|
133
|
+
if (!this.triggerElement)
|
|
134
|
+
return;
|
|
135
|
+
switch (this.tooltipType) {
|
|
136
|
+
case TOOLTIP_TYPES.DESCRIPTION:
|
|
137
|
+
this.triggerElement.setAttribute('aria-describedby', this.id);
|
|
138
|
+
break;
|
|
139
|
+
case TOOLTIP_TYPES.LABEL:
|
|
140
|
+
this.triggerElement.setAttribute('aria-labelledby', this.id);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
break;
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
146
|
/**
|
|
@@ -181,17 +203,26 @@ class Tooltip extends Popover {
|
|
|
181
203
|
}
|
|
182
204
|
await super.isOpenUpdated(oldValue, newValue);
|
|
183
205
|
}
|
|
184
|
-
|
|
185
|
-
super.
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
willUpdate(changedProperties) {
|
|
207
|
+
super.willUpdate(changedProperties);
|
|
208
|
+
// Ensure an id exists before render. Assigning `this.id` inside willUpdate is folded into
|
|
209
|
+
// the current update cycle, so there is no extra render and no loop risk (the previous
|
|
210
|
+
// implementation set the id from inside `update()` and awaited `updateComplete`, which
|
|
211
|
+
// produced an infinite re-render loop under @lit/react wrappers that keep clearing the id).
|
|
212
|
+
const idChanged = changedProperties.has('id');
|
|
213
|
+
if (idChanged) {
|
|
214
|
+
this.ensureId();
|
|
188
215
|
}
|
|
189
216
|
if (changedProperties.has('placement')) {
|
|
190
217
|
this.onPlacementUpdated();
|
|
191
218
|
}
|
|
192
219
|
if (changedProperties.has('tooltipType')) {
|
|
220
|
+
// onTooltipTypeUpdated also refreshes the trigger aria attribute, so it covers the id sync.
|
|
193
221
|
this.onTooltipTypeUpdated(changedProperties);
|
|
194
222
|
}
|
|
223
|
+
else if (idChanged) {
|
|
224
|
+
this.syncTriggerAriaForId();
|
|
225
|
+
}
|
|
195
226
|
}
|
|
196
227
|
show() {
|
|
197
228
|
if (this.onlyShowWhenTriggerOverflows && this.triggerElement && hasOverflowMixin(this.triggerElement)) {
|
|
@@ -44925,7 +44925,7 @@
|
|
|
44925
44925
|
"declarations": [
|
|
44926
44926
|
{
|
|
44927
44927
|
"kind": "class",
|
|
44928
|
-
"description": "This component manages focus using spatial navigation and provides context for child components.\n\nPlace it at the root of the application.\n\n[Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among\nelements on a 2D plane, common on TVs and game consoles with remotes or gamepads.\n\n## Focus management\n\nThe provider listens to keyboard events and moves focus among elements based on arrow key input.\nYou can influence or override this behavior.\n\nNote: The algorithm is distance-based, so the UI should be designed so focusable elements are\npredictably reachable. Relative element positions should remain stable; responsive layouts can\nmake navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected\nbehavior in Storybook when resizing. See the \"Limitations\" section.\n\n### Automatic\n\nBy default, the next focus target is computed from element positions:\n\n1. Find the nearest focus area (scrollable container or active focus trap) relative to the current element.\n2. Collect focusable elements in that area.\n3. Compute distances from the current element to candidates using the W3C \"find the shortest\n distance\" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance\n4. If no candidates are found, repeat from step 1, skipping areas already checked.\n5. Focus the closest candidate.\n\nElements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be\n(e.g., `tabindex=\"-1\"`).\n\n### Overwrite next element\n\nOverride automatic navigation by adding one of these attributes to a focusable element:\n\n- `data-spatial-up`\n- `data-spatial-down`\n- `data-spatial-left`\n- `data-spatial-right`\n\nEach attribute value must be the id of the element to focus when the corresponding key is pressed.\n\n### Element internal navigation\n\nComplex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves\nfocus internally on Down until the last item, after which Down should fall back to provider navigation.\n\nTo prevent the provider from handling a key, listen to `navbeforeprocess` and call `event.preventDefault()`.\nThis event fires after the component handles `keydown`.\n\n### Cancel focus change\n\nBefore focusing a computed target, the provider dispatches `navbeforefocus` on the current element. Call\n`event.preventDefault()` on this event to cancel the focus change.\n\n## Enter action\n\nPressing Enter triggers `.click()` on the currently focused element.\n\nYou can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n\n## Escape/Back action\n\nPressing Escape tries to find a focusable element with `data-spatial-go-back` and clicks it. If none exists,\nthe provider calls `history.back()`.\n\nYou can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n\nYou can also intercept the back click by listening to `navback` and calling `event.preventDefault()`.\n\n## Control data attributes\n\nSupported data attributes:\n\n| Attribute | Value | Default | Description |\n|------------------------|-------------|---------|-------------------------------------------------------------------------------------|\n| `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |\n| `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |\n| `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |\n| `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |\n| `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |\n| `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex=\"-1\"`)
|
|
44928
|
+
"description": "This component manages focus using spatial navigation and provides context for child components.\n\nPlace it at the root of the application.\n\n[Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among\nelements on a 2D plane, common on TVs and game consoles with remotes or gamepads.\n\n## Focus management\n\nThe provider listens to keyboard events and moves focus among elements based on arrow key input.\nYou can influence or override this behavior.\n\nNote: The algorithm is distance-based, so the UI should be designed so focusable elements are\npredictably reachable. Relative element positions should remain stable; responsive layouts can\nmake navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected\nbehavior in Storybook when resizing. See the \"Limitations\" section.\n\n### Automatic\n\nBy default, the next focus target is computed from element positions:\n\n1. Find the nearest focus area (scrollable container or active focus trap) relative to the current element.\n2. Collect focusable elements in that area.\n3. Compute distances from the current element to candidates using the W3C \"find the shortest\n distance\" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance\n4. If no candidates are found, repeat from step 1, skipping areas already checked.\n5. Focus the closest candidate.\n\nElements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be\n(e.g., `tabindex=\"-1\"`).\n\nElements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they\nare focusable.\n\n### Overwrite next element\n\nOverride automatic navigation by adding one of these attributes to a focusable element:\n\n- `data-spatial-up`\n- `data-spatial-down`\n- `data-spatial-left`\n- `data-spatial-right`\n\nEach attribute value must be the id of the element to focus when the corresponding key is pressed.\n\n### Element internal navigation\n\nComplex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves\nfocus internally on Down until the last item, after which Down should fall back to provider navigation.\n\nTo prevent the provider from handling a key, listen to `navbeforeprocess` and call `event.preventDefault()`.\nThis event fires after the component handles `keydown`.\n\n### Cancel focus change\n\nBefore focusing a computed target, the provider dispatches `navbeforefocus` on the current element. Call\n`event.preventDefault()` on this event to cancel the focus change.\n\n## Enter action\n\nPressing Enter triggers `.click()` on the currently focused element.\n\nYou can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n\n## Escape/Back action\n\nPressing Escape tries to find a focusable element with `data-spatial-go-back` and clicks it. If none exists,\nthe provider calls `history.back()`.\n\nYou can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n\nYou can also intercept the back click by listening to `navback` and calling `event.preventDefault()`.\n\n## Control data attributes\n\nSupported data attributes:\n\n| Attribute | Value | Default | Description |\n|------------------------|-------------|---------|-------------------------------------------------------------------------------------|\n| `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |\n| `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |\n| `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |\n| `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |\n| `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |\n| `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex=\"-1\"`) |\n| `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |\n\n## Event emitting order\n\nOn a navigation key press, events fire in this order:\n\n1. `navbeforeprocess` on the currently focused element.\n2. If not prevented:\na. Arrow keys: `navbeforefocus` on the currently focused element.\nb. Enter: `.click()` on the currently focused element.\nc. Escape/Back: `navback` on the provider, then `.click()` on the go-back element or `history.back()`.\n3. If no target is found in the requested direction: `navnotarget` on the provider.\n\n## Handle complex components\n\n### Generic components\n\nComponents that handle navigation internally should prevent the provider from acting. Handle `navbeforeprocess`\nand call `event.preventDefault()` for keys you process yourself.\n\n### Form inputs\n\nNative inputs often submit on Enter, which is not desirable here. Enter should toggle or activate the control\n(e.g., check/uncheck). Provide a dedicated submit button users can navigate to and press Enter on.\n\n### Utilities for complex components\n\n#### KeyToActionMixin\n\nMaps key events to action names. Call `getActionForKeyEvent` to get the action for a keyboard event. Also provides\n`getKeyboardNavMode` to check whether navigation is spatial or default.\n\n#### KeyDownHandledMixin\n\nNotify the provider when a component handled `keydown` internally. Call `keyDownEventHandled` whenever you process\nkeydown yourself.\n\n## Platform specific behaviors\n\nConsider remote/gamepad constraints. Often focus alone is not enough and users press Enter to \"enter\" an interactive mode:\n- Select: Enter opens options rather than arrow keys opening a popover.\n- Text inputs: see the next section.\n- Slider: Enter to start adjusting, arrow keys to change value, Enter/Escape to stop.\n\n### Text inputs\n\nOn TV-like platforms without physical keyboards, Enter/focus on an input should open a virtual keyboard instead of submitting\nthe form. Users must close the keyboard (Escape) to continue spatial navigation.\n\nIf navigation keys are mapped to letters (e.g., `w/a/s/d`), they should navigate, not change input values. Inputs should\nbe edited via the virtual keyboard.\n\nNote: Stories do not emulate virtual keyboards, so letter-based navigation may change input values in Storybook.\n\n## Debugging\n\n### Storybook toolbar\n\nEnable \"Spatial navigation\" in the toolbar. Key mapping:\n- Up - ArrowUp\n- Left - ArrowLeft\n- Down - ArrowDown\n- Right - ArrowRight\n- Enter - Enter\n- Escape - Escape\n\nWith wrapper: wraps the component in a 3x3 grid with surrounding buttons for testing.\nWithout wrapper: renders the component alone.\n\n### Visual debugger\n\nWith spatial navigation enabled, press Shift + navigation key to visualize calculations:\n\n- Star: next active element\n- `#{number}`: candidate order by distance\n- `D: {distance}`: computed distance\n\n## Limitations\n\n### Completeness\n\nThe algorithm cannot guarantee reachability to all elements using the four directions. Some components can be isolated.\n\nWorkarounds:\n- Use data attributes to explicitly link navigation targets.\n- Arrange DOM to improve spatial consistency:\n- Group focusable elements using dedicated components (lists, menus, etc.).\n- Avoid complex grid-like layouts with variable-sized items.\n- Avoid overlap along horizontal or vertical axes.\n- Avoid nested focusable elements where possible.\n- Tune algorithm weights to match your UI layout.\n\n### Scrollable containers\n\nContent scrolling is not supported yet, e.g.:\n- Focused element larger than the viewport.\n- Scrollable content without interactive children.\n\n### Nested providers\n\nOnly one provider instance is supported in the application at a time.",
|
|
44929
44929
|
"name": "SpatialNavigationProvider",
|
|
44930
44930
|
"members": [
|
|
44931
44931
|
{
|
|
@@ -45025,7 +45025,7 @@
|
|
|
45025
45025
|
"module": "/src/models"
|
|
45026
45026
|
},
|
|
45027
45027
|
"tagName": "mdc-spatialnavigationprovider",
|
|
45028
|
-
"jsDoc": "/**\n * This component manages focus using spatial navigation and provides context for child components.\n *\n * Place it at the root of the application.\n *\n * [Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among\n * elements on a 2D plane, common on TVs and game consoles with remotes or gamepads.\n *\n * ## Focus management\n *\n * The provider listens to keyboard events and moves focus among elements based on arrow key input.\n * You can influence or override this behavior.\n *\n * Note: The algorithm is distance-based, so the UI should be designed so focusable elements are\n * predictably reachable. Relative element positions should remain stable; responsive layouts can\n * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected\n * behavior in Storybook when resizing. See the \"Limitations\" section.\n *\n * ### Automatic\n *\n * By default, the next focus target is computed from element positions:\n *\n * 1. Find the nearest focus area (scrollable container or active focus trap) relative to the current element.\n * 2. Collect focusable elements in that area.\n * 3. Compute distances from the current element to candidates using the W3C \"find the shortest\n * distance\" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance\n * 4. If no candidates are found, repeat from step 1, skipping areas already checked.\n * 5. Focus the closest candidate.\n *\n * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be\n * (e.g., `tabindex=\"-1\"`).\n *\n * ### Overwrite next element\n *\n * Override automatic navigation by adding one of these attributes to a focusable element:\n *\n * - `data-spatial-up`\n * - `data-spatial-down`\n * - `data-spatial-left`\n * - `data-spatial-right`\n *\n * Each attribute value must be the id of the element to focus when the corresponding key is pressed.\n *\n * ### Element internal navigation\n *\n * Complex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves\n * focus internally on Down until the last item, after which Down should fall back to provider navigation.\n *\n * To prevent the provider from handling a key, listen to `navbeforeprocess` and call `event.preventDefault()`.\n * This event fires after the component handles `keydown`.\n *\n * ### Cancel focus change\n *\n * Before focusing a computed target, the provider dispatches `navbeforefocus` on the current element. Call\n * `event.preventDefault()` on this event to cancel the focus change.\n *\n * ## Enter action\n *\n * Pressing Enter triggers `.click()` on the currently focused element.\n *\n * You can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n *\n * ## Escape/Back action\n *\n * Pressing Escape tries to find a focusable element with `data-spatial-go-back` and clicks it. If none exists,\n * the provider calls `history.back()`.\n *\n * You can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n *\n * You can also intercept the back click by listening to `navback` and calling `event.preventDefault()`.\n *\n * ## Control data attributes\n *\n * Supported data attributes:\n *\n * | Attribute | Value | Default | Description |\n * |------------------------|-------------|---------|-------------------------------------------------------------------------------------|\n * | `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |\n * | `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |\n * | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |\n * | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |\n * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |\n * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex=\"-1\"`)
|
|
45028
|
+
"jsDoc": "/**\n * This component manages focus using spatial navigation and provides context for child components.\n *\n * Place it at the root of the application.\n *\n * [Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among\n * elements on a 2D plane, common on TVs and game consoles with remotes or gamepads.\n *\n * ## Focus management\n *\n * The provider listens to keyboard events and moves focus among elements based on arrow key input.\n * You can influence or override this behavior.\n *\n * Note: The algorithm is distance-based, so the UI should be designed so focusable elements are\n * predictably reachable. Relative element positions should remain stable; responsive layouts can\n * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected\n * behavior in Storybook when resizing. See the \"Limitations\" section.\n *\n * ### Automatic\n *\n * By default, the next focus target is computed from element positions:\n *\n * 1. Find the nearest focus area (scrollable container or active focus trap) relative to the current element.\n * 2. Collect focusable elements in that area.\n * 3. Compute distances from the current element to candidates using the W3C \"find the shortest\n * distance\" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance\n * 4. If no candidates are found, repeat from step 1, skipping areas already checked.\n * 5. Focus the closest candidate.\n *\n * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be\n * (e.g., `tabindex=\"-1\"`).\n *\n * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they\n * are focusable.\n *\n * ### Overwrite next element\n *\n * Override automatic navigation by adding one of these attributes to a focusable element:\n *\n * - `data-spatial-up`\n * - `data-spatial-down`\n * - `data-spatial-left`\n * - `data-spatial-right`\n *\n * Each attribute value must be the id of the element to focus when the corresponding key is pressed.\n *\n * ### Element internal navigation\n *\n * Complex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves\n * focus internally on Down until the last item, after which Down should fall back to provider navigation.\n *\n * To prevent the provider from handling a key, listen to `navbeforeprocess` and call `event.preventDefault()`.\n * This event fires after the component handles `keydown`.\n *\n * ### Cancel focus change\n *\n * Before focusing a computed target, the provider dispatches `navbeforefocus` on the current element. Call\n * `event.preventDefault()` on this event to cancel the focus change.\n *\n * ## Enter action\n *\n * Pressing Enter triggers `.click()` on the currently focused element.\n *\n * You can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n *\n * ## Escape/Back action\n *\n * Pressing Escape tries to find a focusable element with `data-spatial-go-back` and clicks it. If none exists,\n * the provider calls `history.back()`.\n *\n * You can prevent this by listening to `navbeforeprocess` and calling `event.preventDefault()`.\n *\n * You can also intercept the back click by listening to `navback` and calling `event.preventDefault()`.\n *\n * ## Control data attributes\n *\n * Supported data attributes:\n *\n * | Attribute | Value | Default | Description |\n * |------------------------|-------------|---------|-------------------------------------------------------------------------------------|\n * | `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |\n * | `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |\n * | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |\n * | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |\n * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |\n * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex=\"-1\"`) |\n * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |\n *\n * ## Event emitting order\n *\n * On a navigation key press, events fire in this order:\n *\n * 1. `navbeforeprocess` on the currently focused element.\n * 2. If not prevented:\n * a. Arrow keys: `navbeforefocus` on the currently focused element.\n * b. Enter: `.click()` on the currently focused element.\n * c. Escape/Back: `navback` on the provider, then `.click()` on the go-back element or `history.back()`.\n * 3. If no target is found in the requested direction: `navnotarget` on the provider.\n *\n * ## Handle complex components\n *\n * ### Generic components\n *\n * Components that handle navigation internally should prevent the provider from acting. Handle `navbeforeprocess`\n * and call `event.preventDefault()` for keys you process yourself.\n *\n * ### Form inputs\n *\n * Native inputs often submit on Enter, which is not desirable here. Enter should toggle or activate the control\n * (e.g., check/uncheck). Provide a dedicated submit button users can navigate to and press Enter on.\n *\n * ### Utilities for complex components\n *\n * #### KeyToActionMixin\n *\n * Maps key events to action names. Call `getActionForKeyEvent` to get the action for a keyboard event. Also provides\n * `getKeyboardNavMode` to check whether navigation is spatial or default.\n *\n * #### KeyDownHandledMixin\n *\n * Notify the provider when a component handled `keydown` internally. Call `keyDownEventHandled` whenever you process\n * keydown yourself.\n *\n * ## Platform specific behaviors\n *\n * Consider remote/gamepad constraints. Often focus alone is not enough and users press Enter to \"enter\" an interactive mode:\n * - Select: Enter opens options rather than arrow keys opening a popover.\n * - Text inputs: see the next section.\n * - Slider: Enter to start adjusting, arrow keys to change value, Enter/Escape to stop.\n *\n * ### Text inputs\n *\n * On TV-like platforms without physical keyboards, Enter/focus on an input should open a virtual keyboard instead of submitting\n * the form. Users must close the keyboard (Escape) to continue spatial navigation.\n *\n * If navigation keys are mapped to letters (e.g., `w/a/s/d`), they should navigate, not change input values. Inputs should\n * be edited via the virtual keyboard.\n *\n * Note: Stories do not emulate virtual keyboards, so letter-based navigation may change input values in Storybook.\n *\n * ## Debugging\n *\n * ### Storybook toolbar\n *\n * Enable \"Spatial navigation\" in the toolbar. Key mapping:\n * - Up - ArrowUp\n * - Left - ArrowLeft\n * - Down - ArrowDown\n * - Right - ArrowRight\n * - Enter - Enter\n * - Escape - Escape\n *\n * With wrapper: wraps the component in a 3x3 grid with surrounding buttons for testing.\n * Without wrapper: renders the component alone.\n *\n * ### Visual debugger\n *\n * With spatial navigation enabled, press Shift + navigation key to visualize calculations:\n *\n * - Star: next active element\n * - `#{number}`: candidate order by distance\n * - `D: {distance}`: computed distance\n *\n * ## Limitations\n *\n * ### Completeness\n *\n * The algorithm cannot guarantee reachability to all elements using the four directions. Some components can be isolated.\n *\n * Workarounds:\n * - Use data attributes to explicitly link navigation targets.\n * - Arrange DOM to improve spatial consistency:\n * - Group focusable elements using dedicated components (lists, menus, etc.).\n * - Avoid complex grid-like layouts with variable-sized items.\n * - Avoid overlap along horizontal or vertical axes.\n * - Avoid nested focusable elements where possible.\n * - Tune algorithm weights to match your UI layout.\n *\n * ### Scrollable containers\n *\n * Content scrolling is not supported yet, e.g.:\n * - Focused element larger than the viewport.\n * - Scrollable content without interactive children.\n *\n * ### Nested providers\n *\n * Only one provider instance is supported in the application at a time.\n *\n * @event navbeforeprocess - (React: onNavBeforeProcess) This event dispatched before spatial navigation process any key event.\n * It can be canceled to prevent any action from spatial navigation, e.g.: back, click or calculating the next candidate.\n * @event navbeforefocus - (React: onNavBeforeFocus) This event is dispatched before the focus is changing to the next element.\n * It can be canceled to prevent the focus change. @see https://www.w3.org/TR/css-nav-1/#event-type-navbeforefocus\n * @event navback - (React: onNavBack) This event dispatched a back navigation triggered by the user.\n * The event's detail contains the goBackElement if any. It is cancelable to prevent click\n * action on the goBackElement.\n * @event navnotarget - (React: onNavNoTarget) This event is dispatched when there is no target to focus in the current focus area and\n * in the given direction .\n *\n * @tagname mdc-spatialnavigationprovider\n */",
|
|
45029
45029
|
"customElement": true
|
|
45030
45030
|
}
|
|
45031
45031
|
],
|
|
@@ -53149,6 +53149,16 @@
|
|
|
53149
53149
|
"module": "components/popover/popover.component.js"
|
|
53150
53150
|
}
|
|
53151
53151
|
},
|
|
53152
|
+
{
|
|
53153
|
+
"kind": "field",
|
|
53154
|
+
"name": "autoGeneratedId",
|
|
53155
|
+
"type": {
|
|
53156
|
+
"text": "string | null"
|
|
53157
|
+
},
|
|
53158
|
+
"privacy": "private",
|
|
53159
|
+
"default": "null",
|
|
53160
|
+
"description": "Cached auto-generated id so that if the consumer (or a React wrapper that re-passes props\non every render) clears the id we restore the same value instead of minting a fresh UUID\non every cycle."
|
|
53161
|
+
},
|
|
53152
53162
|
{
|
|
53153
53163
|
"kind": "field",
|
|
53154
53164
|
"name": "backdrop",
|
|
@@ -53338,6 +53348,17 @@
|
|
|
53338
53348
|
"module": "components/popover/popover.component.js"
|
|
53339
53349
|
}
|
|
53340
53350
|
},
|
|
53351
|
+
{
|
|
53352
|
+
"kind": "method",
|
|
53353
|
+
"name": "ensureId",
|
|
53354
|
+
"privacy": "private",
|
|
53355
|
+
"return": {
|
|
53356
|
+
"type": {
|
|
53357
|
+
"text": "void"
|
|
53358
|
+
}
|
|
53359
|
+
},
|
|
53360
|
+
"description": "Ensures the tooltip has a non-empty id. Called from `willUpdate` so the assignment is folded\ninto the in-flight update cycle (no extra render pass, no risk of an update loop).\n\n- If the consumer provides an explicit id, we drop the cached auto id so a future clear can\n regenerate a fresh one.\n- If the id is empty, we reuse the cached auto id when present, otherwise generate one."
|
|
53361
|
+
},
|
|
53341
53362
|
{
|
|
53342
53363
|
"kind": "field",
|
|
53343
53364
|
"name": "focusBackToTrigger",
|
|
@@ -53558,17 +53579,6 @@
|
|
|
53558
53579
|
"module": "components/popover/popover.component.js"
|
|
53559
53580
|
}
|
|
53560
53581
|
},
|
|
53561
|
-
{
|
|
53562
|
-
"kind": "method",
|
|
53563
|
-
"name": "onIdUpdated",
|
|
53564
|
-
"privacy": "private",
|
|
53565
|
-
"return": {
|
|
53566
|
-
"type": {
|
|
53567
|
-
"text": "Promise<void>"
|
|
53568
|
-
}
|
|
53569
|
-
},
|
|
53570
|
-
"description": "Updates the tooltip id if it is empty."
|
|
53571
|
-
},
|
|
53572
53582
|
{
|
|
53573
53583
|
"kind": "field",
|
|
53574
53584
|
"name": "onlyShowWhenTriggerOverflows",
|
|
@@ -53761,6 +53771,17 @@
|
|
|
53761
53771
|
"module": "components/popover/popover.component.js"
|
|
53762
53772
|
}
|
|
53763
53773
|
},
|
|
53774
|
+
{
|
|
53775
|
+
"kind": "method",
|
|
53776
|
+
"name": "syncTriggerAriaForId",
|
|
53777
|
+
"privacy": "private",
|
|
53778
|
+
"return": {
|
|
53779
|
+
"type": {
|
|
53780
|
+
"text": "void"
|
|
53781
|
+
}
|
|
53782
|
+
},
|
|
53783
|
+
"description": "Writes the appropriate aria attribute on the trigger element based on the current tooltipType.\nUsed when only the id changed (tooltipType-driven aria updates are handled in\n`onTooltipTypeUpdated`)."
|
|
53784
|
+
},
|
|
53764
53785
|
{
|
|
53765
53786
|
"kind": "field",
|
|
53766
53787
|
"name": "togglePopoverVisible",
|
|
@@ -33,6 +33,9 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
|
|
|
33
33
|
* Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
|
|
34
34
|
* (e.g., `tabindex="-1"`).
|
|
35
35
|
*
|
|
36
|
+
* Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
|
|
37
|
+
* are focusable.
|
|
38
|
+
*
|
|
36
39
|
* ### Overwrite next element
|
|
37
40
|
*
|
|
38
41
|
* Override automatic navigation by adding one of these attributes to a focusable element:
|
|
@@ -83,7 +86,8 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
|
|
|
83
86
|
* | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
|
|
84
87
|
* | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
|
|
85
88
|
* | `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"`)
|
|
89
|
+
* | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
|
|
90
|
+
* | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
|
|
87
91
|
*
|
|
88
92
|
* ## Event emitting order
|
|
89
93
|
*
|
|
@@ -34,6 +34,9 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
|
|
|
34
34
|
* Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
|
|
35
35
|
* (e.g., `tabindex="-1"`).
|
|
36
36
|
*
|
|
37
|
+
* Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
|
|
38
|
+
* are focusable.
|
|
39
|
+
*
|
|
37
40
|
* ### Overwrite next element
|
|
38
41
|
*
|
|
39
42
|
* Override automatic navigation by adding one of these attributes to a focusable element:
|
|
@@ -84,7 +87,8 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
|
|
|
84
87
|
* | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
|
|
85
88
|
* | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
|
|
86
89
|
* | `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"`)
|
|
90
|
+
* | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
|
|
91
|
+
* | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
|
|
88
92
|
*
|
|
89
93
|
* ## Event emitting order
|
|
90
94
|
*
|
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') {
|