@momentum-design/components 0.134.10 → 0.134.12

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.
@@ -415,8 +415,16 @@ declare class Popover extends Popover_base implements StackedOverlayComponent {
415
415
  * Sets up the trigger related event listeners, based on the trigger type.
416
416
  * Includes fallback for mouseenter trigger to also handle focusin for non-interactive popovers.
417
417
  *
418
- * We are using capture phase for to make sure we capture trigger events even when they are not propagated during the
419
- * bubble phase (e.g.: buttons in list item)
418
+ * We use capture phase on `document` for all trigger listeners to make sure we capture trigger
419
+ * events even when they are not propagated during the bubble phase (e.g.: buttons in list item).
420
+ *
421
+ * Hover is detected via `mouseover`/`mouseout` (not `mouseenter`/`mouseleave`). The latter are
422
+ * spec'd as `composed: false` and do not bubble, so a document-level listener never sees them
423
+ * when the trigger lives inside a shadow root (e.g. wrapped in `mdc-iconprovider` or
424
+ * `mdc-themeprovider`). `mouseover`/`mouseout` are `composed: true` and bubble, so they cross
425
+ * shadow boundaries and reach `document`, letting us keep event delegation (no direct reference
426
+ * to the trigger element and no dependency on its mount/unmount lifecycle). The handlers filter
427
+ * out movements that stay within the trigger's subtree, recreating enter/leave semantics.
420
428
  * @internal
421
429
  */
422
430
  private setupTriggerListeners;
@@ -465,13 +473,25 @@ declare class Popover extends Popover_base implements StackedOverlayComponent {
465
473
  */
466
474
  protected isOpenUpdated(oldValue: boolean, newValue: boolean): Promise<void>;
467
475
  /**
468
- * Handles mouse enter event on the trigger element.
469
- * This method sets the `isHovered` flag to true and shows the popover
476
+ * Determines whether a delegated `mouseover`/`mouseout` event is movement that stays within the
477
+ * trigger's subtree rather than a genuine enter/leave of the trigger.
478
+ *
479
+ * Both events fire repeatedly while the pointer moves between elements inside the trigger
480
+ * (e.g. an icon with internal SVG elements). `relatedTarget` is the element on the other side of
481
+ * the boundary (the element being left for `mouseover`, or entered for `mouseout`); if it
482
+ * resolves to the trigger or one of its shadow hosts, the pointer has not crossed the trigger
483
+ * boundary and the event should be ignored.
484
+ * @internal
485
+ */
486
+ private isHoverWithinTrigger;
487
+ /**
488
+ * Handles the pointer moving over the trigger element (delegated `mouseover`).
489
+ * This method sets the `isHovered` flag to true and shows the popover.
470
490
  * @internal
471
491
  */
472
492
  private handleMouseEnter;
473
493
  /**
474
- * Handles mouse leave event on the trigger element.
494
+ * Handles the pointer leaving the trigger element (delegated `mouseout`).
475
495
  * This method sets the `isHovered` flag to false and starts the close delay
476
496
  * timer to hide the popover.
477
497
  * @internal
@@ -437,8 +437,16 @@ class Popover extends KeyDownHandledMixin(KeyToActionMixin(BackdropMixin(Prevent
437
437
  * Sets up the trigger related event listeners, based on the trigger type.
438
438
  * Includes fallback for mouseenter trigger to also handle focusin for non-interactive popovers.
439
439
  *
440
- * We are using capture phase for to make sure we capture trigger events even when they are not propagated during the
441
- * bubble phase (e.g.: buttons in list item)
440
+ * We use capture phase on `document` for all trigger listeners to make sure we capture trigger
441
+ * events even when they are not propagated during the bubble phase (e.g.: buttons in list item).
442
+ *
443
+ * Hover is detected via `mouseover`/`mouseout` (not `mouseenter`/`mouseleave`). The latter are
444
+ * spec'd as `composed: false` and do not bubble, so a document-level listener never sees them
445
+ * when the trigger lives inside a shadow root (e.g. wrapped in `mdc-iconprovider` or
446
+ * `mdc-themeprovider`). `mouseover`/`mouseout` are `composed: true` and bubble, so they cross
447
+ * shadow boundaries and reach `document`, letting us keep event delegation (no direct reference
448
+ * to the trigger element and no dependency on its mount/unmount lifecycle). The handlers filter
449
+ * out movements that stay within the trigger's subtree, recreating enter/leave semantics.
442
450
  * @internal
443
451
  */
444
452
  this.setupTriggerListeners = () => {
@@ -451,10 +459,10 @@ class Popover extends KeyDownHandledMixin(KeyToActionMixin(BackdropMixin(Prevent
451
459
  if (this.trigger.includes('mouseenter')) {
452
460
  const hoverBridge = this.renderRoot.querySelector('div[part="popover-hover-bridge"]');
453
461
  hoverBridge === null || hoverBridge === void 0 ? void 0 : hoverBridge.addEventListener('mouseenter', this.show);
454
- document.addEventListener('mouseenter', this.handleMouseEnter, { capture: true });
455
- document.addEventListener('mouseleave', this.handleMouseLeave, { capture: true });
456
462
  this.addEventListener('mouseenter', this.cancelCloseDelay);
457
463
  this.addEventListener('mouseleave', this.startCloseDelay);
464
+ document.addEventListener('mouseover', this.handleMouseEnter, { capture: true });
465
+ document.addEventListener('mouseout', this.handleMouseLeave, { capture: true });
458
466
  }
459
467
  if (this.trigger.includes('focusin')) {
460
468
  document.addEventListener('focusin', this.handleFocusIn, { capture: true });
@@ -473,8 +481,8 @@ class Popover extends KeyDownHandledMixin(KeyToActionMixin(BackdropMixin(Prevent
473
481
  // mouseenter trigger
474
482
  const hoverBridge = this.renderRoot.querySelector('div[part="popover-hover-bridge"]');
475
483
  hoverBridge === null || hoverBridge === void 0 ? void 0 : hoverBridge.removeEventListener('mouseenter', this.show);
476
- document.removeEventListener('mouseenter', this.handleMouseEnter, { capture: true });
477
- document.removeEventListener('mouseleave', this.handleMouseLeave, { capture: true });
484
+ document.removeEventListener('mouseover', this.handleMouseEnter, { capture: true });
485
+ document.removeEventListener('mouseout', this.handleMouseLeave, { capture: true });
478
486
  this.removeEventListener('mouseenter', this.cancelCloseDelay);
479
487
  this.removeEventListener('mouseleave', this.startCloseDelay);
480
488
  // focusin trigger
@@ -551,18 +559,39 @@ class Popover extends KeyDownHandledMixin(KeyToActionMixin(BackdropMixin(Prevent
551
559
  }
552
560
  };
553
561
  /**
554
- * Handles mouse enter event on the trigger element.
555
- * This method sets the `isHovered` flag to true and shows the popover
562
+ * Determines whether a delegated `mouseover`/`mouseout` event is movement that stays within the
563
+ * trigger's subtree rather than a genuine enter/leave of the trigger.
564
+ *
565
+ * Both events fire repeatedly while the pointer moves between elements inside the trigger
566
+ * (e.g. an icon with internal SVG elements). `relatedTarget` is the element on the other side of
567
+ * the boundary (the element being left for `mouseover`, or entered for `mouseout`); if it
568
+ * resolves to the trigger or one of its shadow hosts, the pointer has not crossed the trigger
569
+ * boundary and the event should be ignored.
570
+ * @internal
571
+ */
572
+ this.isHoverWithinTrigger = (event) => {
573
+ const { triggerElement } = this;
574
+ const { relatedTarget } = event;
575
+ if (triggerElement && relatedTarget instanceof Element) {
576
+ return getHostComposePath(relatedTarget).includes(triggerElement);
577
+ }
578
+ return false;
579
+ };
580
+ /**
581
+ * Handles the pointer moving over the trigger element (delegated `mouseover`).
582
+ * This method sets the `isHovered` flag to true and shows the popover.
556
583
  * @internal
557
584
  */
558
585
  this.handleMouseEnter = (event) => {
559
586
  if (!this.isEventFromTrigger(event))
560
587
  return;
588
+ if (this.isHoverWithinTrigger(event))
589
+ return;
561
590
  this.isHovered = true;
562
591
  this.show();
563
592
  };
564
593
  /**
565
- * Handles mouse leave event on the trigger element.
594
+ * Handles the pointer leaving the trigger element (delegated `mouseout`).
566
595
  * This method sets the `isHovered` flag to false and starts the close delay
567
596
  * timer to hide the popover.
568
597
  * @internal
@@ -570,16 +599,8 @@ class Popover extends KeyDownHandledMixin(KeyToActionMixin(BackdropMixin(Prevent
570
599
  this.handleMouseLeave = (event) => {
571
600
  if (!this.isEventFromTrigger(event))
572
601
  return;
573
- // When the trigger contains shadow DOM children (e.g. an icon with internal SVG elements),
574
- // mouseleave fires on internal elements as the mouse moves between them.
575
- // Only close if the mouse has actually left the trigger element.
576
- const mouseEvent = event;
577
- const { triggerElement } = this;
578
- if (triggerElement && mouseEvent.relatedTarget instanceof Element) {
579
- if (getHostComposePath(mouseEvent.relatedTarget).includes(triggerElement)) {
580
- return;
581
- }
582
- }
602
+ if (this.isHoverWithinTrigger(event))
603
+ return;
583
604
  this.isHovered = false;
584
605
  this.startCloseDelay();
585
606
  };
@@ -18,8 +18,11 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
18
18
  * Spatial navigation goes trough 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).
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
25
+ * scroll view.
23
26
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
24
27
  * prevented.
25
28
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
@@ -102,15 +105,16 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
102
105
  *
103
106
  * Supported data attributes:
104
107
  *
105
- * | Attribute | Value | Default | Description |
106
- * |--------------------------|---------------------------|---------|-------------------------------------------------------------------------------|
107
- * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
108
- * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
109
- * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
110
- * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
111
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
112
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
113
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
108
+ * | Attribute | Value | Default | Description |
109
+ * |--------------------------|---------------------------|---------|------------------------------------------------------------------------------------|
110
+ * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
111
+ * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
112
+ * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
113
+ * | `data-spatial-down` | empty string / element id | 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 ative element in scrollable area even if the is not fit in view |
114
118
  *
115
119
  * ## Event emitting order
116
120
  *
@@ -202,12 +206,6 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
202
206
  * - Avoid nested focusable elements where possible.
203
207
  * - Tune algorithm weights to match your UI layout.
204
208
  *
205
- * ### Scrollable containers
206
- *
207
- * Content scrolling is not supported yet, e.g.:
208
- * - Focused element larger than the viewport.
209
- * - Scrollable content without interactive children.
210
- *
211
209
  * ### Nested providers
212
210
  *
213
211
  * Only one provider instance is supported in the application at a time.
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { Provider } from '../../models';
12
- import { findFocusable, getDomActiveElement, getHostComposePath, isScrollable } from '../../utils/dom';
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
15
  import { DEFAULTS } from './spatialnavigationprovider.constants';
@@ -34,8 +34,11 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
34
34
  * Spatial navigation goes trough 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).
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
41
+ * scroll view.
39
42
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
40
43
  * prevented.
41
44
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
@@ -118,15 +121,16 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
118
121
  *
119
122
  * Supported data attributes:
120
123
  *
121
- * | Attribute | Value | Default | Description |
122
- * |--------------------------|---------------------------|---------|-------------------------------------------------------------------------------|
123
- * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
124
- * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
125
- * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
126
- * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
127
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
128
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
129
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
124
+ * | Attribute | Value | Default | Description |
125
+ * |--------------------------|---------------------------|---------|------------------------------------------------------------------------------------|
126
+ * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
127
+ * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
128
+ * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
129
+ * | `data-spatial-down` | empty string / element id | 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 ative element in scrollable area even if the is not fit in view |
130
134
  *
131
135
  * ## Event emitting order
132
136
  *
@@ -218,12 +222,6 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
218
222
  * - Avoid nested focusable elements where possible.
219
223
  * - Tune algorithm weights to match your UI layout.
220
224
  *
221
- * ### Scrollable containers
222
- *
223
- * Content scrolling is not supported yet, e.g.:
224
- * - Focused element larger than the viewport.
225
- * - Scrollable content without interactive children.
226
- *
227
225
  * ### Nested providers
228
226
  *
229
227
  * Only one provider instance is supported in the application at a time.
@@ -305,15 +303,51 @@ class SpatialNavigationProvider extends Provider {
305
303
  if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey || !this.isNavigationKey(evt.key)) {
306
304
  return;
307
305
  }
306
+ let eventHandled = false;
307
+ const target = evt.target;
308
308
  const action = this.context.value.keyToActionMap[evt.key];
309
309
  if (this.isDirectionKey(evt.key) &&
310
- this.getElementIdForDirectionAttr(evt.target, action) !== undefined) {
310
+ this.getElementIdForDirectionAttr(target, action) !== undefined) {
311
+ eventHandled = true;
312
+ // Need to call Spatial navigation key handler manually after all propagation stopped
313
+ this.handleKeyDown(evt);
314
+ }
315
+ // Handle over sized elements inside scrollable area
316
+ if (target.parentElement && !target.hasAttribute('data-spatial-noscroll')) {
317
+ const parent = target.parentElement;
318
+ const targetScrollAxis = getScrollableAxis(parent);
319
+ if (targetScrollAxis) {
320
+ const targetBB = target.getBoundingClientRect();
321
+ const parentBB = parent.getBoundingClientRect();
322
+ // Vertical scrolling
323
+ if (targetScrollAxis === 'vertical' || targetScrollAxis === 'both') {
324
+ if (action === 'up' && targetBB.top < parentBB.top && parent.scrollTop > 0) {
325
+ parent.scrollTo({ top: parent.scrollTop - parentBB.height / 2, behavior: 'auto' });
326
+ eventHandled = true;
327
+ }
328
+ if (action === 'down' && targetBB.bottom > parentBB.bottom && (parent.scrollTop + parentBB.height) < parent.scrollHeight) {
329
+ parent.scrollTo({ top: parent.scrollTop + parentBB.height / 2, behavior: 'auto' });
330
+ eventHandled = true;
331
+ }
332
+ }
333
+ // horizontal scrolling
334
+ if (targetScrollAxis === 'horizontal' || targetScrollAxis === 'both') {
335
+ if (action === 'right' && targetBB.left < parentBB.left && parent.scrollLeft > 0) {
336
+ parent.scrollTo({ left: parent.scrollLeft - parentBB.width / 2, behavior: 'auto' });
337
+ eventHandled = true;
338
+ }
339
+ if (action === 'left' && targetBB.right > parentBB.right && (parent.scrollLeft + parentBB.width) < parent.scrollWidth) {
340
+ parent.scrollTo({ left: parent.scrollLeft + parentBB.width / 2, behavior: 'auto' });
341
+ eventHandled = true;
342
+ }
343
+ }
344
+ }
345
+ }
346
+ if (eventHandled) {
311
347
  // prevent native key events
312
348
  evt.preventDefault();
313
349
  // prevent MDC component key events
314
350
  evt.stopImmediatePropagation();
315
- // Need to call Spatial navigation key handler manually after all propagation stopped
316
- this.handleKeyDown(evt);
317
351
  }
318
352
  };
319
353
  /**
@@ -474,7 +508,7 @@ class SpatialNavigationProvider extends Provider {
474
508
  path = path.includes(this.root) ? path : [this.root];
475
509
  // Walk through the composed path to find the focus areas (scrollable or focus trap)
476
510
  for (const el of path) {
477
- if (el === activeTrap || el === this.root || isScrollable(el)) {
511
+ if (el === activeTrap || el === this.root || getScrollableAxis(el)) {
478
512
  // Find focusable elements within the current focus area (excluding the already checked focus areas)
479
513
  focusableElements.push(...findFocusable(el, {
480
514
  excludedElements: checkedFocusArea ? [checkedFocusArea] : undefined,
@@ -173,8 +173,8 @@
173
173
  "attribute": "disabled",
174
174
  "reflects": true,
175
175
  "inheritedFrom": {
176
- "name": "AccordionButton",
177
- "module": "components/accordionbutton/accordionbutton.component.js"
176
+ "name": "DisabledMixin",
177
+ "module": "utils/mixins/DisabledMixin.js"
178
178
  }
179
179
  },
180
180
  {
@@ -493,8 +493,8 @@
493
493
  "default": "undefined",
494
494
  "fieldName": "disabled",
495
495
  "inheritedFrom": {
496
- "name": "AccordionButton",
497
- "module": "src/components/accordionbutton/accordionbutton.component.ts"
496
+ "name": "DisabledMixin",
497
+ "module": "src/utils/mixins/DisabledMixin.ts"
498
498
  }
499
499
  },
500
500
  {
@@ -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).\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 / element id | N/A | Prevent native navigation in Left direction and focus element if exists |\n| `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |\n| `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |\n| `data-spatial-down` | empty string / element id | 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\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.",
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 / element id | N/A | Prevent native navigation in Left direction and focus element if exists |\n| `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |\n| `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |\n| `data-spatial-down` | empty string / element id | 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 ative element in scrollable area even if the is not fit in view |\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.",
44929
44929
  "name": "SpatialNavigationProvider",
44930
44930
  "members": [
44931
44931
  {
@@ -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 * 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 / element id | N/A | Prevent native navigation in Left direction and focus element if exists |\n * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |\n * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |\n * | `data-spatial-down` | empty string / element id | 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 *\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 */",
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 / element id | N/A | Prevent native navigation in Left direction and focus element if exists |\n * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |\n * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |\n * | `data-spatial-down` | empty string / element id | 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 ative element in scrollable area even if the is not fit in view |\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 */",
45091
45091
  "customElement": true
45092
45092
  }
45093
45093
  ],
@@ -19,8 +19,11 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
19
19
  * Spatial navigation goes trough the following steps after each keydown:
20
20
  *
21
21
  * 1. Handle `keydown` in the capture phase.
22
- * When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
23
- * provider own `keydown` handler (see step 3).
22
+ * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
23
+ * provider own `keydown` handler (see step 3).
24
+ * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not
25
+ * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the
26
+ * scroll view.
24
27
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
25
28
  * prevented.
26
29
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
@@ -103,15 +106,16 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
103
106
  *
104
107
  * Supported data attributes:
105
108
  *
106
- * | Attribute | Value | Default | Description |
107
- * |--------------------------|---------------------------|---------|-------------------------------------------------------------------------------|
108
- * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
109
- * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
110
- * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
111
- * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
112
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
113
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
114
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
109
+ * | Attribute | Value | Default | Description |
110
+ * |--------------------------|---------------------------|---------|------------------------------------------------------------------------------------|
111
+ * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
112
+ * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
113
+ * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
114
+ * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
115
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
116
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
117
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
118
+ * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for ative element in scrollable area even if the is not fit in view |
115
119
  *
116
120
  * ## Event emitting order
117
121
  *
@@ -203,12 +207,6 @@ import type { Events } from '../../components/spatialnavigationprovider/spatialn
203
207
  * - Avoid nested focusable elements where possible.
204
208
  * - Tune algorithm weights to match your UI layout.
205
209
  *
206
- * ### Scrollable containers
207
- *
208
- * Content scrolling is not supported yet, e.g.:
209
- * - Focused element larger than the viewport.
210
- * - Scrollable content without interactive children.
211
- *
212
210
  * ### Nested providers
213
211
  *
214
212
  * Only one provider instance is supported in the application at a time.
@@ -20,8 +20,11 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
20
20
  * Spatial navigation goes trough the following steps after each keydown:
21
21
  *
22
22
  * 1. Handle `keydown` in the capture phase.
23
- * When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
24
- * provider own `keydown` handler (see step 3).
23
+ * - When active element has `data-spatial-{direction}` attribute then prevent all component navigation and call the
24
+ * provider own `keydown` handler (see step 3).
25
+ * - When active element's parent is scrollable and it is not fully visible in the given direction and it does not
26
+ * have `data-spatial-noscroll` attribute, prevent all navigation and scroll in the give direction half size of the
27
+ * scroll view.
25
28
  * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
26
29
  * prevented.
27
30
  * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
@@ -104,15 +107,16 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
104
107
  *
105
108
  * Supported data attributes:
106
109
  *
107
- * | Attribute | Value | Default | Description |
108
- * |--------------------------|---------------------------|---------|-------------------------------------------------------------------------------|
109
- * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
110
- * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
111
- * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
112
- * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
113
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
114
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
115
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
110
+ * | Attribute | Value | Default | Description |
111
+ * |--------------------------|---------------------------|---------|------------------------------------------------------------------------------------|
112
+ * | `data-spatial-left` | empty string / element id | N/A | Prevent native navigation in Left direction and focus element if exists |
113
+ * | `data-spatial-up` | empty string / element id | N/A | Prevent native navigation in Up direction and focus element if exists |
114
+ * | `data-spatial-right` | empty string / element id | N/A | Prevent native navigation in Right direction and focus element if exists |
115
+ * | `data-spatial-down` | empty string / element id | N/A | Prevent native navigation in Down direction and focus element if exists |
116
+ * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
117
+ * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
118
+ * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
119
+ * | `data-spatial-noscroll` | N/A | N/A | Prevent scroll for ative element in scrollable area even if the is not fit in view |
116
120
  *
117
121
  * ## Event emitting order
118
122
  *
@@ -204,12 +208,6 @@ import { TAG_NAME } from '../../components/spatialnavigationprovider/spatialnavi
204
208
  * - Avoid nested focusable elements where possible.
205
209
  * - Tune algorithm weights to match your UI layout.
206
210
  *
207
- * ### Scrollable containers
208
- *
209
- * Content scrolling is not supported yet, e.g.:
210
- * - Focused element larger than the viewport.
211
- * - Scrollable content without interactive children.
212
- *
213
211
  * ### Nested providers
214
212
  *
215
213
  * Only one provider instance is supported in the application at a time.
@@ -64,7 +64,7 @@ export declare const hasZeroDimensions: (element: HTMLElement) => boolean;
64
64
  * @param element - The element to check.
65
65
  * @returns True if the element is scrollable.
66
66
  */
67
- export declare const isScrollable: (element: HTMLElement) => boolean;
67
+ export declare const getScrollableAxis: (element: HTMLElement) => null | "horizontal" | "vertical" | "both";
68
68
  /**
69
69
  * Determines if the element is not visible in the DOM.
70
70
  *
package/dist/utils/dom.js CHANGED
@@ -49,13 +49,20 @@ export const hasZeroDimensions = (element) => {
49
49
  * @param element - The element to check.
50
50
  * @returns True if the element is scrollable.
51
51
  */
52
- export const isScrollable = (element) => {
52
+ export const getScrollableAxis = (element) => {
53
53
  if (!(element instanceof Element))
54
- return false;
54
+ return null;
55
55
  const computedStyle = getComputedStyle(element);
56
56
  const { overflowX, overflowY } = computedStyle;
57
- // Check if overflow is set to scrollable values
58
- return overflowX === 'auto' || overflowX === 'scroll' || overflowY === 'auto' || overflowY === 'scroll';
57
+ const horizontal = overflowX === 'auto' || overflowX === 'scroll';
58
+ const vertical = overflowY === 'auto' || overflowY === 'scroll';
59
+ if (horizontal && vertical)
60
+ return 'both';
61
+ if (horizontal)
62
+ return 'horizontal';
63
+ if (vertical)
64
+ return 'vertical';
65
+ return null;
59
66
  };
60
67
  /**
61
68
  * Determines if the element is not visible in the DOM.