@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.
@@ -15,30 +15,30 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
15
15
  *
16
16
  * ### Steps
17
17
  *
18
- * Spatial navigation goes trough the following steps after each keydown:
18
+ * Spatial navigation goes through the following steps after each keydown:
19
19
  *
20
20
  * 1. Handle `keydown` in the capture phase.
21
- * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
22
- * provider own `keydown` handler (see step 3).
23
- * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not
24
- * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the
21
+ * - When the active element has a `data-spatial-{direction}` attribute, then prevent all component navigation and call the
22
+ * provider's own `keydown` handler (see step 3).
23
+ * - When the active element's parent is scrollable and it is not fully visible in the given direction, and it does not
24
+ * have a `data-spatial-noscroll` attribute, prevent all navigation and scroll in the given direction half-size of the
25
25
  * scroll view.
26
26
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
27
27
  * prevented.
28
28
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
29
- * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
29
+ * - If a key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
30
30
  * the key event itself. If `navbeforeprocess` event is prevented, stop here.
31
- * - If the component did not handle `keydown`, it calculate the next focusable item
32
- * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
31
+ * - If the component did not handle `keydown`, it calculates the next focusable item
32
+ * - if the active element has a `data-spatial-{direction}` attribute, it will try to focus the element with the id.
33
33
  * - Otherwise calculate the next focused item based on the direction and distances.
34
34
  * - If there is no next item, it emits `navnotarget` event
35
35
  * - Otherwise emit `navbeforefocus`,
36
- * - If this event prevented, nothing happens
36
+ * - If this event is prevented, nothing happens
37
37
  * - Otherwise the focus moves to the next element
38
38
  *
39
39
  * ### Determine next focus
40
40
  *
41
- * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
41
+ * The provider uses multiple ways to determine the next focused element. The order defined in the "Steps" section.
42
42
  *
43
43
  * #### Calculated focus
44
44
  *
@@ -49,9 +49,9 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
49
49
  * 3. Compute distances from the current element to candidates using the W3C "find the shortest
50
50
  * distance" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance
51
51
  * 4. If no candidates are found, repeat from step 1, skipping areas already checked.
52
- * 5. Focus the closest candidate.
52
+ * 5. Focus on the closest candidate.
53
53
  *
54
- * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
54
+ * Elements with `data-spatial-focusable` are treated as focusable even if they do otherwise not be
55
55
  * (e.g., `tabindex="-1"`).
56
56
  *
57
57
  * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
@@ -62,7 +62,7 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
62
62
  * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
63
63
  * behavior in Storybook when resizing. See the "Limitations" section.
64
64
  *
65
- * #### Overwrite next element
65
+ * #### Overwrite the next element
66
66
  *
67
67
  * Override calculated navigation by adding one of these attributes to a focusable element:
68
68
  *
@@ -105,17 +105,17 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
105
105
  *
106
106
  * Supported data attributes:
107
107
  *
108
- * | Attribute | Value | Default | Description |
109
- * |------------------------------|-------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------|
110
- * | `data-spatial-left` | empty string / id / selector | N/A | Prevent native navigation in Left direction and focus element if exists |
111
- * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction and focus element if exists |
112
- * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in Right direction and focus element if exists |
113
- * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction and focus element if exists |
114
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
115
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
116
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
117
- * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
118
- * | `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 | |
108
+ * | Attribute | Value | Default | Description |
109
+ * |------------------------------|-------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------|
110
+ * | `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. |
111
+ * | `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. |
112
+ * | `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. |
113
+ * | `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. |
114
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
115
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
116
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
117
+ * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
118
+ * | `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 |
119
119
  *
120
120
  * ## Event emitting order
121
121
  *
@@ -154,7 +154,7 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
154
154
  *
155
155
  * ## Platform specific behaviors
156
156
  *
157
- * Consider remote/gamepad constraints. Often focus alone is not enough and users press Enter to "enter" an interactive mode:
157
+ * Consider remote/gamepad constraints. Often focus alone is not enough, and users press Enter to "enter" an interactive mode:
158
158
  * - Select: Enter opens options rather than arrow keys opening a popover.
159
159
  * - Text inputs: see the next section.
