@pzerelles/headlessui-svelte 2.1.2-next.3 → 2.1.2-next.4

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