@pzerelles/headlessui-svelte 2.1.2-next.57 → 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.
@@ -27,35 +27,16 @@
27
27
  </script>
28
28
 
29
29
  <script lang="ts">
30
- import { useId } from "../hooks/use-id.js"
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 { BROWSER } from "esm-env"
33
+ import InternalDialog from "./InternalDialog.svelte"
53
34
 
54
- let { element = $bindable(), transition = false, open: theirOpen, ...rest }: DialogProps = $props()
35
+ let { element = $bindable(), transition = false, open, ...rest }: DialogProps = $props()
55
36
 
56
37
  // Validations
57
38
  const usesOpenClosedState = useOpenClosed()
58
- const hasOpen = $derived(theirOpen !== undefined || usesOpenClosedState)
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
- {#snippet internal(transitionProps?: Record<string, any>)}
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
- {@render internal(props)}
73
+ <InternalDialog {...rest} {...props} bind:element />
363
74
  {/snippet}
364
75
  </Transition>
365
76
  </MainTreeProvider>
366
77
  {:else}
367
78
  <MainTreeProvider>
368
- {@render internal()}
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>
@@ -0,0 +1,4 @@
1
+ import { type DialogProps } from "./Dialog.svelte";
2
+ declare const InternalDialog: import("svelte").Component<DialogProps, {}, "element">;
3
+ type InternalDialog = ReturnType<typeof InternalDialog>;
4
+ export default InternalDialog;
@@ -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
- containers.push(container);
15
+ if (container instanceof HTMLElement)
16
+ containers.push(container);
15
17
  }
16
18
  // Resolve portal containers
17
19
  if (portals) {
package/dist/index.d.ts CHANGED
@@ -20,7 +20,10 @@ export * from "./tabs/index.js";
20
20
  export * from "./textarea/index.js";
21
21
  export * from "./transition/index.js";
22
22
  export * from "./utils/index.js";
23
- export * from "./hooks/use-id.js";
23
+ export * from "./hooks/use-active-press.svelte.js";
24
24
  export * from "./hooks/use-disabled.js";
25
- export * from "./hooks/use-outside-click.svelte.js";
26
25
  export * from "./hooks/use-escape.svelte.js";
26
+ export * from "./hooks/use-focus-ring.svelte.js";
27
+ export * from "./hooks/use-hover.svelte.js";
28
+ export * from "./hooks/use-id.js";
29
+ export * from "./hooks/use-outside-click.svelte.js";
package/dist/index.js CHANGED
@@ -20,7 +20,10 @@ export * from "./tabs/index.js";
20
20
  export * from "./textarea/index.js";
21
21
  export * from "./transition/index.js";
22
22
  export * from "./utils/index.js";
23
- export * from "./hooks/use-id.js";
23
+ export * from "./hooks/use-active-press.svelte.js";
24
24
  export * from "./hooks/use-disabled.js";
25
- export * from "./hooks/use-outside-click.svelte.js";
26
25
  export * from "./hooks/use-escape.svelte.js";
26
+ export * from "./hooks/use-focus-ring.svelte.js";
27
+ export * from "./hooks/use-hover.svelte.js";
28
+ export * from "./hooks/use-id.js";
29
+ export * from "./hooks/use-outside-click.svelte.js";
@@ -93,6 +93,6 @@
93
93
  </script>
94
94
 
95
95
  {#if children}{@render children()}{/if}
96
- {#if resolvedMainTreeNode === null}
96
+ {#if resolvedMainTreeNode.node === null}
97
97
  <Hidden features={HiddenFeatures.Hidden} bind:element={el} />
98
98
  {/if}
@@ -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 end" | "top start" | "right end" | "right start" | "bottom end" | "bottom start" | "left end" | "left start" | "selection" | "selection end" | "selection start" | undefined;
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 end" | "top start" | "right end" | "right start" | "bottom end" | "bottom start" | "left end" | "left start" | "selection" | "selection end" | "selection start" | undefined;
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
- <ElementOrComponent {theirProps} slots={slot} defaultTag={DEFAULT_POPOVER_TAG} name="Popover" bind:element />
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
  }
@@ -1,5 +1,5 @@
1
1
  import type { Props } from "../utils/types.js";
2
- type PortalParentContext = {
2
+ export type PortalParentContext = {
3
3
  register: (portal: HTMLElement) => () => void;
4
4
  unregister: (portal: HTMLElement) => void;
5
5
  readonly portals: HTMLElement[];
@@ -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.57",
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.0",
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.6",
43
+ "@tailwindcss/vite": "^4.1.8",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
- "@testing-library/svelte": "^5.2.7",
45
+ "@testing-library/svelte": "^5.2.8",
46
46
  "@types/eslint": "^9.6.1",
47
- "@types/node": "^22.15.18",
48
- "eslint": "^9.26.0",
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.1.0",
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.3.3",
56
- "prettier-plugin-tailwindcss": "^0.6.11",
55
+ "prettier-plugin-svelte": "^3.4.0",
56
+ "prettier-plugin-tailwindcss": "^0.6.12",
57
57
  "publint": "^0.3.12",
58
- "svelte": "^5.28.6",
59
- "svelte-check": "^4.1.7",
60
- "tailwindcss": "^4.1.6",
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.32.1",
63
+ "typescript-eslint": "^8.33.1",
64
64
  "vite": "^6.3.5",
65
- "vitest": "^3.1.3"
65
+ "vitest": "^3.2.0"
66
66
  },
67
67
  "dependencies": {
68
- "@floating-ui/core": "^1.7.0",
69
- "@floating-ui/dom": "^1.7.0",
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"