@pzerelles/headlessui-svelte 2.1.2-next.58 → 2.1.2-next.59
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/dialog/Dialog.svelte +7 -296
- package/dist/dialog/DialogPanel.svelte +7 -0
- package/dist/dialog/InternalDialog.svelte +298 -0
- package/dist/dialog/InternalDialog.svelte.d.ts +4 -0
- package/dist/hooks/use-root-containers.svelte.js +3 -1
- package/dist/internal/MainTreeProvider.svelte +1 -1
- package/dist/internal/floating.svelte.d.ts +2 -2
- package/dist/popover/Popover.svelte +4 -1
- package/dist/portal/InternalPortal.svelte +1 -2
- package/dist/portal/InternalPortal.svelte.d.ts +1 -1
- package/dist/portal/PortalWrapper.svelte +10 -0
- package/dist/portal/PortalWrapper.svelte.d.ts +9 -0
- package/package.json +16 -16
|
@@ -27,35 +27,16 @@
|
|
|
27
27
|
</script>
|
|
28
28
|
|
|
29
29
|
<script lang="ts">
|
|
30
|
-
import {
|
|
31
|
-
import { useMainTreeNode, useRootContainers } from "../hooks/use-root-containers.svelte.js"
|
|
32
|
-
import { clearOpenClosedContext, State, useOpenClosed } from "../internal/open-closed.js"
|
|
33
|
-
import { useNestedPortals } from "../portal/InternalPortal.svelte"
|
|
34
|
-
import { getOwnerDocument } from "../utils/owner.js"
|
|
35
|
-
import { useInertOthers } from "../hooks/use-inert-others.svelte.js"
|
|
36
|
-
import { useOutsideClick } from "../hooks/use-outside-click.svelte.js"
|
|
37
|
-
import { useEscape } from "../hooks/use-escape.svelte.js"
|
|
38
|
-
import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js"
|
|
39
|
-
import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js"
|
|
40
|
-
import { setContext } from "svelte"
|
|
41
|
-
import { useIsTouchDevice } from "../hooks/use-is-touch-device.svelte.js"
|
|
42
|
-
import FocusTrap, { FocusTrapFeatures } from "../focus-trap/FocusTrap.svelte"
|
|
43
|
-
import Portal from "../portal/Portal.svelte"
|
|
44
|
-
import PortalGroup from "../portal/PortalGroup.svelte"
|
|
45
|
-
import ForcePortalRoot from "../internal/ForcePortalRoot.svelte"
|
|
46
|
-
import { createCloseContext } from "../internal/close-provider.js"
|
|
47
|
-
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
48
|
-
import { DialogStates, type DialogContext, type StateDefinition } from "./context.svelte.js"
|
|
49
|
-
import { useDescriptions } from "../description/context.svelte.js"
|
|
30
|
+
import { useOpenClosed } from "../internal/open-closed.js"
|
|
50
31
|
import MainTreeProvider from "../internal/MainTreeProvider.svelte"
|
|
51
32
|
import Transition from "../transition/Transition.svelte"
|
|
52
|
-
import
|
|
33
|
+
import InternalDialog from "./InternalDialog.svelte"
|
|
53
34
|
|
|
54
|
-
let { element = $bindable(), transition = false, open
|
|
35
|
+
let { element = $bindable(), transition = false, open, ...rest }: DialogProps = $props()
|
|
55
36
|
|
|
56
37
|
// Validations
|
|
57
38
|
const usesOpenClosedState = useOpenClosed()
|
|
58
|
-
const hasOpen = $derived(
|
|
39
|
+
const hasOpen = $derived(open !== undefined || usesOpenClosedState)
|
|
59
40
|
const hasOnClose = $derived(rest.hasOwnProperty("onclose"))
|
|
60
41
|
|
|
61
42
|
$effect(() => {
|
|
@@ -83,288 +64,18 @@
|
|
|
83
64
|
)
|
|
84
65
|
}
|
|
85
66
|
})
|
|
86
|
-
|
|
87
|
-
const internalId = useId()
|
|
88
|
-
let {
|
|
89
|
-
id = `headlessui-dialog-${internalId}`,
|
|
90
|
-
onclose,
|
|
91
|
-
initialFocus,
|
|
92
|
-
role: theirRole = "dialog",
|
|
93
|
-
autofocus = true,
|
|
94
|
-
__demoMode = false,
|
|
95
|
-
unmount = false,
|
|
96
|
-
...theirProps
|
|
97
|
-
} = $derived(rest)
|
|
98
|
-
|
|
99
|
-
let didWarnOnRole = $state(false)
|
|
100
|
-
|
|
101
|
-
const role = $derived.by(() => {
|
|
102
|
-
if (theirRole === "dialog" || theirRole === "alertdialog") {
|
|
103
|
-
return theirRole
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!didWarnOnRole) {
|
|
107
|
-
didWarnOnRole = true
|
|
108
|
-
console.warn(
|
|
109
|
-
`Invalid role [${theirRole}] passed to <Dialog />. Only \`dialog\` and and \`alertdialog\` are supported. Using \`dialog\` instead.`
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return "dialog"
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
// Update the `open` prop based on the open closed state
|
|
117
|
-
const open = $derived(
|
|
118
|
-
theirOpen === undefined && usesOpenClosedState !== null
|
|
119
|
-
? (usesOpenClosedState.value & State.Open) === State.Open
|
|
120
|
-
: theirOpen
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
const ownerDocument = $derived(getOwnerDocument(element))
|
|
124
|
-
|
|
125
|
-
const dialogState = $derived(open ? DialogStates.Open : DialogStates.Closed)
|
|
126
|
-
|
|
127
|
-
let _state = $state({
|
|
128
|
-
titleId: null,
|
|
129
|
-
panelRef: null,
|
|
130
|
-
} as StateDefinition)
|
|
131
|
-
|
|
132
|
-
const close = $derived(() => onclose(false))
|
|
133
|
-
|
|
134
|
-
const setTitleId = (id: string | null) => (_state.titleId = id)
|
|
135
|
-
|
|
136
|
-
const ready = BROWSER
|
|
137
|
-
const enabled = $derived(ready ? dialogState === DialogStates.Open : false)
|
|
138
|
-
const nestedPortals = useNestedPortals()
|
|
139
|
-
const { portals } = $derived(nestedPortals)
|
|
140
|
-
|
|
141
|
-
// We use this because reading these values during initial render(s)
|
|
142
|
-
// can result in `null` rather then the actual elements
|
|
143
|
-
// This doesn't happen when using certain components like a
|
|
144
|
-
// `<Dialog.Title>` because they cause the parent to re-render
|
|
145
|
-
const defaultContainer: { readonly current: HTMLElement | undefined } = {
|
|
146
|
-
get current() {
|
|
147
|
-
return _state.panelRef ?? element
|
|
148
|
-
},
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const mainTreeNode = useMainTreeNode()
|
|
152
|
-
let { resolvedContainers: resolvedRootContainers } = $derived(
|
|
153
|
-
useRootContainers({
|
|
154
|
-
get mainTreeNode() {
|
|
155
|
-
return mainTreeNode.node
|
|
156
|
-
},
|
|
157
|
-
get portals() {
|
|
158
|
-
return portals
|
|
159
|
-
},
|
|
160
|
-
get defaultContainers() {
|
|
161
|
-
return defaultContainer.current ? [defaultContainer.current] : []
|
|
162
|
-
},
|
|
163
|
-
})
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
// When the `Dialog` is wrapped in a `Transition` (or another Headless UI component that exposes
|
|
167
|
-
// the OpenClosed state) then we get some information via context about its state. When the
|
|
168
|
-
// `Transition` is about to close, then the `State.Closing` state will be exposed. This allows us
|
|
169
|
-
// to enable/disable certain functionality in the `Dialog` upfront instead of waiting until the
|
|
170
|
-
// `Transition` is done transitioning.
|
|
171
|
-
const isClosing = $derived(
|
|
172
|
-
usesOpenClosedState !== null ? (usesOpenClosedState.value & State.Closing) === State.Closing : false
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
// Ensure other elements can't be interacted with
|
|
176
|
-
const inertOthersEnabled = $derived(__demoMode ? false : isClosing ? false : enabled)
|
|
177
|
-
useInertOthers({
|
|
178
|
-
get enabled() {
|
|
179
|
-
return inertOthersEnabled
|
|
180
|
-
},
|
|
181
|
-
elements: {
|
|
182
|
-
get allowed() {
|
|
183
|
-
return [
|
|
184
|
-
// Allow the headlessui-portal of the Dialog to be interactive. This
|
|
185
|
-
// contains the current dialog and the necessary focus guard elements.
|
|
186
|
-
element?.closest<HTMLElement>("[data-headlessui-portal]") ?? null,
|
|
187
|
-
]
|
|
188
|
-
},
|
|
189
|
-
get disallowed() {
|
|
190
|
-
return [
|
|
191
|
-
// Disallow the "main" tree root node
|
|
192
|
-
mainTreeNode.node?.closest<HTMLElement>("body > *:not(#headlessui-portal-root)") ?? null,
|
|
193
|
-
]
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
// Close Dialog on outside click
|
|
199
|
-
useOutsideClick({
|
|
200
|
-
get enabled() {
|
|
201
|
-
return enabled
|
|
202
|
-
},
|
|
203
|
-
get containers() {
|
|
204
|
-
return resolvedRootContainers
|
|
205
|
-
},
|
|
206
|
-
cb(event) {
|
|
207
|
-
event.preventDefault()
|
|
208
|
-
close()
|
|
209
|
-
},
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// Handle `Escape` to close
|
|
213
|
-
useEscape({
|
|
214
|
-
get enabled() {
|
|
215
|
-
return enabled
|
|
216
|
-
},
|
|
217
|
-
get view() {
|
|
218
|
-
return ownerDocument?.defaultView ?? null
|
|
219
|
-
},
|
|
220
|
-
cb(event) {
|
|
221
|
-
event.preventDefault()
|
|
222
|
-
event.stopPropagation()
|
|
223
|
-
|
|
224
|
-
// Ensure that we blur the current activeElement to prevent maintaining
|
|
225
|
-
// focus and potentially scrolling the page to the end (because the Dialog
|
|
226
|
-
// is rendered in a Portal at the end of the document.body and the browser
|
|
227
|
-
// tries to keep the focused element in view)
|
|
228
|
-
//
|
|
229
|
-
// Typically only happens in Safari.
|
|
230
|
-
if (
|
|
231
|
-
document.activeElement &&
|
|
232
|
-
"blur" in document.activeElement &&
|
|
233
|
-
typeof document.activeElement.blur === "function"
|
|
234
|
-
) {
|
|
235
|
-
document.activeElement.blur()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
close()
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// Scroll lock
|
|
243
|
-
const scrollLockEnabled = $derived(__demoMode ? false : isClosing ? false : enabled)
|
|
244
|
-
useScrollLock({
|
|
245
|
-
get enabled() {
|
|
246
|
-
return scrollLockEnabled
|
|
247
|
-
},
|
|
248
|
-
get ownerDocument() {
|
|
249
|
-
return ownerDocument
|
|
250
|
-
},
|
|
251
|
-
resolveAllowedContainers() {
|
|
252
|
-
return resolvedRootContainers
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
// Ensure we close the dialog as soon as the dialog itself becomes hidden
|
|
257
|
-
useOnDisappear({
|
|
258
|
-
get enabled() {
|
|
259
|
-
return enabled
|
|
260
|
-
},
|
|
261
|
-
get ref() {
|
|
262
|
-
return element
|
|
263
|
-
},
|
|
264
|
-
get ondisappear() {
|
|
265
|
-
return close
|
|
266
|
-
},
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
const describedby = useDescriptions()
|
|
270
|
-
|
|
271
|
-
setContext<DialogContext>("DialogContext", {
|
|
272
|
-
get titleId() {
|
|
273
|
-
return _state.titleId
|
|
274
|
-
},
|
|
275
|
-
get panelRef() {
|
|
276
|
-
return _state.panelRef
|
|
277
|
-
},
|
|
278
|
-
get dialogState() {
|
|
279
|
-
return dialogState
|
|
280
|
-
},
|
|
281
|
-
get close() {
|
|
282
|
-
return close
|
|
283
|
-
},
|
|
284
|
-
get unmount() {
|
|
285
|
-
return unmount
|
|
286
|
-
},
|
|
287
|
-
setTitleId,
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
const slot = $derived({ open: dialogState === DialogStates.Open } satisfies DialogRenderPropArg)
|
|
291
|
-
|
|
292
|
-
const ourProps = $derived({
|
|
293
|
-
id,
|
|
294
|
-
role,
|
|
295
|
-
tabIndex: -1,
|
|
296
|
-
"aria-modal": __demoMode ? undefined : dialogState === DialogStates.Open ? true : undefined,
|
|
297
|
-
"aria-labelledby": _state.titleId,
|
|
298
|
-
"aria-describedby": describedby.value,
|
|
299
|
-
unmount,
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
const shouldMoveFocusInside = !useIsTouchDevice().value
|
|
303
|
-
const focusTrapFeatures = $derived.by(() => {
|
|
304
|
-
let focusTrapFeatures = FocusTrapFeatures.None
|
|
305
|
-
|
|
306
|
-
if (enabled && !__demoMode) {
|
|
307
|
-
focusTrapFeatures |= FocusTrapFeatures.RestoreFocus
|
|
308
|
-
focusTrapFeatures |= FocusTrapFeatures.TabLock
|
|
309
|
-
|
|
310
|
-
if (autofocus) {
|
|
311
|
-
focusTrapFeatures |= FocusTrapFeatures.AutoFocus
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (shouldMoveFocusInside) {
|
|
315
|
-
focusTrapFeatures |= FocusTrapFeatures.InitialFocus
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return focusTrapFeatures
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
clearOpenClosedContext()
|
|
323
|
-
createCloseContext({
|
|
324
|
-
get close() {
|
|
325
|
-
return close
|
|
326
|
-
},
|
|
327
|
-
})
|
|
328
67
|
</script>
|
|
329
68
|
|
|
330
|
-
{#
|
|
331
|
-
<ForcePortalRoot force={true}>
|
|
332
|
-
<Portal>
|
|
333
|
-
<PortalGroup target={element ?? null}>
|
|
334
|
-
<ForcePortalRoot force={false}>
|
|
335
|
-
<FocusTrap
|
|
336
|
-
{initialFocus}
|
|
337
|
-
initialFocusFallback={element}
|
|
338
|
-
containers={resolvedRootContainers}
|
|
339
|
-
features={focusTrapFeatures}
|
|
340
|
-
>
|
|
341
|
-
<ElementOrComponent
|
|
342
|
-
{ourProps}
|
|
343
|
-
theirProps={{ ...theirProps, ...transitionProps }}
|
|
344
|
-
slots={slot}
|
|
345
|
-
defaultTag={DEFAULT_DIALOG_TAG}
|
|
346
|
-
features={DialogRenderFeatures}
|
|
347
|
-
visible={dialogState === DialogStates.Open}
|
|
348
|
-
name="Dialog"
|
|
349
|
-
bind:element
|
|
350
|
-
/>
|
|
351
|
-
</FocusTrap>
|
|
352
|
-
</ForcePortalRoot>
|
|
353
|
-
</PortalGroup>
|
|
354
|
-
</Portal>
|
|
355
|
-
</ForcePortalRoot>
|
|
356
|
-
{/snippet}
|
|
357
|
-
|
|
358
|
-
{#if (open !== undefined || transition) && !theirProps.static}
|
|
69
|
+
{#if (open !== undefined || transition) && !rest.static}
|
|
359
70
|
<MainTreeProvider>
|
|
360
71
|
<Transition asChild show={open} {transition} unmount={rest.unmount} {element}>
|
|
361
72
|
{#snippet children({ props })}
|
|
362
|
-
{
|
|
73
|
+
<InternalDialog {...rest} {...props} bind:element />
|
|
363
74
|
{/snippet}
|
|
364
75
|
</Transition>
|
|
365
76
|
</MainTreeProvider>
|
|
366
77
|
{:else}
|
|
367
78
|
<MainTreeProvider>
|
|
368
|
-
{
|
|
79
|
+
<InternalDialog bind:element {open} {...rest} />
|
|
369
80
|
</MainTreeProvider>
|
|
370
81
|
{/if}
|
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
const _state = useDialogContext("Dialog.Panel")
|
|
33
33
|
const { dialogState, unmount } = $derived(_state)
|
|
34
34
|
|
|
35
|
+
$effect(() => {
|
|
36
|
+
_state.panelRef = element ?? null
|
|
37
|
+
return () => {
|
|
38
|
+
_state.panelRef = null
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
35
42
|
const slot = $derived({ open: dialogState === DialogStates.Open } satisfies PanelRenderPropArg)
|
|
36
43
|
|
|
37
44
|
// Prevent the click events inside the Dialog.Panel from bubbling through the React Tree which
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useId } from "../hooks/use-id.js"
|
|
3
|
+
import { useMainTreeNode, useRootContainers } from "../hooks/use-root-containers.svelte.js"
|
|
4
|
+
import { clearOpenClosedContext, State, useOpenClosed } from "../internal/open-closed.js"
|
|
5
|
+
import { useNestedPortals } from "../portal/InternalPortal.svelte"
|
|
6
|
+
import { getOwnerDocument } from "../utils/owner.js"
|
|
7
|
+
import { useInertOthers } from "../hooks/use-inert-others.svelte.js"
|
|
8
|
+
import { useOutsideClick } from "../hooks/use-outside-click.svelte.js"
|
|
9
|
+
import { useEscape } from "../hooks/use-escape.svelte.js"
|
|
10
|
+
import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js"
|
|
11
|
+
import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js"
|
|
12
|
+
import { setContext } from "svelte"
|
|
13
|
+
import { useIsTouchDevice } from "../hooks/use-is-touch-device.svelte.js"
|
|
14
|
+
import FocusTrap, { FocusTrapFeatures } from "../focus-trap/FocusTrap.svelte"
|
|
15
|
+
import Portal from "../portal/Portal.svelte"
|
|
16
|
+
import PortalGroup from "../portal/PortalGroup.svelte"
|
|
17
|
+
import ForcePortalRoot from "../internal/ForcePortalRoot.svelte"
|
|
18
|
+
import { createCloseContext } from "../internal/close-provider.js"
|
|
19
|
+
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
20
|
+
import { DialogStates, type DialogContext, type StateDefinition } from "./context.svelte.js"
|
|
21
|
+
import { useDescriptions } from "../description/context.svelte.js"
|
|
22
|
+
import { BROWSER } from "esm-env"
|
|
23
|
+
import { DEFAULT_DIALOG_TAG, DialogRenderFeatures, type DialogProps, type DialogRenderPropArg } from "./Dialog.svelte"
|
|
24
|
+
import PortalWrapper from "../portal/PortalWrapper.svelte"
|
|
25
|
+
|
|
26
|
+
const internalId = useId()
|
|
27
|
+
let {
|
|
28
|
+
element = $bindable(),
|
|
29
|
+
id = `headlessui-dialog-${internalId}`,
|
|
30
|
+
open: theirOpen,
|
|
31
|
+
onclose,
|
|
32
|
+
initialFocus,
|
|
33
|
+
role: theirRole = "dialog",
|
|
34
|
+
autofocus = true,
|
|
35
|
+
__demoMode = false,
|
|
36
|
+
unmount = false,
|
|
37
|
+
...theirProps
|
|
38
|
+
}: DialogProps = $props()
|
|
39
|
+
|
|
40
|
+
let didWarnOnRole = $state(false)
|
|
41
|
+
|
|
42
|
+
const role = $derived.by(() => {
|
|
43
|
+
if (theirRole === "dialog" || theirRole === "alertdialog") {
|
|
44
|
+
return theirRole
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!didWarnOnRole) {
|
|
48
|
+
didWarnOnRole = true
|
|
49
|
+
console.warn(
|
|
50
|
+
`Invalid role [${theirRole}] passed to <Dialog />. Only \`dialog\` and and \`alertdialog\` are supported. Using \`dialog\` instead.`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return "dialog"
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Update the `open` prop based on the open closed state
|
|
58
|
+
const usesOpenClosedState = useOpenClosed()
|
|
59
|
+
const open = $derived(
|
|
60
|
+
theirOpen === undefined && usesOpenClosedState !== null
|
|
61
|
+
? (usesOpenClosedState.value & State.Open) === State.Open
|
|
62
|
+
: theirOpen
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const ownerDocument = $derived(getOwnerDocument(element))
|
|
66
|
+
|
|
67
|
+
const dialogState = $derived(open ? DialogStates.Open : DialogStates.Closed)
|
|
68
|
+
|
|
69
|
+
let _state = $state({
|
|
70
|
+
titleId: null,
|
|
71
|
+
panelRef: null,
|
|
72
|
+
} as StateDefinition)
|
|
73
|
+
|
|
74
|
+
const close = $derived(() => onclose(false))
|
|
75
|
+
|
|
76
|
+
const setTitleId = (id: string | null) => (_state.titleId = id)
|
|
77
|
+
|
|
78
|
+
const ready = BROWSER
|
|
79
|
+
const enabled = $derived(ready ? dialogState === DialogStates.Open : false)
|
|
80
|
+
const nestedPortals = useNestedPortals()
|
|
81
|
+
const { portals } = $derived(nestedPortals)
|
|
82
|
+
|
|
83
|
+
// We use this because reading these values during initial render(s)
|
|
84
|
+
// can result in `null` rather then the actual elements
|
|
85
|
+
// This doesn't happen when using certain components like a
|
|
86
|
+
// `<Dialog.Title>` because they cause the parent to re-render
|
|
87
|
+
const defaultContainer = $derived(_state.panelRef ?? element)
|
|
88
|
+
|
|
89
|
+
const mainTreeNode = useMainTreeNode()
|
|
90
|
+
let { resolvedContainers: resolvedRootContainers } = $derived(
|
|
91
|
+
useRootContainers({
|
|
92
|
+
get mainTreeNode() {
|
|
93
|
+
return mainTreeNode.node
|
|
94
|
+
},
|
|
95
|
+
get portals() {
|
|
96
|
+
return portals
|
|
97
|
+
},
|
|
98
|
+
get defaultContainers() {
|
|
99
|
+
return defaultContainer ? [defaultContainer] : []
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// When the `Dialog` is wrapped in a `Transition` (or another Headless UI component that exposes
|
|
105
|
+
// the OpenClosed state) then we get some information via context about its state. When the
|
|
106
|
+
// `Transition` is about to close, then the `State.Closing` state will be exposed. This allows us
|
|
107
|
+
// to enable/disable certain functionality in the `Dialog` upfront instead of waiting until the
|
|
108
|
+
// `Transition` is done transitioning.
|
|
109
|
+
const isClosing = $derived(
|
|
110
|
+
usesOpenClosedState !== null ? (usesOpenClosedState.value & State.Closing) === State.Closing : false
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// Ensure other elements can't be interacted with
|
|
114
|
+
const inertOthersEnabled = $derived(__demoMode ? false : isClosing ? false : enabled)
|
|
115
|
+
useInertOthers({
|
|
116
|
+
get enabled() {
|
|
117
|
+
return inertOthersEnabled
|
|
118
|
+
},
|
|
119
|
+
elements: {
|
|
120
|
+
get allowed() {
|
|
121
|
+
return [
|
|
122
|
+
// Allow the headlessui-portal of the Dialog to be interactive. This
|
|
123
|
+
// contains the current dialog and the necessary focus guard elements.
|
|
124
|
+
element?.closest<HTMLElement>("[data-headlessui-portal]") ?? null,
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
get disallowed() {
|
|
128
|
+
return [
|
|
129
|
+
// Disallow the "main" tree root node
|
|
130
|
+
mainTreeNode.node?.closest<HTMLElement>("body > *:not(#headlessui-portal-root)") ?? null,
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Close Dialog on outside click
|
|
137
|
+
useOutsideClick({
|
|
138
|
+
get enabled() {
|
|
139
|
+
return enabled
|
|
140
|
+
},
|
|
141
|
+
get containers() {
|
|
142
|
+
return resolvedRootContainers
|
|
143
|
+
},
|
|
144
|
+
cb(event) {
|
|
145
|
+
console.log("close")
|
|
146
|
+
event.preventDefault()
|
|
147
|
+
close()
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Handle `Escape` to close
|
|
152
|
+
useEscape({
|
|
153
|
+
get enabled() {
|
|
154
|
+
return enabled
|
|
155
|
+
},
|
|
156
|
+
get view() {
|
|
157
|
+
return ownerDocument?.defaultView ?? null
|
|
158
|
+
},
|
|
159
|
+
cb(event) {
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
event.stopPropagation()
|
|
162
|
+
|
|
163
|
+
// Ensure that we blur the current activeElement to prevent maintaining
|
|
164
|
+
// focus and potentially scrolling the page to the end (because the Dialog
|
|
165
|
+
// is rendered in a Portal at the end of the document.body and the browser
|
|
166
|
+
// tries to keep the focused element in view)
|
|
167
|
+
//
|
|
168
|
+
// Typically only happens in Safari.
|
|
169
|
+
if (
|
|
170
|
+
document.activeElement &&
|
|
171
|
+
"blur" in document.activeElement &&
|
|
172
|
+
typeof document.activeElement.blur === "function"
|
|
173
|
+
) {
|
|
174
|
+
document.activeElement.blur()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
close()
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Scroll lock
|
|
182
|
+
const scrollLockEnabled = $derived(__demoMode ? false : isClosing ? false : enabled)
|
|
183
|
+
useScrollLock({
|
|
184
|
+
get enabled() {
|
|
185
|
+
return scrollLockEnabled
|
|
186
|
+
},
|
|
187
|
+
get ownerDocument() {
|
|
188
|
+
return ownerDocument
|
|
189
|
+
},
|
|
190
|
+
resolveAllowedContainers() {
|
|
191
|
+
return resolvedRootContainers
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Ensure we close the dialog as soon as the dialog itself becomes hidden
|
|
196
|
+
useOnDisappear({
|
|
197
|
+
get enabled() {
|
|
198
|
+
return enabled
|
|
199
|
+
},
|
|
200
|
+
get ref() {
|
|
201
|
+
return element
|
|
202
|
+
},
|
|
203
|
+
get ondisappear() {
|
|
204
|
+
return close
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const describedby = useDescriptions()
|
|
209
|
+
|
|
210
|
+
setContext<DialogContext>("DialogContext", {
|
|
211
|
+
get titleId() {
|
|
212
|
+
return _state.titleId
|
|
213
|
+
},
|
|
214
|
+
get panelRef() {
|
|
215
|
+
return _state.panelRef
|
|
216
|
+
},
|
|
217
|
+
set panelRef(value) {
|
|
218
|
+
_state.panelRef = value
|
|
219
|
+
},
|
|
220
|
+
get dialogState() {
|
|
221
|
+
return dialogState
|
|
222
|
+
},
|
|
223
|
+
get close() {
|
|
224
|
+
return close
|
|
225
|
+
},
|
|
226
|
+
get unmount() {
|
|
227
|
+
return unmount
|
|
228
|
+
},
|
|
229
|
+
setTitleId,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const slot = $derived({ open: dialogState === DialogStates.Open } satisfies DialogRenderPropArg)
|
|
233
|
+
|
|
234
|
+
const ourProps = $derived({
|
|
235
|
+
id,
|
|
236
|
+
role,
|
|
237
|
+
tabIndex: -1,
|
|
238
|
+
"aria-modal": __demoMode ? undefined : dialogState === DialogStates.Open ? true : undefined,
|
|
239
|
+
"aria-labelledby": _state.titleId,
|
|
240
|
+
"aria-describedby": describedby.value,
|
|
241
|
+
unmount,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const shouldMoveFocusInside = !useIsTouchDevice().value
|
|
245
|
+
const focusTrapFeatures = $derived.by(() => {
|
|
246
|
+
let focusTrapFeatures = FocusTrapFeatures.None
|
|
247
|
+
|
|
248
|
+
if (enabled && !__demoMode) {
|
|
249
|
+
focusTrapFeatures |= FocusTrapFeatures.RestoreFocus
|
|
250
|
+
focusTrapFeatures |= FocusTrapFeatures.TabLock
|
|
251
|
+
|
|
252
|
+
if (autofocus) {
|
|
253
|
+
focusTrapFeatures |= FocusTrapFeatures.AutoFocus
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (shouldMoveFocusInside) {
|
|
257
|
+
focusTrapFeatures |= FocusTrapFeatures.InitialFocus
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return focusTrapFeatures
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
clearOpenClosedContext()
|
|
265
|
+
createCloseContext({
|
|
266
|
+
get close() {
|
|
267
|
+
return close
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
</script>
|
|
271
|
+
|
|
272
|
+
<ForcePortalRoot force={true}>
|
|
273
|
+
<Portal>
|
|
274
|
+
<PortalGroup target={element ?? null}>
|
|
275
|
+
<ForcePortalRoot force={false}>
|
|
276
|
+
<PortalWrapper context={nestedPortals}>
|
|
277
|
+
<FocusTrap
|
|
278
|
+
{initialFocus}
|
|
279
|
+
initialFocusFallback={element}
|
|
280
|
+
containers={resolvedRootContainers}
|
|
281
|
+
features={focusTrapFeatures}
|
|
282
|
+
>
|
|
283
|
+
<ElementOrComponent
|
|
284
|
+
{ourProps}
|
|
285
|
+
theirProps={{ ...theirProps }}
|
|
286
|
+
slots={slot}
|
|
287
|
+
defaultTag={DEFAULT_DIALOG_TAG}
|
|
288
|
+
features={DialogRenderFeatures}
|
|
289
|
+
visible={dialogState === DialogStates.Open}
|
|
290
|
+
name="Dialog"
|
|
291
|
+
bind:element
|
|
292
|
+
/>
|
|
293
|
+
</FocusTrap>
|
|
294
|
+
</PortalWrapper>
|
|
295
|
+
</ForcePortalRoot>
|
|
296
|
+
</PortalGroup>
|
|
297
|
+
</Portal>
|
|
298
|
+
</ForcePortalRoot>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getOwnerDocument } from "../utils/owner.js";
|
|
2
2
|
export { default as MainTreeProvider, useMainTreeNode } from "../internal/MainTreeProvider.svelte";
|
|
3
3
|
export function useRootContainers(options = {}) {
|
|
4
|
+
$inspect(options);
|
|
4
5
|
const { defaultContainers = [], portals,
|
|
5
6
|
// Reference to a node in the "main" tree, not in the portalled Dialog tree.
|
|
6
7
|
mainTreeNode, } = $derived(options);
|
|
@@ -11,7 +12,8 @@ export function useRootContainers(options = {}) {
|
|
|
11
12
|
for (const container of defaultContainers) {
|
|
12
13
|
if (!container)
|
|
13
14
|
continue;
|
|
14
|
-
|
|
15
|
+
if (container instanceof HTMLElement)
|
|
16
|
+
containers.push(container);
|
|
15
17
|
}
|
|
16
18
|
// Resolve portal containers
|
|
17
19
|
if (portals) {
|
|
@@ -60,7 +60,7 @@ export declare function useFloatingReferenceProps(): {
|
|
|
60
60
|
readonly getReferenceProps: (userProps?: import("svelte/elements.js").HTMLAttributes<Element>) => Record<string, unknown>;
|
|
61
61
|
};
|
|
62
62
|
export declare function useFloatingPanelProps(): (...args: Parameters<(userProps?: import("svelte/elements.js").HTMLAttributes<HTMLElement>) => Record<string, unknown>>) => Record<string, unknown> & {
|
|
63
|
-
"data-anchor": Placement | "top
|
|
63
|
+
"data-anchor": Placement | "top start" | "top end" | "right start" | "right end" | "bottom start" | "bottom end" | "left start" | "left end" | "selection" | "selection start" | "selection end" | undefined;
|
|
64
64
|
};
|
|
65
65
|
export declare function useFloatingPanel(options?: {
|
|
66
66
|
placement: (AnchorPropsWithSelection & InternalFloatingPanelProps) | null;
|
|
@@ -75,7 +75,7 @@ export declare function useResolvedConfig(options: {
|
|
|
75
75
|
config: (Exclude<AnchorPropsWithSelection, boolean | string> & InternalFloatingPanelProps) | null;
|
|
76
76
|
element?: HTMLElement | null;
|
|
77
77
|
}): {
|
|
78
|
-
readonly to: Placement | "top
|
|
78
|
+
readonly to: Placement | "top start" | "top end" | "right start" | "right end" | "bottom start" | "bottom end" | "left start" | "left end" | "selection" | "selection start" | "selection end" | undefined;
|
|
79
79
|
readonly gap: number | undefined;
|
|
80
80
|
readonly offset: number | undefined;
|
|
81
81
|
readonly padding: number | undefined;
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { createCloseContext } from "../internal/close-provider.js"
|
|
39
39
|
import { createOpenClosedContext, State } from "../internal/open-closed.js"
|
|
40
40
|
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
41
|
+
import PortalWrapper from "../portal/PortalWrapper.svelte"
|
|
41
42
|
|
|
42
43
|
let { element = $bindable(), __demoMode = false, ...theirProps }: PopoverProps = $props()
|
|
43
44
|
|
|
@@ -224,5 +225,7 @@
|
|
|
224
225
|
</script>
|
|
225
226
|
|
|
226
227
|
<MainTreeProvider node={mainTreeNode.node}>
|
|
227
|
-
<
|
|
228
|
+
<PortalWrapper context={nestedPortals}>
|
|
229
|
+
<ElementOrComponent {theirProps} slots={slot} defaultTag={DEFAULT_POPOVER_TAG} name="Popover" bind:element />
|
|
230
|
+
</PortalWrapper>
|
|
228
231
|
</MainTreeProvider>
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
// ---
|
|
58
58
|
|
|
59
|
-
type PortalParentContext = {
|
|
59
|
+
export type PortalParentContext = {
|
|
60
60
|
register: (portal: HTMLElement) => () => void
|
|
61
61
|
unregister: (portal: HTMLElement) => void
|
|
62
62
|
readonly portals: HTMLElement[]
|
|
@@ -85,7 +85,6 @@
|
|
|
85
85
|
return portals
|
|
86
86
|
},
|
|
87
87
|
}
|
|
88
|
-
setContext("PortalParentContext", context)
|
|
89
88
|
|
|
90
89
|
return context
|
|
91
90
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext, type Snippet } from "svelte"
|
|
3
|
+
import type { PortalParentContext } from "./InternalPortal.svelte"
|
|
4
|
+
|
|
5
|
+
const { context, children }: { context: PortalParentContext; children: Snippet } = $props()
|
|
6
|
+
|
|
7
|
+
setContext("PortalParentContext", context)
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{@render children()}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import type { PortalParentContext } from "./InternalPortal.svelte";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
context: PortalParentContext;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const PortalWrapper: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type PortalWrapper = ReturnType<typeof PortalWrapper>;
|
|
9
|
+
export default PortalWrapper;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pzerelles/headlessui-svelte",
|
|
3
|
-
"version": "2.1.2-next.
|
|
3
|
+
"version": "2.1.2-next.59",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run package",
|
|
@@ -37,36 +37,36 @@
|
|
|
37
37
|
"@playwright/test": "^1.52.0",
|
|
38
38
|
"@pzerelles/heroicons-svelte": "^2.2.1",
|
|
39
39
|
"@sveltejs/adapter-auto": "^3.3.1",
|
|
40
|
-
"@sveltejs/kit": "^2.21.
|
|
40
|
+
"@sveltejs/kit": "^2.21.1",
|
|
41
41
|
"@sveltejs/package": "^2.3.11",
|
|
42
42
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
43
|
-
"@tailwindcss/vite": "^4.1.
|
|
43
|
+
"@tailwindcss/vite": "^4.1.8",
|
|
44
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
|
-
"@testing-library/svelte": "^5.2.
|
|
45
|
+
"@testing-library/svelte": "^5.2.8",
|
|
46
46
|
"@types/eslint": "^9.6.1",
|
|
47
|
-
"@types/node": "^22.15.
|
|
48
|
-
"eslint": "^9.
|
|
47
|
+
"@types/node": "^22.15.29",
|
|
48
|
+
"eslint": "^9.28.0",
|
|
49
49
|
"eslint-config-prettier": "^9.1.0",
|
|
50
50
|
"eslint-plugin-svelte": "^2.46.1",
|
|
51
|
-
"globals": "^16.
|
|
51
|
+
"globals": "^16.2.0",
|
|
52
52
|
"jsdom": "^25.0.1",
|
|
53
53
|
"outdent": "^0.8.0",
|
|
54
54
|
"prettier": "^3.5.3",
|
|
55
|
-
"prettier-plugin-svelte": "^3.
|
|
56
|
-
"prettier-plugin-tailwindcss": "^0.6.
|
|
55
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
56
|
+
"prettier-plugin-tailwindcss": "^0.6.12",
|
|
57
57
|
"publint": "^0.3.12",
|
|
58
|
-
"svelte": "^5.
|
|
59
|
-
"svelte-check": "^4.1
|
|
60
|
-
"tailwindcss": "^4.1.
|
|
58
|
+
"svelte": "^5.33.14",
|
|
59
|
+
"svelte-check": "^4.2.1",
|
|
60
|
+
"tailwindcss": "^4.1.8",
|
|
61
61
|
"tslib": "^2.8.1",
|
|
62
62
|
"typescript": "^5.8.3",
|
|
63
|
-
"typescript-eslint": "^8.
|
|
63
|
+
"typescript-eslint": "^8.33.1",
|
|
64
64
|
"vite": "^6.3.5",
|
|
65
|
-
"vitest": "^3.
|
|
65
|
+
"vitest": "^3.2.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@floating-ui/core": "^1.7.
|
|
69
|
-
"@floating-ui/dom": "^1.7.
|
|
68
|
+
"@floating-ui/core": "^1.7.1",
|
|
69
|
+
"@floating-ui/dom": "^1.7.1",
|
|
70
70
|
"@floating-ui/utils": "^0.2.9",
|
|
71
71
|
"esm-env": "^1.2.2",
|
|
72
72
|
"nanoid": "^5.1.5"
|