@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.
- package/dist/browser/index.js +2 -2
- package/dist/browser/index.js.map +3 -3
- package/dist/components/popover/popover.component.d.ts +25 -5
- package/dist/components/popover/popover.component.js +40 -19
- package/dist/components/spatialnavigationprovider/spatialnavigationprovider.component.d.ts +59 -21
- package/dist/components/spatialnavigationprovider/spatialnavigationprovider.component.js +96 -31
- package/dist/custom-elements.json +70 -8
- package/dist/react/spatialnavigationprovider/index.d.ts +44 -20
- package/dist/react/spatialnavigationprovider/index.js +44 -20
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
*
|
|
469
|
-
*
|
|
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
|
|
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
|
|
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('
|
|
477
|
-
document.removeEventListener('
|
|
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
|
-
*
|
|
555
|
-
*
|
|
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
|
|
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
|
-
|
|
574
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
82
|
-
*
|
|
83
|
-
* | `data-spatial-left`
|
|
84
|
-
* | `data-spatial-up`
|
|
85
|
-
* | `data-spatial-right`
|
|
86
|
-
* | `data-spatial-down`
|
|
87
|
-
* | `data-spatial-go-back`
|
|
88
|
-
* | `data-spatial-focusable` | N/A
|
|
89
|
-
* | `data-spatial-exclude`
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
98
|
-
*
|
|
99
|
-
* | `data-spatial-left`
|
|
100
|
-
* | `data-spatial-up`
|
|
101
|
-
* | `data-spatial-right`
|
|
102
|
-
* | `data-spatial-down`
|
|
103
|
-
* | `data-spatial-go-back`
|
|
104
|
-
* | `data-spatial-focusable` | N/A
|
|
105
|
-
* | `data-spatial-exclude`
|
|
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
|
-
|
|
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.
|
|
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;
|