Skip to content

Commit 56cf494

Browse files
Add attachTo elements to the keyboard focus flow (#3230)
1 parent d0e4416 commit 56cf494

File tree

8 files changed

+401
-45
lines changed

8 files changed

+401
-45
lines changed

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
nodejs 20.18.2
1+
nodejs 20.19.0
22
pnpm 9.15.4

shepherd.js/rollup.config.mjs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ const plugins = [
5656

5757
// If we are running with --environment DEVELOPMENT, serve via browsersync for local development
5858
if (process.env.DEVELOPMENT) {
59-
plugins.push(
60-
serve({ contentBase: ['.', 'dist', 'dummy'], open: true })
61-
);
59+
plugins.push(serve({ contentBase: ['.', 'dist', 'dummy'], open: true }));
6260
plugins.push(livereload());
6361
}
6462

shepherd.js/src/components/shepherd-element.svelte

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
import { onMount, afterUpdate } from 'svelte';
2+
import { onDestroy, onMount, afterUpdate } from 'svelte';
33
import ShepherdContent from './shepherd-content.svelte';
44
import { isUndefined, isString } from '../utils/type-check.ts';
55
@@ -8,13 +8,20 @@
88
const LEFT_ARROW = 37;
99
const RIGHT_ARROW = 39;
1010
11-
export let classPrefix,
11+
export let attachToElement,
12+
attachTofocusableDialogElements,
13+
classPrefix,
1214
element,
1315
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,
1624
labelId,
17-
lastFocusableElement,
1825
step,
1926
dataStepId;
2027
@@ -33,11 +40,35 @@
3340
onMount(() => {
3441
// Get all elements that are focusable
3542
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);
4172
});
4273
4374
afterUpdate(() => {
@@ -85,23 +116,43 @@
85116
const { tour } = step;
86117
switch (e.keyCode) {
87118
case KEY_TAB:
88-
if (focusableElements.length === 0) {
119+
if (
120+
(!focusableAttachToElements ||
121+
focusableAttachToElements.length === 0) &&
122+
focusableDialogElements.length === 0
123+
) {
89124
e.preventDefault();
90125
break;
91126
}
92127
// Backward tab
93128
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.
94131
if (
95-
document.activeElement === firstFocusableElement ||
132+
document.activeElement === firstFocusableDialogElement ||
96133
document.activeElement.classList.contains('shepherd-element')
97134
) {
98135
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();
100144
}
101145
} 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) {
103154
e.preventDefault();
104-
firstFocusableElement.focus();
155+
firstFocusableDialogElement.focus();
105156
}
106157
}
107158
break;

shepherd.js/src/utils/floating-ui.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ function setPosition(
106106
// Replaces focusAfterRender modifier.
107107
.then((step: Step) => {
108108
if (step?.el) {
109+
step.el.tabIndex = 0;
109110
step.el.focus({ preventScroll: true });
110111
}
111112
})

test/cypress/dummy/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ <h3>Example</h3>
120120
<div>
121121
<img src="sheep.svg" />
122122
</div>
123+
124+
<div id="complex-attach-to" tabindex="0">
125+
<button id="nested-button-1">Button 1</button>
126+
<input id="nested-input" type="text" placeholder="Input" />
127+
<button id="nested-button-2">Button 2</button>
128+
</div>
123129
</div>
124130
</div>
125131

0 commit comments

Comments
 (0)