|
1 | 1 | <script> |
2 | | - import { onMount, afterUpdate } from 'svelte'; |
| 2 | + import { onDestroy, onMount, afterUpdate } from 'svelte'; |
3 | 3 | import ShepherdContent from './shepherd-content.svelte'; |
4 | 4 | import { isUndefined, isString } from '../utils/type-check.ts'; |
5 | 5 |
|
|
8 | 8 | const LEFT_ARROW = 37; |
9 | 9 | const RIGHT_ARROW = 39; |
10 | 10 |
|
11 | | - export let classPrefix, |
| 11 | + export let attachToElement, |
| 12 | + attachTofocusableDialogElements, |
| 13 | + classPrefix, |
12 | 14 | element, |
13 | 15 | descriptionId, |
14 | | - firstFocusableElement, |
15 | | - focusableElements, |
| 16 | + // Focusable attachTo elements |
| 17 | + focusableAttachToElements, |
| 18 | + firstFocusableAttachToElement, |
| 19 | + lastFocusableAttachToElement, |
| 20 | + // Focusable dialog elements |
| 21 | + firstFocusableDialogElement, |
| 22 | + focusableDialogElements, |
| 23 | + lastFocusableDialogElement, |
16 | 24 | labelId, |
17 | | - lastFocusableElement, |
18 | 25 | step, |
19 | 26 | dataStepId; |
20 | 27 |
|
|
33 | 40 | onMount(() => { |
34 | 41 | // Get all elements that are focusable |
35 | 42 | dataStepId = { [`data-${classPrefix}shepherd-step-id`]: step.id }; |
36 | | - focusableElements = element.querySelectorAll( |
37 | | - 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' |
38 | | - ); |
39 | | - firstFocusableElement = focusableElements[0]; |
40 | | - lastFocusableElement = focusableElements[focusableElements.length - 1]; |
| 43 | + focusableDialogElements = [ |
| 44 | + ...element.querySelectorAll( |
| 45 | + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' |
| 46 | + ) |
| 47 | + ]; |
| 48 | + firstFocusableDialogElement = focusableDialogElements[0]; |
| 49 | + lastFocusableDialogElement = |
| 50 | + focusableDialogElements[focusableDialogElements.length - 1]; |
| 51 | +
|
| 52 | + const attachTo = step._getResolvedAttachToOptions(); |
| 53 | + if (attachTo?.element) { |
| 54 | + attachToElement = attachTo.element; |
| 55 | + attachToElement.tabIndex = 0; |
| 56 | + focusableAttachToElements = [ |
| 57 | + attachToElement, |
| 58 | + ...attachToElement.querySelectorAll( |
| 59 | + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' |
| 60 | + ) |
| 61 | + ]; |
| 62 | + firstFocusableAttachToElement = focusableAttachToElements[0]; |
| 63 | + lastFocusableAttachToElement = |
| 64 | + focusableAttachToElements[focusableAttachToElements.length - 1]; |
| 65 | + // Add keydown listener to attachTo element |
| 66 | + attachToElement.addEventListener('keydown', handleKeyDown); |
| 67 | + } |
| 68 | + }); |
| 69 | +
|
| 70 | + onDestroy(() => { |
| 71 | + attachToElement?.removeEventListener('keydown', handleKeyDown); |
41 | 72 | }); |
42 | 73 |
|
43 | 74 | afterUpdate(() => { |
|
85 | 116 | const { tour } = step; |
86 | 117 | switch (e.keyCode) { |
87 | 118 | case KEY_TAB: |
88 | | - if (focusableElements.length === 0) { |
| 119 | + if ( |
| 120 | + (!focusableAttachToElements || |
| 121 | + focusableAttachToElements.length === 0) && |
| 122 | + focusableDialogElements.length === 0 |
| 123 | + ) { |
89 | 124 | e.preventDefault(); |
90 | 125 | break; |
91 | 126 | } |
92 | 127 | // Backward tab |
93 | 128 | if (e.shiftKey) { |
| 129 | + // If at the beginning of elements in the dialog, go to last element in attachTo |
| 130 | + // If attachToElement is undefined, circle around to the last element in the dialog. |
94 | 131 | if ( |
95 | | - document.activeElement === firstFocusableElement || |
| 132 | + document.activeElement === firstFocusableDialogElement || |
96 | 133 | document.activeElement.classList.contains('shepherd-element') |
97 | 134 | ) { |
98 | 135 | e.preventDefault(); |
99 | | - lastFocusableElement.focus(); |
| 136 | + ( |
| 137 | + lastFocusableAttachToElement ?? lastFocusableDialogElement |
| 138 | + ).focus(); |
| 139 | + } |
| 140 | + // If at the beginning of elements in attachTo |
| 141 | + else if (document.activeElement === firstFocusableAttachToElement) { |
| 142 | + e.preventDefault(); |
| 143 | + lastFocusableDialogElement.focus(); |
100 | 144 | } |
101 | 145 | } else { |
102 | | - if (document.activeElement === lastFocusableElement) { |
| 146 | + if (document.activeElement === lastFocusableDialogElement) { |
| 147 | + e.preventDefault(); |
| 148 | + ( |
| 149 | + firstFocusableAttachToElement ?? firstFocusableDialogElement |
| 150 | + ).focus(); |
| 151 | + } |
| 152 | + // If at the end of elements in attachTo |
| 153 | + else if (document.activeElement === lastFocusableAttachToElement) { |
103 | 154 | e.preventDefault(); |
104 | | - firstFocusableElement.focus(); |
| 155 | + firstFocusableDialogElement.focus(); |
105 | 156 | } |
106 | 157 | } |
107 | 158 | break; |
|
0 commit comments