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

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 (65) 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 +419 -290
  15. package/dist/focus-trap/FocusTrap.svelte.d.ts +2 -14
  16. package/dist/focus-trap/FocusTrapFeatures.d.ts +14 -0
  17. package/dist/focus-trap/FocusTrapFeatures.js +15 -0
  18. package/dist/input/Input.svelte +85 -53
  19. package/dist/internal/FocusSentinel.svelte +16 -8
  20. package/dist/internal/ForcePortalRoot.svelte +7 -3
  21. package/dist/internal/FormFields.svelte +31 -20
  22. package/dist/internal/FormResolver.svelte +20 -15
  23. package/dist/internal/Hidden.svelte +44 -27
  24. package/dist/internal/Hidden.svelte.d.ts +2 -5
  25. package/dist/internal/HiddenFeatures.d.ts +5 -0
  26. package/dist/internal/HiddenFeatures.js +9 -0
  27. package/dist/internal/HoistFormFields.svelte +7 -4
  28. package/dist/internal/MainTreeProvider.svelte +89 -36
  29. package/dist/internal/Portal.svelte +18 -14
  30. package/dist/label/Label.svelte +91 -57
  31. package/dist/legend/Legend.svelte +18 -3
  32. package/dist/listbox/Listbox.svelte +588 -409
  33. package/dist/listbox/Listbox.svelte.d.ts +2 -12
  34. package/dist/listbox/ListboxButton.svelte +176 -127
  35. package/dist/listbox/ListboxOption.svelte +166 -125
  36. package/dist/listbox/ListboxOptions.svelte +340 -244
  37. package/dist/listbox/ListboxSelectedOption.svelte +38 -15
  38. package/dist/listbox/ListboxStates.d.ts +12 -0
  39. package/dist/listbox/ListboxStates.js +15 -0
  40. package/dist/menu/Menu.svelte +307 -218
  41. package/dist/menu/MenuButton.svelte +157 -115
  42. package/dist/menu/MenuHeading.svelte +34 -14
  43. package/dist/menu/MenuItem.svelte +145 -107
  44. package/dist/menu/MenuItems.svelte +298 -224
  45. package/dist/menu/MenuSection.svelte +26 -9
  46. package/dist/menu/MenuSeparator.svelte +20 -4
  47. package/dist/portal/InternalPortal.svelte +141 -85
  48. package/dist/portal/Portal.svelte +5 -2
  49. package/dist/portal/PortalGroup.svelte +30 -9
  50. package/dist/switch/Switch.svelte +179 -122
  51. package/dist/switch/Switch.svelte.d.ts +4 -4
  52. package/dist/switch/SwitchGroup.svelte +44 -31
  53. package/dist/tabs/Tab.svelte +195 -143
  54. package/dist/tabs/TabGroup.svelte +292 -205
  55. package/dist/tabs/TabList.svelte +31 -11
  56. package/dist/tabs/TabPanel.svelte +68 -43
  57. package/dist/tabs/TabPanels.svelte +18 -7
  58. package/dist/textarea/Textarea.svelte +83 -53
  59. package/dist/transition/InternalTransitionChild.svelte +259 -170
  60. package/dist/transition/Transition.svelte +96 -66
  61. package/dist/transition/TransitionChild.svelte +31 -11
  62. package/dist/utils/ElementOrComponent.svelte +44 -23
  63. package/dist/utils/Generic.svelte +29 -17
  64. package/dist/utils/StableCollection.svelte +54 -36
  65. package/package.json +10 -10
@@ -1,314 +1,443 @@
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
+ import { FocusTrapFeatures } from "./FocusTrapFeatures.js"
17
+
18
+ type Containers =
19
+ // Lazy resolved containers
20
+ | (() => Iterable<HTMLElement>)
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)
47
33
  }
48
- },
49
- get dependencies() {
50
- return [enabled, history, localHistory];
51
34
  }
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;
35
+ return all
36
+ }
37
+
38
+ let DEFAULT_FOCUS_TRAP_TAG = "div" as const
39
+
40
+ export * from "./FocusTrapFeatures.js"
41
+
42
+ type FocusTrapRenderPropArg = {}
43
+ type FocusTrapPropsWeControl = never
44
+
45
+ export type FocusTrapProps<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG> = Props<
46
+ TTag,
47
+ FocusTrapRenderPropArg,
48
+ FocusTrapPropsWeControl,
49
+ {
50
+ initialFocus?: HTMLElement
51
+ // A fallback element to focus, but this element will be skipped when tabbing around. This is
52
+ // only done for focusing a fallback parent container (e.g.: A `Dialog`, but you want to tab
53
+ // *inside* the dialog excluding the dialog itself).
54
+ initialFocusFallback?: HTMLElement
55
+ features?: FocusTrapFeatures
56
+ containers?: Containers
65
57
  }
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];
58
+ >
59
+
60
+ function useRestoreElement(options?: { enabled: boolean }) {
61
+ const { enabled } = $derived(options ?? { enabled: true })
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
+ },
76
91
  }
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);
92
+ }
93
+
94
+ function useRestoreFocus(options: { features: FocusTrapFeatures; ownerDocument: Document | null }) {
95
+ const { features, ownerDocument } = $derived(options)
96
+ const enabled = $derived(Boolean(features & FocusTrapFeatures.RestoreFocus))
97
+
98
+ const restoreElement = useRestoreElement({
99
+ get enabled() {
100
+ return enabled
101
+ },
102
+ })
103
+
104
+ // Restore the focus to the previous element when `enabled` becomes false again
105
+ useWatch({
106
+ action: () => {
107
+ if (enabled) return
108
+
109
+ if (ownerDocument?.activeElement === ownerDocument?.body) {
110
+ focusElement(restoreElement.lastElement)
101
111
  }
102
- return;
103
- }
104
- let containerElement = container;
105
- if (!containerElement) return;
106
- microTask(() => {
107
- if (!mounted.current) {
108
- return;
112
+ },
113
+ get dependencies() {
114
+ return [enabled]
115
+ },
116
+ })
117
+
118
+ // Restore the focus to the previous element when the component is unmounted
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
109
150
  }
110
- let activeElement = ownerDocument?.activeElement;
111
- if (initialFocus) {
112
- if (initialFocus === activeElement) {
113
- previousActiveElement = activeElement;
114
- return;
151
+
152
+ if (!enabled) {
153
+ // If we are disabling the initialFocus, then we should focus the fallback element if one is
154
+ // provided. This is needed to ensure _something_ is focused. Typically a wrapping element
155
+ // (e.g.: `Dialog` component).
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)
115
161
  }
116
- } else if (containerElement.contains(activeElement)) {
117
- previousActiveElement = activeElement;
118
- return;
162
+
163
+ return
119
164
  }
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;
165
+ let containerElement = container
166
+ if (!containerElement) return
167
+
168
+ // Delaying the focus to the next microtask ensures that a few conditions are true:
169
+ // - The container is rendered
170
+ // - Transitions could be started
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
126
189
  }
127
- } else if (focusIn(containerElement, Focus.First) !== FocusResult.Error) {
128
- return;
190
+ } else if (containerElement!.contains(activeElement)) {
191
+ previousActiveElement = activeElement
192
+ return // Already focused within Dialog
129
193
  }
