@momentum-design/components 0.134.9 → 0.134.11

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
  };
@@ -1,24 +1,43 @@
1
1
  import { Provider } from '../../models';
2
2
  import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigationActionToKeyMap } from './spatialnavigationprovider.types';
3
3
  /**
4
- * This component manages focus using spatial navigation and provides context for child components.
5
- *
6
- * Place it at the root of the application.
4
+ * Spatial navigation focus manager
7
5
  *
8
6
  * [Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among
9
7
  * elements on a 2D plane, common on TVs and game consoles with remotes or gamepads.
10
8
  *
9
+ * It should have only one instance and it should placed at the root of the application.
10
+ *
11
11
  * ## Focus management
12
12
  *
13
13
  * The provider listens to keyboard events and moves focus among elements based on arrow key input.
14
14
  * You can influence or override this behavior.
15
15
  *
16
- * Note: The algorithm is distance-based, so the UI should be designed so focusable elements are
17
- * predictably reachable. Relative element positions should remain stable; responsive layouts can
18
- * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
19
- * behavior in Storybook when resizing. See the "Limitations" section.
16
+ * ### Steps
20
17
  *
21
- * ### Automatic
18
+ * Spatial navigation goes trough the following steps after each keydown:
19
+ *
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
+ * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
24
+ * prevented.
25
+ * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
26
+ * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
27
+ * the key event itself. If `navbeforeprocess` event is prevented, stop here.
28
+ * - If the component did not handle `keydown`, it calculate the next focusable item
29
+ * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
30
+ * - Otherwise calculate the next focused item based on the direction and distances.
31
+ * - If there is no next item, it emits `navnotarget` event
32
+ * - Otherwise emit `navbeforefocus`,
33
+ * - If this event prevented, nothing happens
34
+ * - Otherwise the focus moves to the next element
35
+ *
36
+ * ### Determine next focus
37
+ *
38
+ * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
39
+ *
40
+ * #### Calculated focus
22
41
  *
23
42
  * By default, the next focus target is computed from element positions:
24
43
  *
@@ -35,9 +54,14 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
35
54
  * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
36
55
  * are focusable.
37
56
  *
38
- * ### Overwrite next element
57
+ * Note: The algorithm is distance-based, so the UI should be designed to focusable elements are
58
+ * predictably reachable. Relative element positions should remain stable; responsive layouts can
59
+ * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
60
+ * behavior in Storybook when resizing. See the "Limitations" section.
61
+ *
62
+ * #### Overwrite next element
39
63
  *
40
- * Override automatic navigation by adding one of these attributes to a focusable element:
64
+ * Override calculated navigation by adding one of these attributes to a focusable element:
41
65
  *
42
66
  * - `data-spatial-up`
43
67
  * - `data-spatial-down`
@@ -46,7 +70,7 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
46
70
  *
47
71
  * Each attribute value must be the id of the element to focus when the corresponding key is pressed.
48
72
  *
49
- * ### Element internal navigation
73
+ * #### Element internal navigation
50
74
  *
51
75
  * Complex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves
52
76
  * focus internally on Down until the last item, after which Down should fall back to provider navigation.
@@ -78,15 +102,15 @@ import { ShortestDistanceWeights, SpatialNavigationContextValue, SpatialNavigati
78
102
  *
79
103
  * Supported data attributes:
80
104
  *
81
- * | Attribute | Value | Default | Description |
82
- * |------------------------|-------------|---------|-------------------------------------------------------------------------------------|
83
- * | `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |
84
- * | `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |
85
- * | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
86
- * | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
87
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
88
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
89
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
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 |
90
114
  *
91
115
  * ## Event emitting order
92
116
  *
@@ -246,7 +270,11 @@ declare class SpatialNavigationProvider extends Provider<SpatialNavigationContex
246
270
  /**
247
271
  * List of navigation keys
248
272
  */
249
- get navigationKeys(): string[];
273
+ isNavigationKey(key: string): boolean;
274
+ /**
275
+ * List of navigation keys
276
+ */
277
+ isDirectionKey(key: string): boolean;
250
278
  constructor();