160
160
  * - Slider: Enter to start adjusting, arrow keys to change value, Enter/Escape to stop.
@@ -182,7 +182,7 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
182
182
  * - Escape - Escape
183
183
  *
184
184
  * With wrapper: wraps the component in a 3x3 grid with surrounding buttons for testing.
185
- * Without wrapper: renders the component alone.
185
+ * Without a wrapper: renders the component alone.
186
186
  *
187
187
  * ### Visual debugger
188
188
  *
@@ -340,7 +340,7 @@ declare class SpatialNavigationProvider extends Provider<SpatialNavigationContex
340
340
  /**
341
341
  * Set the active element.
342
342
  *
343
- * Also, setup MutationObserver to track the element removal from the DOM.
343
+ * Also, set up MutationObserver to track the element removal from the DOM.
344
344
  *
345
345
  * @param element - New active element
346
346
  * @internal
@@ -389,8 +389,7 @@ declare class SpatialNavigationProvider extends Provider<SpatialNavigationContex
389
389
  /**
390
390
  * Handle back action
391
391
  *
392
- * Either trigger click on goBack element if any
393
- * otherwise call default go back handler
392
+ * Either trigger click on the goBack element if any otherwise call the default go back handler
394
393
  *
395
394
  * @returns true when go back handled, false otherwise
396
395
  */
@@ -12,7 +12,7 @@ import { Provider } from '../../models';
12
12
  import { findFocusable, getDomActiveElement, getHostComposePath, getScrollableAxis } from '../../utils/dom';
13
13
  import { FocusTrapStack } from '../../utils/mixins/focus/FocusTrapStack';
14
14
  import SpatialNavigationProviderContext from './spatialnavigationprovider.context';
15
- import { DEFAULTS } from './spatialnavigationprovider.constants';
15
+ import { DATA_ATTRIBUTES, DEFAULTS } from './spatialnavigationprovider.constants';
16
16
  import { orderElementsByDistance } from './spatialnavigationprovider.utils';
17
17
  import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
18
18
  // AI-Assisted
@@ -31,30 +31,30 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
31
31
  *
32
32
  * ### Steps
33
33
  *
34
- * Spatial navigation goes trough the following steps after each keydown:
34
+ * Spatial navigation goes through the following steps after each keydown:
35
35
  *
36
36
  * 1. Handle `keydown` in the capture phase.
37
- * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
38
- * provider own `keydown` handler (see step 3).
39
- * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not
40
- * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the
37
+ * - When the active element has a `data-spatial-{direction}` attribute, then prevent all component navigation and call the
38
+ * provider's own `keydown` handler (see step 3).
39
+ * - When the active element's parent is scrollable and it is not fully visible in the given direction, and it does not
40
+ * have a `data-spatial-noscroll` attribute, prevent all navigation and scroll in the given direction half-size of the
41
41
  * scroll view.
42
42
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
43
43
  * prevented.
44
44
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
45
- * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
45
+ * - If a key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
46
46
  * the key event itself. If `navbeforeprocess` event is prevented, stop here.
47
- * - If the component did not handle `keydown`, it calculate the next focusable item
48
- * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
47
+ * - If the component did not handle `keydown`, it calculates the next focusable item
48
+ * - if the active element has a `data-spatial-{direction}` attribute, it will try to focus the element with the id.
49
49
  * - Otherwise calculate the next focused item based on the direction and distances.
50
50
  * - If there is no next item, it emits `navnotarget` event
51
51
  * - Otherwise emit `navbeforefocus`,
52
- * - If this event prevented, nothing happens
52
+ * - If this event is prevented, nothing happens
53
53
  * - Otherwise the focus moves to the next element
54
54
  *
55
55
  * ### Determine next focus
56
56
  *
57
- * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
57
+ * The provider uses multiple ways to determine the next focused element. The order defined in the "Steps" section.
58
58
  *
59
59
  * #### Calculated focus
60
60
  *
@@ -65,9 +65,9 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
65
65
  * 3. Compute distances from the current element to candidates using the W3C "find the shortest
66
66
  * distance" algorithm: https://www.w3.org/TR/css-nav-1/#find-the-shortest-distance
