@pzerelles/headlessui-svelte 2.1.2-next.30 → 2.1.2-next.32
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 +84 -54
- package/dist/button/Button.svelte.d.ts +2 -3
- package/dist/checkbox/Checkbox.svelte +173 -120
- package/dist/checkbox/Checkbox.svelte.d.ts +2 -3
- package/dist/close-button/CloseButton.svelte +12 -6
- package/dist/close-button/CloseButton.svelte.d.ts +7 -8
- package/dist/combobox/Combobox.svelte +50 -3
- package/dist/data-interactive/DataInteractive.svelte +55 -29
- package/dist/data-interactive/DataInteractive.svelte.d.ts +2 -3
- package/dist/description/Description.svelte +31 -21
- package/dist/description/Description.svelte.d.ts +1 -2
- package/dist/dialog/Dialog.svelte +320 -228
- package/dist/dialog/Dialog.svelte.d.ts +2 -3
- package/dist/dialog/DialogBackdrop.svelte +30 -13
- package/dist/dialog/DialogBackdrop.svelte.d.ts +2 -3
- package/dist/dialog/DialogPanel.svelte +49 -26
- package/dist/dialog/DialogPanel.svelte.d.ts +2 -3
- package/dist/dialog/DialogTitle.svelte +38 -23
- package/dist/dialog/DialogTitle.svelte.d.ts +2 -3
- package/dist/field/Field.svelte +47 -25
- package/dist/field/Field.svelte.d.ts +1 -2
- package/dist/fieldset/Fieldset.svelte +50 -29
- package/dist/fieldset/Fieldset.svelte.d.ts +1 -2
- package/dist/focus-trap/FocusTrap.svelte +419 -283
- package/dist/focus-trap/FocusTrap.svelte.d.ts +1 -2
- package/dist/input/Input.svelte +84 -53
- package/dist/input/Input.svelte.d.ts +2 -3
- package/dist/internal/FloatingProvider.svelte +14 -9
- package/dist/internal/FocusSentinel.svelte +16 -8
- package/dist/internal/ForcePortalRoot.svelte +7 -3
- package/dist/internal/FormFields.svelte +47 -34
- package/dist/internal/FormFieldsProvider.svelte +9 -5
- package/dist/internal/FormResolver.svelte +20 -15
- package/dist/internal/Hidden.svelte +50 -29
- package/dist/internal/Hidden.svelte.d.ts +1 -2
- package/dist/internal/MainTreeProvider.svelte +89 -36
- package/dist/internal/Portal.svelte +18 -14
- package/dist/internal/floating.svelte.d.ts +2 -3
- package/dist/internal/floating.svelte.js +0 -1
- package/dist/label/Label.svelte +93 -58
- package/dist/label/Label.svelte.d.ts +1 -2
- package/dist/legend/Legend.svelte +12 -3
- package/dist/listbox/Listbox.svelte +525 -387
- package/dist/listbox/Listbox.svelte.d.ts +2 -3
- package/dist/listbox/ListboxButton.svelte +173 -127
- package/dist/listbox/ListboxButton.svelte.d.ts +2 -3
- package/dist/listbox/ListboxOption.svelte +170 -129
- package/dist/listbox/ListboxOption.svelte.d.ts +2 -3
- package/dist/listbox/ListboxOptions.svelte +400 -304
- package/dist/listbox/ListboxOptions.svelte.d.ts +2 -3
- package/dist/listbox/ListboxSelectedOption.svelte +38 -15
- package/dist/listbox/ListboxSelectedOption.svelte.d.ts +1 -2
- package/dist/menu/Menu.svelte +77 -51
- package/dist/menu/Menu.svelte.d.ts +2 -4
- package/dist/menu/MenuButton.svelte +157 -117
- package/dist/menu/MenuButton.svelte.d.ts +2 -3
- package/dist/menu/MenuHeading.svelte +32 -14
- package/dist/menu/MenuHeading.svelte.d.ts +1 -2
- package/dist/menu/MenuItem.svelte +142 -107
- package/dist/menu/MenuItem.svelte.d.ts +2 -3
- package/dist/menu/MenuItems.svelte +301 -229
- package/dist/menu/MenuItems.svelte.d.ts +2 -3
- package/dist/menu/MenuSection.svelte +24 -9
- package/dist/menu/MenuSection.svelte.d.ts +1 -2
- package/dist/menu/MenuSeparator.svelte +17 -4
- package/dist/menu/MenuSeparator.svelte.d.ts +1 -2
- package/dist/popover/Popover.svelte +216 -150
- package/dist/popover/Popover.svelte.d.ts +2 -3
- package/dist/popover/PopoverBackdrop.svelte +67 -41
- package/dist/popover/PopoverBackdrop.svelte.d.ts +2 -3
- package/dist/popover/PopoverButton.svelte +292 -212
- package/dist/popover/PopoverButton.svelte.d.ts +2 -3
- package/dist/popover/PopoverGroup.svelte +62 -35
- package/dist/popover/PopoverGroup.svelte.d.ts +1 -2
- package/dist/popover/PopoverPanel.svelte +311 -229
- package/dist/popover/PopoverPanel.svelte.d.ts +2 -3
- package/dist/portal/InternalPortal.svelte +141 -85
- package/dist/portal/InternalPortal.svelte.d.ts +1 -2
- package/dist/portal/Portal.svelte +5 -2
- package/dist/portal/PortalGroup.svelte +30 -9
- package/dist/portal/PortalGroup.svelte.d.ts +1 -2
- package/dist/select/Select.svelte +98 -68
- package/dist/select/Select.svelte.d.ts +2 -3
- package/dist/switch/Switch.svelte +179 -132
- package/dist/switch/Switch.svelte.d.ts +2 -3
- package/dist/switch/SwitchGroup.svelte +44 -31
- package/dist/switch/SwitchGroup.svelte.d.ts +1 -2
- package/dist/tabs/Tab.svelte +194 -142
- package/dist/tabs/Tab.svelte.d.ts +2 -3
- package/dist/tabs/TabGroup.svelte +86 -56
- package/dist/tabs/TabGroup.svelte.d.ts +2 -3
- package/dist/tabs/TabList.svelte +31 -11
- package/dist/tabs/TabList.svelte.d.ts +2 -3
- package/dist/tabs/TabPanel.svelte +67 -42
- package/dist/tabs/TabPanel.svelte.d.ts +2 -3
- package/dist/tabs/TabPanels.svelte +18 -7
- package/dist/tabs/TabPanels.svelte.d.ts +2 -3
- package/dist/textarea/Textarea.svelte +84 -53
- package/dist/textarea/Textarea.svelte.d.ts +2 -3
- package/dist/transition/InternalTransitionChild.svelte +259 -170
- package/dist/transition/InternalTransitionChild.svelte.d.ts +2 -3
- package/dist/transition/Transition.svelte +96 -66
- package/dist/transition/Transition.svelte.d.ts +2 -3
- package/dist/transition/TransitionChild.svelte +31 -11
- package/dist/transition/TransitionChild.svelte.d.ts +2 -3
- package/dist/utils/DisabledProvider.svelte +7 -3
- package/dist/utils/ElementOrComponent.svelte +46 -23
- package/dist/utils/ElementOrComponent.svelte.d.ts +8 -10
- package/dist/utils/Generic.svelte +30 -19
- package/dist/utils/Generic.svelte.d.ts +6 -7
- package/dist/utils/StableCollection.svelte +54 -36
- package/dist/utils/floating-ui/svelte/components/FloatingNode.svelte +27 -12
- package/dist/utils/floating-ui/svelte/components/FloatingTree.svelte +88 -44
- package/dist/utils/types.d.ts +4 -5
- package/package.json +1 -1
|
@@ -1,307 +1,443 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (oldEnabled === true && newEnabled === false) {
|
|
34
|
-
microTask(() => {
|
|
35
|
-
localHistory.splice(0);
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
if (oldEnabled === false && newEnabled === true) {
|
|
39
|
-
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)
|
|
40
33
|
}
|
|
41
|
-
},
|
|
42
|
-
get dependencies() {
|
|
43
|
-
return [enabled, history, localHistory];
|
|
44
34
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
},
|
|
69
91
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (!enabled) {
|
|
92
|
-
if (initialFocusFallback) {
|
|
93
|
-
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)
|
|
94
111
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
102
150
|
}
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
108
161
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return;
|
|
162
|
+
|
|
163
|
+
return
|
|
112
164
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
119
189
|
}
|
|
120
|
-
} else if (
|
|
121
|
-
|
|
190
|
+
} else if (containerElement!.contains(activeElement)) {
|
|
191
|
+
previousActiveElement = activeElement
|
|
192
|
+
return // Already focused within Dialog
|
|
122
193
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
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 />")
|
|
128
221
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
},
|
|
144
238
|
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function useFocusLock(options
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
279
|
} else {
|
|
170
|
-
|
|
171
|
-
focusElement(toElement);
|
|
280
|
+
focusElement(previousActiveElement)
|
|
172
281
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
options: true
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
function contains(containers, element) {
|
|
181
|
-
for (let container of containers) {
|
|
182
|
-
if (container.contains(element)) return true;
|
|
282
|
+
},
|
|
283
|
+
options: true,
|
|
284
|
+
})
|
|
183
285
|
}
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
</script>
|
|
187
286
|
|
|
188
|
-
|
|
189
|
-
let {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
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
|
|
221
293
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
})
|
|
241
384
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
})
|
|
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
|
+
})
|
|
259
402
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
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
|
|
301
416
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
})
|
|
305
441
|
</script>
|
|
306
442
|
|
|
307
443
|
{#if tabLockEnabled}
|