251
279
  connectedCallback(): void;
252
280
  disconnectedCallback(): void;
@@ -285,6 +313,15 @@ declare class SpatialNavigationProvider extends Provider<SpatialNavigationContex
285
313
  * @internal
286
314
  */
287
315
  private emitNavBeforeFocusEvent;
316
+ /**
317
+ * Check if the current active element has instruction to find the next focusable
318
+ * We look for the element in all the shadow DOMs in the composed path of the active element,
319
+ * so mdc component can use this feature as well.
320
+ *
321
+ * @param currentDomActiveElement - The current active element in the DOM
322
+ * @param direction - Direction
323
+ */
324
+ private getElementIdForDirectionAttr;
288
325
  /**
289
326
  * Focus the next element in the given direction.
290
327
  *
@@ -322,6 +359,7 @@ declare class SpatialNavigationProvider extends Provider<SpatialNavigationContex
322
359
  * @internal
323
360
  */
324
361
  private getActiveElement;
362
+ private handleKeyDownBefore;
325
363
  /**
326
364
  * Handle keydown event
327
365
  *
@@ -17,24 +17,43 @@ import { orderElementsByDistance } from './spatialnavigationprovider.utils';
17
17
  import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
18
18
  // AI-Assisted
19
19
  /**
20
- * This component manages focus using spatial navigation and provides context for child components.
21
- *
22
- * Place it at the root of the application.
20
+ * Spatial navigation focus manager
23
21
  *
24
22
  * [Spatial navigation](https://en.wikipedia.org/wiki/Spatial_navigation) lets users move focus among
25
23
  * elements on a 2D plane, common on TVs and game consoles with remotes or gamepads.
26
24
  *
25
+ * It should have only one instance and it should placed at the root of the application.
26
+ *
27
27
  * ## Focus management
28
28
  *
29
29
  * The provider listens to keyboard events and moves focus among elements based on arrow key input.
30
30
  * You can influence or override this behavior.
31
31
  *
32
- * Note: The algorithm is distance-based, so the UI should be designed so focusable elements are
33
- * predictably reachable. Relative element positions should remain stable; responsive layouts can
34
- * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
35
- * behavior in Storybook when resizing. See the "Limitations" section.
32
+ * ### Steps
33
+ *
34
+ * Spatial navigation goes trough the following steps after each keydown:
36
35
  *
37
- * ### Automatic
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
+ * 2. Component own `keydown` handler executed (bubble phase) (e.g., list moves focus internally) it it was not
40
+ * prevented.
41
+ * 3. Spatial Navigation Provider's `keydown` handler executed (bubble phase)
42
+ * - If key event was not prevented in step 1. emit `navbeforeprocess` to check if any component want to handle
43
+ * the key event itself. If `navbeforeprocess` event is prevented, stop here.
44
+ * - If the component did not handle `keydown`, it calculate the next focusable item
45
+ * - if the active element has `data-spatial-{direction}` attribute, it will try to focus the element with the id.
46
+ * - Otherwise calculate the next focused item based on the direction and distances.
47
+ * - If there is no next item, it emits `navnotarget` event
48
+ * - Otherwise emit `navbeforefocus`,
49
+ * - If this event prevented, nothing happens
50
+ * - Otherwise the focus moves to the next element
51
+ *
52
+ * ### Determine next focus
53
+ *
54
+ * The provider use multiple ways to determine the next focused element. The order defined in the "Steps" section.
55
+ *
56
+ * #### Calculated focus
38
57
  *
39
58
  * By default, the next focus target is computed from element positions:
40
59
  *
@@ -51,9 +70,14 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
51
70
  * Elements with `data-spatial-exclude` are excluded (with its subtree) from the navigation, even if they
52
71
  * are focusable.
53
72
  *
54
- * ### Overwrite next element
73
+ * Note: The algorithm is distance-based, so the UI should be designed to focusable elements are
74
+ * predictably reachable. Relative element positions should remain stable; responsive layouts can
75
+ * make navigation unpredictable. This is less of an issue on fixed-size TV UIs but can show unexpected
76
+ * behavior in Storybook when resizing. See the "Limitations" section.
77
+ *
78
+ * #### Overwrite next element
55
79
  *
56
- * Override automatic navigation by adding one of these attributes to a focusable element:
80
+ * Override calculated navigation by adding one of these attributes to a focusable element:
57
81
  *
58
82
  * - `data-spatial-up`
59
83
  * - `data-spatial-down`
@@ -62,7 +86,7 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
62
86
  *
63
87
  * Each attribute value must be the id of the element to focus when the corresponding key is pressed.
64
88
  *
65
- * ### Element internal navigation
89
+ * #### Element internal navigation
66
90
  *
67
91
  * Complex components (List, Combobox, Tree, etc.) may handle their own navigation. For example, a List moves
68
92
  * focus internally on Down until the last item, after which Down should fall back to provider navigation.
@@ -94,15 +118,15 @@ import { SpatialNavigationEvent } from './spatialnavigationprovider.events';
94
118
  *
95
119
  * Supported data attributes:
96
120
  *
97
- * | Attribute | Value | Default | Description |
98
- * |------------------------|-------------|---------|-------------------------------------------------------------------------------------|
99
- * | `data-spatial-left` | element id | N/A | Focus this element when Left is pressed |
100
- * | `data-spatial-up` | element id | N/A | Focus this element when Up is pressed |
101
- * | `data-spatial-right` | element id | N/A | Focus this element when Right is pressed |
102
- * | `data-spatial-down` | element id | N/A | Focus this element when Down is pressed |
103
- * | `data-spatial-go-back` | N/A | N/A | First focusable element with this attribute is clicked on Back/Escape |
104
- * | `data-spatial-focusable` | N/A | N/A | Treat element as focusable even if it normally is not (e.g., `tabindex="-1"`) |
105
- * | `data-spatial-exclude` | N/A | N/A | Exclude focusable element (and its subtree) from the navigation |
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 |
106
130
  *
107
131
  * ## Event emitting order
108
132
  *
@@ -220,7 +244,7 @@ class SpatialNavigationProvider extends Provider {
220
244
  /**
221
245
  * List of navigation keys
222
246
  */