67
67
  * 4. If no candidates are found, repeat from step 1, skipping areas already checked.
68
- * 5. Focus the closest candidate.
68
+ * 5. Focus on the closest candidate.
69
69
  *
70
- * Elements with `data-spatial-focusable` are treated as focusable even if they would otherwise not be
70
+ * Elements with `data-spatial-focusable` are treated as focusable even if they do otherwise not be
71
71
  * (e.g., `tabindex="-1"`).
72
72
  *
73
73
  * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
@@ -78,7 +78,7 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
78
78
  * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
79
79
  * behavior in Storybook when resizing. See the "Limitations" section.
80
80
  *
81
- * #### Overwrite next element
81
+ * #### Overwrite the next element
82
82
  *
83
83
  * Override calculated navigation by adding one of these attributes to a focusable element:
84
84
  *
@@ -121,17 +121,17 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
121
121
  *
122
122
  * Supported data attributes:
123
123
  *
124
- * | Attribute | Value | Default | Description |
125
- * |------------------------------|-------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------|
126
- * | `data-spatial-left` | empty string / id / selector | N/A | Prevent native navigation in Left direction and focus element if exists |
127
- * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction and focus element if exists |
128
- * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in Right direction and focus element if exists |
129
- * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction and focus element if exists |
130
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
131
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
132
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
133
- * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
134
- * | `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 | |
124
+ * | Attribute | Value | Default | Description |
125
+ * |------------------------------|-------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------|
126
+ * | `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. |
127
+ * | `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. |
128
+ * | `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. |
129
+ * | `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. |
130
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
131
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
132
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
133
+ * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |
134
+ * | `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 |
135
135
  *
136
136
  * ## Event emitting order
137
137
  *
@@ -170,7 +170,7 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
170
170
  *
171
171
  * ## Platform specific behaviors
172
172
  *
173
- * Consider remote/gamepad constraints. Often focus alone is not enough and users press Enter to "enter" an interactive mode:
173
+ * Consider remote/gamepad constraints. Often focus alone is not enough, and users press Enter to "enter" an interactive mode:
174
174
  * - Select: Enter opens options rather than arrow keys opening a popover.
175
175
  * - Text inputs: see the next section.
176
176
  * - Slider: Enter to start adjusting, arrow keys to change value, Enter/Escape to stop.
@@ -198,7 +198,7 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
198
198
  * - Escape - Escape
199
199
  *
200
200
  * With wrapper: wraps the component in a 3x3 grid with surrounding buttons for testing.
201
- * Without wrapper: renders the component alone.
201
+ * Without a wrapper: renders the component alone.
202
202
  *
203
203
  * ### Visual debugger
204
204
  *
@@ -315,8 +315,8 @@ class SpatialNavigationProvider extends Provider {
315
315
  this.handleKeyDown(evt);
316
316
  }
317
317
  // Handle over sized elements inside scrollable area