130
- if (initialFocusFallback) {
131
- focusElement(initialFocusFallback);
132
- if (ownerDocument?.activeElement === initialFocusFallback) {
133
- return;
194
+
195
+ // Try to focus the initialFocus ref
196
+ if (initialFocus) {
197
+ focusElement(initialFocus)
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
+ }
134
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
+ }
217
+ }
218
+
219
+ // Nothing worked
220
+ console.warn("There are no focusable elements inside the <FocusTrap />")
135
221
  }
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;
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
+ },
151
238
  }
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);
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
+ }
176
279
  } else {
177
- options.previousActiveElement = toElement;
178
- focusElement(toElement);
280
+ focusElement(previousActiveElement)
179
281
  }
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;
282
+ },
283
+ options: true,
284
+ })
190
285
  }
191
- return false;
192
- }
193
- </script>
194
286
 
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;
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
228
293
  }
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;
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
354
+ },
355
+ get previousActiveElement() {
356
+ return previousActiveElement.value
357
+ },
358
+ set previousActiveElement(element) {
359
+ previousActiveElement.value = element
360
+ },
361
+ })
362
+
363
+ const direction = useTabDirection()
364
+ const handleFocus = (e: FocusEvent) => {
365
+ let el = container as HTMLElement
366
+ if (!el) return
367
+
368
+ // TODO: Cleanup once we are using real browser tests
369
+ let wrapper = process.env.NODE_ENV === "test" ? microTask : (cb: Function) => cb()
370
+ wrapper(() => {
371
+ match(direction.current, {
372
+ [TabDirection.Forwards]: () => {
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
+ })
248
384
  }
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
- });
385
+
386
+ let tabLockEnabled = useIsTopLayer({
387
+ get enabled() {
388
+ return Boolean(features & FocusTrapFeatures.TabLock)
389
+ },
390
+ scope: "focus-trap#tab-lock",
391
+ })
392
+
393
+ const d = useDisposables()
394
+ let recentlyUsedTabKey = $state(false)
395
+ const ourProps = $derived({
396
+ onkeydown(e: KeyboardEvent) {
397
+ if (e.key == "Tab") {
398
+ recentlyUsedTabKey = true
399
+ d.requestAnimationFrame(() => {
400
+ recentlyUsedTabKey = false
401
+ })
266
402
  }
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);
403
+ },
404
+ onblur(e: FocusEvent) {
405
+ if (!(features & FocusTrapFeatures.FocusLock)) return
406
+
407
+ let allContainers = resolveContainers(containers)
408
+ if (container instanceof HTMLElement) allContainers.add(container)
409
+
410
+ let relatedTarget = e.relatedTarget
411
+ if (!(relatedTarget instanceof HTMLElement)) return
412
+
413
+ // Known guards, leave them alone!
414
+ if (relatedTarget.dataset.headlessuiFocusGuard === "true") {
415
+ return
308
416
  }
309
- }
310
- }
311
- });
417
+
418
+ // Blur is triggered due to focus on relatedTarget, and the relatedTarget is not inside any
419
+ // of the dialog containers. In other words, let's move focus back in!
420
+ if (!contains(allContainers, relatedTarget)) {
421
+ // Was the blur invoked via the keyboard? Redirect to the next in line.
422
+ if (recentlyUsedTabKey) {
423
+ focusIn(
424
+ container as HTMLElement,
425
+ match(direction.current, {
426
+ [TabDirection.Forwards]: () => Focus.Next,
427
+ [TabDirection.Backwards]: () => Focus.Previous,
428
+ }) | Focus.WrapAround,
429
+ { relativeTo: e.target as HTMLElement }
430
+ )
431
+ }
432
+
433
+ // It was invoked via something else (e.g.: click, programmatically, ...). Redirect to the
434
+ // previous active item in the FocusTrap
435
+ else if (e.target instanceof HTMLElement) {
436
+ focusElement(e.target)
437
+ }
438
+ }
439
+ },
440
+ })
312
441
  </script>
313
442
 
314
443
  {#if tabLockEnabled}