@pzerelles/headlessui-svelte 2.1.2-next.7 → 2.1.2-next.9
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/button/Button.svelte +54 -84
- package/dist/checkbox/Checkbox.svelte +120 -174
- package/dist/checkbox/Checkbox.svelte.d.ts +4 -6
- package/dist/close-button/CloseButton.svelte +6 -12
- package/dist/combobox/Combobox.svelte +3 -50
- package/dist/data-interactive/DataInteractive.svelte +29 -57
- package/dist/description/Description.svelte +21 -32
- package/dist/dialog/Dialog.svelte +34 -69
- package/dist/dialog/DialogBackdrop.svelte +12 -29
- package/dist/dialog/DialogPanel.svelte +26 -49
- package/dist/dialog/DialogTitle.svelte +23 -38
- package/dist/dialog/InternalDialog.svelte +202 -263
- package/dist/field/Field.svelte +25 -49
- package/dist/fieldset/Fieldset.svelte +29 -50
- package/dist/focus-trap/FocusTrap.svelte +283 -419
- package/dist/input/Input.svelte +53 -84
- package/dist/internal/FocusSentinel.svelte +8 -16
- package/dist/internal/ForcePortalRoot.svelte +3 -7
- package/dist/internal/FormFields.svelte +20 -31
- package/dist/internal/FormResolver.svelte +15 -20
- package/dist/internal/Hidden.svelte +23 -44
- package/dist/internal/HoistFormFields.svelte +4 -7
- package/dist/internal/MainTreeProvider.svelte +36 -89
- package/dist/internal/Portal.svelte +14 -18
- package/dist/label/Label.svelte +58 -91
- package/dist/legend/Legend.svelte +3 -18
- package/dist/listbox/Listbox.svelte +396 -588
- package/dist/listbox/ListboxButton.svelte +127 -176
- package/dist/listbox/ListboxButton.svelte.d.ts +4 -6
- package/dist/listbox/ListboxOption.svelte +125 -166
- package/dist/listbox/ListboxOptions.svelte +244 -340
- package/dist/listbox/ListboxSelectedOption.svelte +15 -38
- package/dist/menu/Menu.svelte +218 -307
- package/dist/menu/MenuButton.svelte +115 -157
- package/dist/menu/MenuHeading.svelte +14 -34
- package/dist/menu/MenuItem.svelte +107 -145
- package/dist/menu/MenuItems.svelte +224 -298
- package/dist/menu/MenuSection.svelte +9 -26
- package/dist/menu/MenuSeparator.svelte +4 -20
- package/dist/portal/InternalPortal.svelte +85 -141
- package/dist/portal/Portal.svelte +2 -5
- package/dist/portal/PortalGroup.svelte +9 -30
- package/dist/switch/Switch.svelte +132 -179
- package/dist/switch/SwitchGroup.svelte +31 -44
- package/dist/tabs/Tab.svelte +143 -195
- package/dist/tabs/TabGroup.svelte +205 -292
- package/dist/tabs/TabList.svelte +11 -31
- package/dist/tabs/TabPanel.svelte +43 -68
- package/dist/tabs/TabPanels.svelte +7 -18
- package/dist/textarea/Textarea.svelte +53 -84
- package/dist/transition/InternalTransitionChild.svelte +170 -259
- package/dist/transition/Transition.svelte +66 -96
- package/dist/transition/TransitionChild.svelte +11 -31
- package/dist/utils/ElementOrComponent.svelte +23 -44
- package/dist/utils/Generic.svelte +17 -29
- package/dist/utils/StableCollection.svelte +36 -54
- package/dist/utils/id.d.ts +1 -1
- package/dist/utils/id.js +1 -1
- package/package.json +12 -12
- package/dist/internal/id.d.ts +0 -8
- package/dist/internal/id.js +0 -11
|
@@ -1,443 +1,307 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// List of containers
|
|
23
|
-
| Iterable<HTMLElement>
|
|
24
|
-
|
|
25
|
-
function resolveContainers(containers?: Containers): Set<HTMLElement> {
|
|
26
|
-
if (!containers) return new Set<HTMLElement>()
|
|
27
|
-
if (typeof containers === "function") return new Set(containers())
|
|
28
|
-
|
|
29
|
-
let all = new Set<HTMLElement>()
|
|
30
|
-
for (let container of containers) {
|
|
31
|
-
if (container instanceof HTMLElement) {
|
|
32
|
-
all.add(container)
|
|
33
|
-
}
|
|
1
|
+
<script lang="ts" module>import { getOwnerDocument } from "../utils/owner.js";
|
|
2
|
+
import { history } from "../utils/active-element-history.js";
|
|
3
|
+
import { useWatch } from "../hooks/use-watch.svelte.js";
|
|
4
|
+
import { microTask } from "../utils/microTask.js";
|
|
5
|
+
import { Focus, focusElement, focusIn, FocusResult } from "../utils/focus-management.js";
|
|
6
|
+
import { useIsTopLayer } from "../hooks/use-is-top-layer.svelte.js";
|
|
7
|
+
import { useIsMounted } from "../hooks/use-is-mounted.svelte.js";
|
|
8
|
+
import { useEventListener } from "../hooks/use-event-listener.svelte.js";
|
|
9
|
+
import { useTabDirection, Direction as TabDirection } from "../hooks/use-tab-direction.svelte.js";
|
|
10
|
+
import { match } from "../utils/match.js";
|
|
11
|
+
import { useDisposables } from "../utils/disposables.js";
|
|
12
|
+
import Hidden, { HiddenFeatures } from "../internal/Hidden.svelte";
|
|
13
|
+
import ElementOrComponent from "../utils/ElementOrComponent.svelte";
|
|
14
|
+
import { FocusTrapFeatures } from "./FocusTrapFeatures.js";
|
|
15
|
+
function resolveContainers(containers) {
|
|
16
|
+
if (!containers) return /* @__PURE__ */ new Set();
|
|
17
|
+
if (typeof containers === "function") return new Set(containers());
|
|
18
|
+
let all = /* @__PURE__ */ new Set();
|
|
19
|
+
for (let container of containers) {
|
|
20
|
+
if (container instanceof HTMLElement) {
|
|
21
|
+
all.add(container);
|
|
34
22
|
}
|
|
35
|
-
return all
|
|
36
23
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
24
|
+
return all;
|
|
25
|
+
}
|
|
26
|
+
let DEFAULT_FOCUS_TRAP_TAG = "div";
|
|
27
|
+
export * from "./FocusTrapFeatures.js";
|
|
28
|
+
function useRestoreElement(options) {
|
|
29
|
+
const { enabled } = $derived(options ?? { enabled: true });
|
|
30
|
+
let localHistory = $state(history.slice());
|
|
31
|
+
useWatch({
|
|
32
|
+
action: ([newEnabled], [oldEnabled]) => {
|
|
33
|
+
if (oldEnabled === true && newEnabled === false) {
|
|
34
|
+
microTask(() => {
|
|
35
|
+
localHistory.splice(0);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (oldEnabled === false && newEnabled === true) {
|
|
39
|
+
localHistory = history.slice();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
get dependencies() {
|
|
43
|
+
return [enabled, history, localHistory];
|
|
57
44
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let localHistory = $state<HTMLElement[]>(history.slice())
|
|
63
|
-
|
|
64
|
-
useWatch({
|
|
65
|
-
action: ([newEnabled], [oldEnabled]) => {
|
|
66
|
-
// We are disabling the restore element, so we need to clear it.
|
|
67
|
-
if (oldEnabled === true && newEnabled === false) {
|
|
68
|
-
// However, let's schedule it in a microTask, so that we can still read the value in the
|
|
69
|
-
// places where we are restoring the focus.
|
|
70
|
-
microTask(() => {
|
|
71
|
-
localHistory.splice(0)
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// We are enabling the restore element, so we need to set it to the last "focused" element.
|
|
76
|
-
if (oldEnabled === false && newEnabled === true) {
|
|
77
|
-
localHistory = history.slice()
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
get dependencies() {
|
|
81
|
-
return [enabled, history, localHistory]
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// We want to return the last element that is still connected to the DOM, so we can restore the
|
|
86
|
-
// focus to it.
|
|
87
|
-
return {
|
|
88
|
-
get lastElement() {
|
|
89
|
-
return localHistory.find((x) => x != null && x.isConnected) ?? null
|
|
90
|
-
},
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
get lastElement() {
|
|
48
|
+
return localHistory.find((x) => x != null && x.isConnected) ?? null;
|
|
91
49
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function useRestoreFocus(options) {
|
|
53
|
+
const { features, ownerDocument } = $derived(options);
|
|
54
|
+
const enabled = $derived(Boolean(features & FocusTrapFeatures.RestoreFocus));
|
|
55
|
+
const restoreElement = useRestoreElement({
|
|
56
|
+
get enabled() {
|
|
57
|
+
return enabled;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
useWatch({
|
|
61
|
+
action: () => {
|
|
62
|
+
if (enabled) return;
|
|
63
|
+
if (ownerDocument?.activeElement === ownerDocument?.body) {
|
|
64
|
+
focusElement(restoreElement.lastElement);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
get dependencies() {
|
|
68
|
+
return [enabled];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (!enabled) return;
|
|
73
|
+
return () => focusElement(restoreElement.lastElement);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function useInitialFocus(options) {
|
|
77
|
+
const { features, ownerDocument, container, initialFocus, initialFocusFallback } = $derived(options);
|
|
78
|
+
let previousActiveElement = $state(null);
|
|
79
|
+
let enabled = useIsTopLayer({
|
|
80
|
+
get enabled() {
|
|
81
|
+
return Boolean(features & FocusTrapFeatures.InitialFocus);
|
|
82
|
+
},
|
|
83
|
+
scope: "focus-trap#initial-focus"
|
|
84
|
+
});
|
|
85
|
+
let mounted = useIsMounted();
|
|
86
|
+
useWatch({
|
|
87
|
+
action: () => {
|
|
88
|
+
if (features === FocusTrapFeatures.None) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!enabled) {
|
|
92
|
+
if (initialFocusFallback) {
|
|
93
|
+
focusElement(initialFocusFallback);
|
|
111
94
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
$effect(() => {
|
|
120
|
-
if (!enabled) return
|
|
121
|
-
|
|
122
|
-
return () => focusElement(restoreElement.lastElement)
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function useInitialFocus(options: {
|
|
127
|
-
features: FocusTrapFeatures
|
|
128
|
-
ownerDocument: Document | null
|
|
129
|
-
container: HTMLElement | null
|
|
130
|
-
initialFocus?: HTMLElement | null
|
|
131
|
-
initialFocusFallback?: HTMLElement | null
|
|
132
|
-
}) {
|
|
133
|
-
const { features, ownerDocument, container, initialFocus, initialFocusFallback } = $derived(options)
|
|
134
|
-
let previousActiveElement = $state<HTMLElement | null>(null)
|
|
135
|
-
let enabled = useIsTopLayer({
|
|
136
|
-
get enabled() {
|
|
137
|
-
return Boolean(features & FocusTrapFeatures.InitialFocus)
|
|
138
|
-
},
|
|
139
|
-
scope: "focus-trap#initial-focus",
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
let mounted = useIsMounted()
|
|
143
|
-
|
|
144
|
-
// Handle initial focus
|
|
145
|
-
useWatch({
|
|
146
|
-
action: () => {
|
|
147
|
-
// No focus management needed
|
|
148
|
-
if (features === FocusTrapFeatures.None) {
|
|
149
|
-
return
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let containerElement = container;
|
|
98
|
+
if (!containerElement) return;
|
|
99
|
+
microTask(() => {
|
|
100
|
+
if (!mounted.current) {
|
|
101
|
+
return;
|
|
150
102
|
}
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
// Note: we _don't_ want to move focus to the `initialFocus` ref, because the `InitialFocus`
|
|
158
|
-
// feature is disabled.
|
|
159
|
-
if (initialFocusFallback) {
|
|
160
|
-
focusElement(initialFocusFallback)
|
|
103
|
+
let activeElement = ownerDocument?.activeElement;
|
|
104
|
+
if (initialFocus) {
|
|
105
|
+
if (initialFocus === activeElement) {
|
|
106
|
+
previousActiveElement = activeElement;
|
|
107
|
+
return;
|
|
161
108
|
}
|
|
162
|
-
|
|
163
|
-
|
|
109
|
+
} else if (containerElement.contains(activeElement)) {
|
|
110
|
+
previousActiveElement = activeElement;
|
|
111
|
+
return;
|
|
164
112
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// If we don't do this, then focusing an element will immediately cancel any transitions. This
|
|
172
|
-
// is not ideal because transitions will look broken.
|
|
173
|
-
// There is an additional issue with doing this immediately. The FocusTrap is used inside a
|
|
174
|
-
// Dialog, the Dialog is rendered inside of a Portal and the Portal is rendered at the end of
|
|
175
|
-
// the `document.body`. This means that the moment we call focus, the browser immediately
|
|
176
|
-
// tries to focus the element, which will still be at the bottom resulting in the page to
|
|
177
|
-
// scroll down. Delaying this will prevent the page to scroll down entirely.
|
|
178
|
-
microTask(() => {
|
|
179
|
-
if (!mounted.current) {
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let activeElement = ownerDocument?.activeElement as HTMLElement
|
|
184
|
-
|
|
185
|
-
if (initialFocus) {
|
|
186
|
-
if (initialFocus === activeElement) {
|
|
187
|
-
previousActiveElement = activeElement
|
|
188
|
-
return // Initial focus ref is already the active element
|
|
113
|
+
if (initialFocus) {
|
|
114
|
+
focusElement(initialFocus);
|
|
115
|
+
} else {
|
|
116
|
+
if (features & FocusTrapFeatures.AutoFocus) {
|
|
117
|
+
if (focusIn(containerElement, Focus.First | Focus.AutoFocus) !== FocusResult.Error) {
|
|
118
|
+
return;
|
|
189
119
|
}
|
|
190
|
-
} else if (containerElement
|
|
191
|
-
|
|
192
|
-
return // Already focused within Dialog
|
|
120
|
+
} else if (focusIn(containerElement, Focus.First) !== FocusResult.Error) {
|
|
121
|
+
return;
|
|
193
122
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} else {
|
|
199
|
-
if (features & FocusTrapFeatures.AutoFocus) {
|
|
200
|
-
// Try to focus the first focusable element with `Focus.AutoFocus` feature enabled
|
|
201
|
-
if (focusIn(containerElement!, Focus.First | Focus.AutoFocus) !== FocusResult.Error) {
|
|
202
|
-
return // Worked, bail
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Try to focus the first focusable element.
|
|
207
|
-
else if (focusIn(containerElement!, Focus.First) !== FocusResult.Error) {
|
|
208
|
-
return // Worked, bail
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Try the fallback
|
|
212
|
-
if (initialFocusFallback) {
|
|
213
|
-
focusElement(initialFocusFallback)
|
|
214
|
-
if (ownerDocument?.activeElement === initialFocusFallback) {
|
|
215
|
-
return // Worked, bail
|
|
216
|
-
}
|
|
123
|
+
if (initialFocusFallback) {
|
|
124
|
+
focusElement(initialFocusFallback);
|
|
125
|
+
if (ownerDocument?.activeElement === initialFocusFallback) {
|
|
126
|
+
return;
|
|
217
127
|
}
|
|
218
|
-
|
|
219
|
-
// Nothing worked
|
|
220
|
-
console.warn("There are no focusable elements inside the <FocusTrap />")
|
|
221
128
|
}
|
|
222
|
-
|
|
223
|
-
previousActiveElement = ownerDocument?.activeElement as HTMLElement
|
|
224
|
-
})
|
|
225
|
-
},
|
|
226
|
-
get dependencies() {
|
|
227
|
-
return [initialFocusFallback, enabled, features]
|
|
228
|
-
},
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
get value() {
|
|
233
|
-
return previousActiveElement
|
|
234
|
-
},
|
|
235
|
-
set value(element) {
|
|
236
|
-
previousActiveElement = element
|
|
237
|
-
},
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function useFocusLock(options: {
|
|
242
|
-
features: FocusTrapFeatures
|
|
243
|
-
ownerDocument: Document | null
|
|
244
|
-
container: HTMLElement | null
|
|
245
|
-
containers?: Containers
|
|
246
|
-
previousActiveElement: HTMLElement | null
|
|
247
|
-
}) {
|
|
248
|
-
let { features, ownerDocument, container, containers, previousActiveElement } = $derived(options)
|
|
249
|
-
const mounted = useIsMounted()
|
|
250
|
-
const enabled = $derived(Boolean(features & FocusTrapFeatures.FocusLock))
|
|
251
|
-
|
|
252
|
-
// Prevent programmatically escaping the container
|
|
253
|
-
useEventListener({
|
|
254
|
-
get element() {
|
|
255
|
-
return ownerDocument?.defaultView
|
|
256
|
-
},
|
|
257
|
-
type: "focus",
|
|
258
|
-
listener: (event) => {
|
|
259
|
-
if (!enabled) return
|
|
260
|
-
if (!mounted.current) return
|
|
261
|
-
|
|
262
|
-
let allContainers = resolveContainers(containers)
|
|
263
|
-
if (container instanceof HTMLElement) allContainers.add(container)
|
|
264
|
-
|
|
265
|
-
let previous = previousActiveElement
|
|
266
|
-
if (!previous) return
|
|
267
|
-
|
|
268
|
-
let toElement = event.target as HTMLElement | null
|
|
269
|
-
|
|
270
|
-
if (toElement && toElement instanceof HTMLElement) {
|
|
271
|
-
if (!contains(allContainers, toElement)) {
|
|
272
|
-
event.preventDefault()
|
|
273
|
-
event.stopPropagation()
|
|
274
|
-
focusElement(previous)
|
|
275
|
-
} else {
|
|
276
|
-
options.previousActiveElement = toElement
|
|
277
|
-
focusElement(toElement)
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
focusElement(previousActiveElement)
|
|
129
|
+
console.warn("There are no focusable elements inside the <FocusTrap />");
|
|
281
130
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function contains(containers: Set<HTMLElement>, element: HTMLElement) {
|
|
288
|
-
for (let container of containers) {
|
|
289
|
-
if (container.contains(element)) return true
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return false
|
|
293
|
-
}
|
|
294
|
-
</script>
|
|
295
|
-
|
|
296
|
-
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG">
|
|
297
|
-
let container = $state<HTMLElement | null>(null)
|
|
298
|
-
let {
|
|
299
|
-
ref = $bindable(),
|
|
300
|
-
initialFocus,
|
|
301
|
-
initialFocusFallback,
|
|
302
|
-
containers,
|
|
303
|
-
features = FocusTrapFeatures.InitialFocus |
|
|
304
|
-
FocusTrapFeatures.TabLock |
|
|
305
|
-
FocusTrapFeatures.FocusLock |
|
|
306
|
-
FocusTrapFeatures.RestoreFocus,
|
|
307
|
-
...theirProps
|
|
308
|
-
}: { as?: TTag } & FocusTrapProps<TTag> = $props()
|
|
309
|
-
|
|
310
|
-
/*if (!useServerHandoffComplete()) {
|
|
311
|
-
features = FocusTrapFeatures.None
|
|
312
|
-
}*/
|
|
313
|
-
|
|
314
|
-
const ownerDocument = $derived(getOwnerDocument(ref))
|
|
315
|
-
|
|
316
|
-
useRestoreFocus({
|
|
317
|
-
get features() {
|
|
318
|
-
return features
|
|
319
|
-
},
|
|
320
|
-
get ownerDocument() {
|
|
321
|
-
return ownerDocument
|
|
322
|
-
},
|
|
323
|
-
})
|
|
324
|
-
let previousActiveElement = useInitialFocus({
|
|
325
|
-
get features() {
|
|
326
|
-
return features
|
|
327
|
-
},
|
|
328
|
-
get ownerDocument() {
|
|
329
|
-
return ownerDocument
|
|
330
|
-
},
|
|
331
|
-
get container() {
|
|
332
|
-
return container
|
|
333
|
-
},
|
|
334
|
-
get initialFocus() {
|
|
335
|
-
return initialFocus
|
|
336
|
-
},
|
|
337
|
-
get initialFocusFallback() {
|
|
338
|
-
return initialFocusFallback
|
|
339
|
-
},
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
useFocusLock({
|
|
343
|
-
get features() {
|
|
344
|
-
return features
|
|
345
|
-
},
|
|
346
|
-
get ownerDocument() {
|
|
347
|
-
return ownerDocument
|
|
348
|
-
},
|
|
349
|
-
get container() {
|
|
350
|
-
return container
|
|
351
|
-
},
|
|
352
|
-
get containers() {
|
|
353
|
-
return containers
|
|
131
|
+
previousActiveElement = ownerDocument?.activeElement;
|
|
132
|
+
});
|
|
354
133
|
},
|
|
355
|
-
get
|
|
356
|
-
return
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
134
|
+
get dependencies() {
|
|
135
|
+
return [initialFocusFallback, enabled, features];
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
get value() {
|
|
140
|
+
return previousActiveElement;
|
|
360
141
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
focusIn(el, Focus.First, {
|
|
374
|
-
skipElements: [e.relatedTarget, initialFocusFallback] as HTMLElement[],
|
|
375
|
-
})
|
|
376
|
-
},
|
|
377
|
-
[TabDirection.Backwards]: () => {
|
|
378
|
-
focusIn(el, Focus.Last, {
|
|
379
|
-
skipElements: [e.relatedTarget, initialFocusFallback] as HTMLElement[],
|
|
380
|
-
})
|
|
381
|
-
},
|
|
382
|
-
})
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let tabLockEnabled = useIsTopLayer({
|
|
387
|
-
get enabled() {
|
|
388
|
-
return Boolean(features & FocusTrapFeatures.TabLock)
|
|
142
|
+
set value(element) {
|
|
143
|
+
previousActiveElement = element;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function useFocusLock(options) {
|
|
148
|
+
let { features, ownerDocument, container, containers, previousActiveElement } = $derived(options);
|
|
149
|
+
const mounted = useIsMounted();
|
|
150
|
+
const enabled = $derived(Boolean(features & FocusTrapFeatures.FocusLock));
|
|
151
|
+
useEventListener({
|
|
152
|
+
get element() {
|
|
153
|
+
return ownerDocument?.defaultView;
|
|
389
154
|
},
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
155
|
+
type: "focus",
|
|
156
|
+
listener: (event) => {
|
|
157
|
+
if (!enabled) return;
|
|
158
|
+
if (!mounted.current) return;
|
|
159
|
+
let allContainers = resolveContainers(containers);
|
|
160
|
+
if (container instanceof HTMLElement) allContainers.add(container);
|
|
161
|
+
let previous = previousActiveElement;
|
|
162
|
+
if (!previous) return;
|
|
163
|
+
let toElement = event.target;
|
|
164
|
+
if (toElement && toElement instanceof HTMLElement) {
|
|
165
|
+
if (!contains(allContainers, toElement)) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
focusElement(previous);
|
|
169
|
+
} else {
|
|
170
|
+
options.previousActiveElement = toElement;
|
|
171
|
+
focusElement(toElement);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
focusElement(previousActiveElement);
|
|
402
175
|
}
|
|
403
176
|
},
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
177
|
+
options: true
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function contains(containers, element) {
|
|
181
|
+
for (let container of containers) {
|
|
182
|
+
if (container.contains(element)) return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
</script>
|
|
412
187
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
188
|
+
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG">let container = $state(null);
|
|
189
|
+
let {
|
|
190
|
+
ref = $bindable(),
|
|
191
|
+
initialFocus,
|
|
192
|
+
initialFocusFallback,
|
|
193
|
+
containers,
|
|
194
|
+
features = FocusTrapFeatures.InitialFocus | FocusTrapFeatures.TabLock | FocusTrapFeatures.FocusLock | FocusTrapFeatures.RestoreFocus,
|
|
195
|
+
...theirProps
|
|
196
|
+
} = $props();
|
|
197
|
+
const ownerDocument = $derived(getOwnerDocument(ref));
|
|
198
|
+
useRestoreFocus({
|
|
199
|
+
get features() {
|
|
200
|
+
return features;
|
|
201
|
+
},
|
|
202
|
+
get ownerDocument() {
|
|
203
|
+
return ownerDocument;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
let previousActiveElement = useInitialFocus({
|
|
207
|
+
get features() {
|
|
208
|
+
return features;
|
|
209
|
+
},
|
|
210
|
+
get ownerDocument() {
|
|
211
|
+
return ownerDocument;
|
|
212
|
+
},
|
|
213
|
+
get container() {
|
|
214
|
+
return container;
|
|
215
|
+
},
|
|
216
|
+
get initialFocus() {
|
|
217
|
+
return initialFocus;
|
|
218
|
+
},
|
|
219
|
+
get initialFocusFallback() {
|
|
220
|
+
return initialFocusFallback;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
useFocusLock({
|
|
224
|
+
get features() {
|
|
225
|
+
return features;
|
|
226
|
+
},
|
|
227
|
+
get ownerDocument() {
|
|
228
|
+
return ownerDocument;
|
|
229
|
+
},
|
|
230
|
+
get container() {
|
|
231
|
+
return container;
|
|
232
|
+
},
|
|
233
|
+
get containers() {
|
|
234
|
+
return containers;
|
|
235
|
+
},
|
|
236
|
+
get previousActiveElement() {
|
|
237
|
+
return previousActiveElement.value;
|
|
238
|
+
},
|
|
239
|
+
set previousActiveElement(element) {
|
|
240
|
+
previousActiveElement.value = element;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const direction = useTabDirection();
|
|
244
|
+
const handleFocus = (e) => {
|
|
245
|
+
let el = container;
|
|
246
|
+
if (!el) return;
|
|
247
|
+
let wrapper = process.env.NODE_ENV === "test" ? microTask : (cb) => cb();
|
|
248
|
+
wrapper(() => {
|
|
249
|
+
match(direction.current, {
|
|
250
|
+
[TabDirection.Forwards]: () => {
|
|
251
|
+
focusIn(el, Focus.First, {
|
|
252
|
+
skipElements: [e.relatedTarget, initialFocusFallback]
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
[TabDirection.Backwards]: () => {
|
|
256
|
+
focusIn(el, Focus.Last, {
|
|
257
|
+
skipElements: [e.relatedTarget, initialFocusFallback]
|
|
258
|
+
});
|
|
416
259
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
let tabLockEnabled = useIsTopLayer({
|
|
264
|
+
get enabled() {
|
|
265
|
+
return Boolean(features & FocusTrapFeatures.TabLock);
|
|
266
|
+
},
|
|
267
|
+
scope: "focus-trap#tab-lock"
|
|
268
|
+
});
|
|
269
|
+
const d = useDisposables();
|
|
270
|
+
let recentlyUsedTabKey = $state(false);
|
|
271
|
+
const ourProps = $derived({
|
|
272
|
+
onkeydown(e) {
|
|
273
|
+
if (e.key == "Tab") {
|
|
274
|
+
recentlyUsedTabKey = true;
|
|
275
|
+
d.requestAnimationFrame(() => {
|
|
276
|
+
recentlyUsedTabKey = false;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
onblur(e) {
|
|
281
|
+
if (!(features & FocusTrapFeatures.FocusLock)) return;
|
|
282
|
+
let allContainers = resolveContainers(containers);
|
|
283
|
+
if (container instanceof HTMLElement) allContainers.add(container);
|
|
284
|
+
let relatedTarget = e.relatedTarget;
|
|
285
|
+
if (!(relatedTarget instanceof HTMLElement)) return;
|
|
286
|
+
if (relatedTarget.dataset.headlessuiFocusGuard === "true") {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!contains(allContainers, relatedTarget)) {
|
|
290
|
+
if (recentlyUsedTabKey) {
|
|
291
|
+
focusIn(
|
|
292
|
+
container,
|
|
293
|
+
match(direction.current, {
|
|
294
|
+
[TabDirection.Forwards]: () => Focus.Next,
|
|
295
|
+
[TabDirection.Backwards]: () => Focus.Previous
|
|
296
|
+
}) | Focus.WrapAround,
|
|
297
|
+
{ relativeTo: e.target }
|
|
298
|
+
);
|
|
299
|
+
} else if (e.target instanceof HTMLElement) {
|
|
300
|
+
focusElement(e.target);
|
|
438
301
|
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
441
305
|
</script>
|
|
442
306
|
|
|
443
307
|
{#if tabLockEnabled}
|