223
- get navigationKeys() {
247
+ isNavigationKey(key) {
224
248
  return [
225
249
  this.navigationKeyMapping.up,
226
250
  this.navigationKeyMapping.down,
@@ -228,7 +252,18 @@ class SpatialNavigationProvider extends Provider {
228
252
  this.navigationKeyMapping.right,
229
253
  this.navigationKeyMapping.enter,
230
254
  this.navigationKeyMapping.escape,
231
- ];
255
+ ].includes(key);
256
+ }
257
+ /**
258
+ * List of navigation keys
259
+ */
260
+ isDirectionKey(key) {
261
+ return [
262
+ this.navigationKeyMapping.up,
263
+ this.navigationKeyMapping.down,
264
+ this.navigationKeyMapping.left,
265
+ this.navigationKeyMapping.right,
266
+ ].includes(key);
232
267
  }
233
268
  constructor() {
234
269
  // initialize the context by running the Provider constructor:
@@ -266,6 +301,21 @@ class SpatialNavigationProvider extends Provider {
266
301
  * @internal
267
302
  */
268
303
  this.initialHistoryLength = window.history.length;
304
+ this.handleKeyDownBefore = (evt) => {
305
+ if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey || !this.isNavigationKey(evt.key)) {
306
+ return;
307
+ }
308
+ const action = this.context.value.keyToActionMap[evt.key];
309
+ if (this.isDirectionKey(evt.key) &&
310
+ this.getElementIdForDirectionAttr(evt.target, action) !== undefined) {
311
+ // prevent native key events
312
+ evt.preventDefault();
313
+ // prevent MDC component key events
314
+ evt.stopImmediatePropagation();
315
+ // Need to call Spatial navigation key handler manually after all propagation stopped
316
+ this.handleKeyDown(evt);
317
+ }
318
+ };
269
319
  /**
270
320
  * Handle keydown event
271
321
  *
@@ -274,7 +324,7 @@ class SpatialNavigationProvider extends Provider {
274
324
  */
275
325
  this.handleKeyDown = (evt) => {
276
326
  var _a;
277
- if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey || !this.navigationKeys.includes(evt.key)) {
327
+ if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey || !this.isNavigationKey(evt.key)) {
278
328
  return;
279
329
  }
280
330
  const action = this.context.value.keyToActionMap[evt.key];
@@ -352,12 +402,14 @@ class SpatialNavigationProvider extends Provider {
352
402
  }
353
403
  connectedCallback() {
354
404
  super.connectedCallback();
405
+ document.addEventListener('keydown', this.handleKeyDownBefore, { capture: true });
355
406
  document.addEventListener('keydown', this.handleKeyDown);
356
407
  document.addEventListener('focus', this.handleFocus);
357
408
  this.initActiveElement();
358
409
  }
359
410
  disconnectedCallback() {
360
411
  super.disconnectedCallback();
412
+ document.removeEventListener('keydown', this.handleKeyDownBefore, { capture: true });
361
413
  document.removeEventListener('keydown', this.handleKeyDown);
362
414
  document.removeEventListener('focus', this.handleFocus);
363
415
  this.activeElement = undefined;
@@ -461,6 +513,19 @@ class SpatialNavigationProvider extends Provider {
461
513
  }
462
514
  return false;
463
515
  }
516
+ /**
517
+ * Check if the current active element has instruction to find the next focusable
518
+ * We look for the element in all the shadow DOMs in the composed path of the active element,
519
+ * so mdc component can use this feature as well.
520
+ *
521
+ * @param currentDomActiveElement - The current active element in the DOM
522
+ * @param direction - Direction
523
+ */
524
+ getElementIdForDirectionAttr(currentDomActiveElement, direction) {
525
+ var _a;
526
+ const dataAttrName = `data-spatial-${direction}`;
527
+ return (_a = currentDomActiveElement === null || currentDomActiveElement === void 0 ? void 0 : currentDomActiveElement.getAttribute(dataAttrName)) !== null && _a !== void 0 ? _a : undefined;
528
+ }
464
529
  /**
465
530
  * Focus the next element in the given direction.
466
531
  *
@@ -471,10 +536,6 @@ class SpatialNavigationProvider extends Provider {
471
536
  */
472
537
  focusNextInFocusableAria(elements, direction) {
473
538
  var _a, _b;
474
- // Do nothing when there is no focusable element
475
- if (elements.length === 0) {
476
- return undefined;
477
- }
478
539
  let currentActiveElement = this.getActiveElement();
479
540
  const currentDomActiveElement = getDomActiveElement();
480
541
  // Sync current active element if necessary
@@ -492,10 +553,6 @@ class SpatialNavigationProvider extends Provider {
492
553
  }
493
554
  this.setActiveElement(currentActiveElement);
494
555
  }
495
- // If DOM active element is not in the ficus area then move focus back to the current active element
496
- if (currentActiveElement !== getDomActiveElement()) {
497
- return currentActiveElement;
498
- }
499
556
  // Check if the current active element has instruction to find the next focusable
500
557
  // We look for the element in all the shadow DOMs in the composed path of the active element,
501
558
  // so mdc component can use this feature as well.
@@ -510,6 +567,14 @@ class SpatialNavigationProvider extends Provider {
510
567
  }
511
568
  }
512
569
  }
570
+ // If DOM active element is not in the focus area then move focus back to the current active element
571
+ if (currentActiveElement !== getDomActiveElement()) {
572
+ return currentActiveElement;
573
+ }
574
+ // Do nothing when there is no focusable element
575
+ if (elements.length === 0) {
576
+ return undefined;
577
+ }
513
578
  // Find the closest element in the given direction
514
579
  const results = orderElementsByDistance(currentActiveElement, elements, direction, this.distanceCalculationWeights);
515
580
  return (_b = results[0]) === null || _b === void 0 ? void 0 : _b.candidate;