318
- if (target.parentElement && !target.hasAttribute('data-spatial-noscroll')) {
319
- const parent = (_a = target.closest('[data-spatial-scroll-parent]')) !== null && _a !== void 0 ? _a : target.parentElement;
318
+ if (target.parentElement && !target.hasAttribute(DATA_ATTRIBUTES.NO_SCROLL)) {
319
+ const parent = (_a = target.closest(`[${DATA_ATTRIBUTES.SCROLL_PARENT}]`)) !== null && _a !== void 0 ? _a : target.parentElement;
320
320
  const targetScrollAxis = getScrollableAxis(parent);
321
321
  if (targetScrollAxis) {
322
322
  const targetBB = target.getBoundingClientRect();
@@ -327,7 +327,9 @@ class SpatialNavigationProvider extends Provider {
327
327
  parent.scrollTo({ top: parent.scrollTop - parentBB.height / 2, behavior: 'auto' });
328
328
  eventHandled = true;
329
329
  }
330
- if (action === 'down' && targetBB.bottom > parentBB.bottom && (parent.scrollTop + parentBB.height) < parent.scrollHeight) {
330
+ if (action === 'down' &&
331
+ targetBB.bottom > parentBB.bottom &&
332
+ parent.scrollTop + parentBB.height < parent.scrollHeight) {
331
333
  parent.scrollTo({ top: parent.scrollTop + parentBB.height / 2, behavior: 'auto' });
332
334
  eventHandled = true;
333
335
  }
@@ -338,7 +340,9 @@ class SpatialNavigationProvider extends Provider {
338
340
  parent.scrollTo({ left: parent.scrollLeft - parentBB.width / 2, behavior: 'auto' });
339
341
  eventHandled = true;
340
342
  }
341
- if (action === 'left' && targetBB.right > parentBB.right && (parent.scrollLeft + parentBB.width) < parent.scrollWidth) {
343
+ if (action === 'left' &&
344
+ targetBB.right > parentBB.right &&
345
+ parent.scrollLeft + parentBB.width < parent.scrollWidth) {
342
346
  parent.scrollTo({ left: parent.scrollLeft + parentBB.width / 2, behavior: 'auto' });
343
347
  eventHandled = true;
344
348
  }
@@ -514,8 +518,8 @@ class SpatialNavigationProvider extends Provider {
514
518
  // Find focusable elements within the current focus area (excluding the already checked focus areas)
515
519
  focusableElements.push(...findFocusable(el, {
516
520
  excludedElements: checkedFocusArea ? [checkedFocusArea] : undefined,
517
- includeSelectors: ['[data-spatial-focusable]'],
518
- excludeSelectors: ['[data-spatial-exclude]'],
521
+ includeSelectors: [`[${DATA_ATTRIBUTES.FOCUSABLE}]`],
522
+ excludeSelectors: [`[${DATA_ATTRIBUTES.EXCLUDE}]`],
519
523
  }));
520
524
  const result = this.focusNextInFocusableAria(focusableElements, direction);
521
525
  // If there is a focusable element found, or reached the active trap or the root, stop searching
@@ -574,18 +578,19 @@ class SpatialNavigationProvider extends Provider {
574
578
  var _a, _b;
575
579
  let currentActiveElement = this.getActiveElement();
576
580
  const currentDomActiveElement = getDomActiveElement();
581
+ let focusableElements = elements;
577
582
  // Sync current active element if necessary
578
583
  // It can be out of sync when:
579
584
  // - the component handled the navigation (programmatically focused another element) so DOM active element is different
580
585
  // - focus fallback to body or other non-focusable element
581
586
  if (!currentActiveElement ||
582
- !elements.includes(currentActiveElement) ||
587
+ !focusableElements.includes(currentActiveElement) ||
583
588
  currentActiveElement !== currentDomActiveElement) {
584
- if (currentDomActiveElement && elements.includes(currentDomActiveElement)) {
589
+ if (currentDomActiveElement && focusableElements.includes(currentDomActiveElement)) {
585
590
  currentActiveElement = currentDomActiveElement;
586
591
  }
587
592
  else {
588
- [currentActiveElement] = elements;
593
+ [currentActiveElement] = focusableElements;
589
594
  }
590
595
  this.setActiveElement(currentActiveElement);
591
596
  }
@@ -600,7 +605,13 @@ class SpatialNavigationProvider extends Provider {
600
605
  const root = elementWithDataset.getRootNode();
601
606
  const nextElement = (_a = root === null || root === void 0 ? void 0 : root.getElementById(nextElementSelector)) !== null && _a !== void 0 ? _a : root === null || root === void 0 ? void 0 : root.querySelector(nextElementSelector);
602
607
  if (nextElement) {
603
- return nextElement;
608
+ const isNextElementInFocusables = focusableElements.includes(nextElement);
609
+ focusableElements = focusableElements.filter(el => nextElement.contains(el) && el);
610
+ if (isNextElementInFocusables || focusableElements.length <= 1) {
611
+ // Use nextElement if it focusable
612
+ return nextElement;
613
+ }
614
+ // Else, fall back to the distance based navigation but search within the targeted element subtree only.
604
615
  }
605
616
  }
606
617
  }
@@ -613,7 +624,7 @@ class SpatialNavigationProvider extends Provider {
613
624
  return undefined;
614
625
  }
615
626
  // Find the closest element in the given direction
616
- const results = orderElementsByDistance(currentActiveElement, elements, direction, this.distanceCalculationWeights);
627
+ const results = orderElementsByDistance(currentActiveElement, focusableElements, direction, this.distanceCalculationWeights);
617
628
  return (_b = results[0]) === null || _b === void 0 ? void 0 : _b.candidate;
618
629
  }
619
630
  /**
@@ -630,7 +641,7 @@ class SpatialNavigationProvider extends Provider {
630
641
  /**
631
642
  * Set the active element.
632
643
  *
633
- * Also, setup MutationObserver to track the element removal from the DOM.
644
+ * Also, set up MutationObserver to track the element removal from the DOM.
634
645
  *
635
646
  * @param element - New active element
636
647
  * @internal
@@ -685,13 +696,12 @@ class SpatialNavigationProvider extends Provider {
685
696
  /**
686
697
  * Handle back action
687
698
  *
688
- * Either trigger click on goBack element if any
689
- * otherwise call default go back handler
699
+ * Either trigger click on the goBack element if any otherwise call the default go back handler
690
700
  *
691
701
  * @returns true when go back handled, false otherwise
692
702
  */
693
703
  goBack() {
694
- const goBackElement = findFocusable(this.root).find(el => el.hasAttribute('data-spatial-go-back'));
704
+ const goBackElement = findFocusable(this.root).find(el => el.hasAttribute(DATA_ATTRIBUTES.GO_BACK));
695
705
  const isDefaultPrevented = this.emitGoBackEvent(goBackElement);
696
706
  if (goBackElement && !isDefaultPrevented) {
697
707
  goBackElement.click();
@@ -1,6 +1,17 @@
1
1
  import { ShortestDistanceWeights, SpatialNavigationActionToKeyMap } from './spatialnavigationprovider.types';
2
2
  export declare const TAG_NAME: "mdc-spatialnavigationprovider";
3
3
  export declare const SPATIAL_NAVIGATION_DIRECTION_KEYS: string[];
4
+ export declare const DATA_ATTRIBUTES: {
5
+ DIRECTION_LEFT: string;
6
+ DIRECTION_RIGHT: string;
7
+ DIRECTION_UP: string;
8
+ DIRECTION_DOWN: string;
9
+ GO_BACK: string;
10
+ FOCUSABLE: string;
11
+ EXCLUDE: string;
12
+ NO_SCROLL: string;
13
+ SCROLL_PARENT: string;
14
+ };
4
15
  export declare const DEFAULTS: {
5
16
  readonly SPATIAL_NAVIGATION_KEY_MAPPING: SpatialNavigationActionToKeyMap;
6
17
  readonly WEIGHTS: ShortestDistanceWeights;
@@ -1,6 +1,17 @@
1
1
  import utils from '../../utils/tag-name';
2
2
  export const TAG_NAME = utils.constructTagName('spatialnavigationprovider');
3
3
  export const SPATIAL_NAVIGATION_DIRECTION_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
4
+ export const DATA_ATTRIBUTES = {
5
+ DIRECTION_LEFT: 'data-spatial-left',
6
+ DIRECTION_RIGHT: 'data-spatial-right',
7
+ DIRECTION_UP: 'data-spatial-up',
8
+ DIRECTION_DOWN: 'data-spatial-down',
9
+ GO_BACK: 'data-spatial-go-back',
10
+ FOCUSABLE: 'data-spatial-focusable',
11
+ EXCLUDE: 'data-spatial-exclude',
12
+ NO_SCROLL: 'data-spatial-noscroll',
13
+ SCROLL_PARENT: 'data-spatial-scroll-parent',
14
+ };
4
15
  export const DEFAULTS = {
5
16
  SPATIAL_NAVIGATION_KEY_MAPPING: {
6
17
  left: 'ArrowLeft',
@@ -1,3 +1,4 @@
1
+ import { DATA_ATTRIBUTES } from './spatialnavigationprovider.constants';
1
2
  /**
2
3
  * Calculate the center point of the element
3
4
  */
@@ -78,7 +79,11 @@ export const orderElementsByDistance = (activeElement, focusableElements, direct
78
79
  // Skip the active element
79
80
  if (candidate === activeElement)
80
81
  return acc;
81
- const cr = getElementRectWithMidPoint(candidate);
82
+ const scrollParent = candidate.closest(`[${DATA_ATTRIBUTES.SCROLL_PARENT}]`);
83
+ // Focusable inside scrollable container can be out of viewport and/or too small to pick up autofocus,
84
+ // so we measure the scroll parent instead.
85
+ const measuredElement = scrollParent && !scrollParent.contains(activeElement) ? scrollParent : candidate;
86
+ const cr = getElementRectWithMidPoint(measuredElement);
82
87
  // Filter out elements that are not in the navigation direction
83
88
  if (direction === 'left' && cr.right > reference.xMid)
84
89
  return acc;
@@ -44925,7 +44925,7 @@
44925
44925
  "declarations": [
44926
44926
  {
44927
44927
  "kind": "class",
44928
- "description": "Spatial navigation focus manager\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\nIt should have only one instance and it should placed at the root of the application.\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\n### Steps\n\nSpatial navigation goes trough the following steps after each keydown:\n\n1. Handle `keydown` in the capture phase.\n - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the\n provider own `keydown` handler (see step 3).\n - When active element's parent is scrollable and it is not fully visible in the given direction and it does not\n have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the\n scroll view.\n2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not\n prevented.\n3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)\n - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle\n the key event itself. If `navbeforeprocess` event is prevented, stop here.\n - If the component did not handle `keydown`, it calculate the next focusable item\n - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.\n - Otherwise calculate the next focused item based on the direction and distances.\n - If there is no next item, it emits `navnotarget` event\n - Otherwise emit `navbeforefocus`,\n - If this event prevented, nothing happens\n - Otherwise the focus moves to the next element\n\n### Determine next focus\n\nThe provider use multiple ways to determine the next focused element. The order defined in the \"Steps\" section.\n\n#### Calculated focus\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\nNote: The algorithm is distance-based, so the UI should be designed to 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#### Overwrite next element\n\nOverride calculated 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` | empty string / id / selector | N/A | Prevent native navigation in Left direction and focus element if exists |\n| `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction and focus element if exists |\n| `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in Right direction and focus element if exists |\n| `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction and focus element if exists |\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| `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |\n| `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 | |\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### Nested providers\n\nOnly one provider instance is supported in the application at a time.",
44928
+ "description": "Spatial navigation focus manager\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\nIt should have only one instance and it should placed at the root of the application.\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\n### Steps\n\nSpatial navigation goes through the following steps after each keydown:\n\n1. Handle `keydown` in the capture phase.\n - When the active element has a `data-spatial-{direction}` attribute, then prevent all component navigation and call the\n provider's own `keydown` handler (see step 3).\n - When the active element's parent is scrollable and it is not fully visible in the given direction, and it does not\n have a `data-spatial-noscroll` attribute, prevent all navigation and scroll in the given direction half-size of the\n scroll view.\n2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not\n prevented.\n3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)\n - If a key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle\n the key event itself. If `navbeforeprocess` event is prevented, stop here.\n - If the component did not handle `keydown`, it calculates the next focusable item\n - if the active element has a `data-spatial-{direction}` attribute, it will try to focus the element with the id.\n - Otherwise calculate the next focused item based on the direction and distances.\n - If there is no next item, it emits `navnotarget` event\n - Otherwise emit `navbeforefocus`,\n - If this event is prevented, nothing happens\n - Otherwise the focus moves to the next element\n\n### Determine next focus\n\nThe provider uses multiple ways to determine the next focused element. The order defined in the \"Steps\" section.\n\n#### Calculated focus\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 on the closest candidate.\n\nElements with `data-spatial-focusable` are treated as focusable even if they do 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\nNote: The algorithm is distance-based, so the UI should be designed to 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#### Overwrite the next element\n\nOverride calculated 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` | 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. |\n| `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. |\n| `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. |\n| `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. |\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| `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |\n| `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 |\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 a 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### Nested providers\n\nOnly one provider instance is supported in the application at a time.",
44929
44929
  "name": "SpatialNavigationProvider",
44930
44930
  "members": [
44931
44931
  {
@@ -44986,7 +44986,7 @@
44986
44986
  "text": ""
44987
44987
  }
44988
44988
  },
44989
- "description": "Handle back action\n\nEither trigger click on goBack element if any\notherwise call default go back handler"
44989
+ "description": "Handle back action\n\nEither trigger click on the goBack element if any otherwise call the default go back handler"
44990
44990
  },
44991
44991
  {
44992
44992
  "kind": "field",
@@ -45087,7 +45087,7 @@
45087
45087
  "module": "/src/models"
45088
45088
  },
45089
45089
  "tagName": "mdc-spatialnavigationprovider",
45090
- "jsDoc": "/**\n * Spatial navigation focus manager\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 * It should have only one instance and it should placed at the root of the application.\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 * ### Steps\n *\n * Spatial navigation goes trough the following steps after each keydown:\n *\n * 1. Handle `keydown` in the capture phase.\n * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the\n * provider own `keydown` handler (see step 3).\n * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not\n * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the\n * scroll view.\n * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not\n * prevented.\n * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)\n * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle\n * the key event itself. If `navbeforeprocess` event is prevented, stop here.\n * - If the component did not handle `keydown`, it calculate the next focusable item\n * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.\n * - Otherwise calculate the next focused item based on the direction and distances.\n * - If there is no next item, it emits `navnotarget` event\n * - Otherwise emit `navbeforefocus`,\n * - If this event prevented, nothing happens\n * - Otherwise the focus moves to the next element\n *\n * ### Determine next focus\n *\n * The provider use multiple ways to determine the next focused element. The order defined in the \"Steps\" section.\n *\n * #### Calculated focus\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 * Note: The algorithm is distance-based, so the UI should be designed to 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 * #### Overwrite next element\n *\n * Override calculated 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` | empty string / id / selector | N/A | Prevent native navigation in Left direction and focus element if exists |\n * | `data-spatial-up` | empty string / id / selector | N/A | Prevent native navigation in Up direction and focus element if exists |\n * | `data-spatial-right` | empty string / id / selector | N/A | Prevent native navigation in Right direction and focus element if exists |\n * | `data-spatial-down` | empty string / id / selector | N/A | Prevent native navigation in Down direction and focus element if exists |\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 * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |\n * | `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 | |\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 * ### 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 */",
45090
+ "jsDoc": "/**\n * Spatial navigation focus manager\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 * It should have only one instance and it should placed at the root of the application.\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 * ### Steps\n *\n * Spatial navigation goes through the following steps after each keydown:\n *\n * 1. Handle `keydown` in the capture phase.\n * - When the active element has a `data-spatial-{direction}` attribute, then prevent all component navigation and call the\n * provider's own `keydown` handler (see step 3).\n * - When the active element's parent is scrollable and it is not fully visible in the given direction, and it does not\n * have a `data-spatial-noscroll` attribute, prevent all navigation and scroll in the given direction half-size of the\n * scroll view.\n * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not\n * prevented.\n * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)\n * - If a key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle\n * the key event itself. If `navbeforeprocess` event is prevented, stop here.\n * - If the component did not handle `keydown`, it calculates the next focusable item\n * - if the active element has a `data-spatial-{direction}` attribute, it will try to focus the element with the id.\n * - Otherwise calculate the next focused item based on the direction and distances.\n * - If there is no next item, it emits `navnotarget` event\n * - Otherwise emit `navbeforefocus`,\n * - If this event is prevented, nothing happens\n * - Otherwise the focus moves to the next element\n *\n * ### Determine next focus\n *\n * The provider uses multiple ways to determine the next focused element. The order defined in the \"Steps\" section.\n *\n * #### Calculated focus\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 on the closest candidate.\n *\n * Elements with `data-spatial-focusable` are treated as focusable even if they do 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 * Note: The algorithm is distance-based, so the UI should be designed to 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 * #### Overwrite the next element\n *\n * Override calculated 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` | 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. |\n * | `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. |\n * | `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. |\n * | `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. |\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 * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for active element in scrollable area even if the is not fit in view |\n * | `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 |\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 a 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 * ### 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 */",
45091
45091
  "customElement": true
45092
45092
  }
45093
45093
  ],