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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/button/Button.svelte +84 -54
  2. package/dist/checkbox/Checkbox.svelte +174 -120
  3. package/dist/close-button/CloseButton.svelte +12 -6
  4. package/dist/combobox/Combobox.svelte +50 -3
  5. package/dist/data-interactive/DataInteractive.svelte +57 -29
  6. package/dist/description/Description.svelte +32 -21
  7. package/dist/dialog/Dialog.svelte +69 -34
  8. package/dist/dialog/DialogBackdrop.svelte +29 -12
  9. package/dist/dialog/DialogPanel.svelte +49 -26
  10. package/dist/dialog/DialogTitle.svelte +38 -23
  11. package/dist/dialog/InternalDialog.svelte +263 -202
  12. package/dist/field/Field.svelte +49 -26
  13. package/dist/fieldset/Fieldset.svelte +50 -29
  14. package/dist/focus-trap/FocusTrap.svelte +436 -290
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/input/Input.svelte +85 -53
  18. package/dist/internal/FocusSentinel.svelte +16 -8
  19. package/dist/internal/ForcePortalRoot.svelte +7 -3
  20. package/dist/internal/FormFields.svelte +31 -20
  21. package/dist/internal/FormResolver.svelte +20 -15
  22. package/dist/internal/Hidden.svelte +44 -27
  23. package/dist/internal/Hidden.svelte.d.ts +2 -5
  24. package/dist/internal/HiddenFeatures.d.ts +5 -0
  25. package/dist/internal/HiddenFeatures.js +9 -0
  26. package/dist/internal/HoistFormFields.svelte +7 -4
  27. package/dist/internal/MainTreeProvider.svelte +89 -36
  28. package/dist/internal/Portal.svelte +18 -14
  29. package/dist/label/Label.svelte +91 -57
  30. package/dist/legend/Legend.svelte +18 -3
  31. package/dist/listbox/Listbox.svelte +600 -409
  32. package/dist/listbox/ListboxButton.svelte +176 -127
  33. package/dist/listbox/ListboxOption.svelte +166 -125
  34. package/dist/listbox/ListboxOptions.svelte +340 -244
  35. package/dist/listbox/ListboxSelectedOption.svelte +38 -15
  36. package/dist/menu/Menu.svelte +307 -218
  37. package/dist/menu/MenuButton.svelte +157 -115
  38. package/dist/menu/MenuHeading.svelte +34 -14
  39. package/dist/menu/MenuItem.svelte +145 -107
  40. package/dist/menu/MenuItems.svelte +298 -224
  41. package/dist/menu/MenuSection.svelte +26 -9
  42. package/dist/menu/MenuSeparator.svelte +20 -4
  43. package/dist/portal/InternalPortal.svelte +141 -85
  44. package/dist/portal/Portal.svelte +5 -2
  45. package/dist/portal/PortalGroup.svelte +30 -9
  46. package/dist/switch/Switch.svelte +179 -122
  47. package/dist/switch/Switch.svelte.d.ts +4 -4
  48. package/dist/switch/SwitchGroup.svelte +44 -31
  49. package/dist/tabs/Tab.svelte +195 -143
  50. package/dist/tabs/TabGroup.svelte +292 -205
  51. package/dist/tabs/TabList.svelte +31 -11
  52. package/dist/tabs/TabPanel.svelte +68 -43
  53. package/dist/tabs/TabPanels.svelte +18 -7
  54. package/dist/textarea/Textarea.svelte +97 -0
  55. package/dist/textarea/Textarea.svelte.d.ts +47 -0
  56. package/dist/textarea/index.d.ts +1 -0
  57. package/dist/textarea/index.js +1 -0
  58. package/dist/transition/InternalTransitionChild.svelte +259 -170
  59. package/dist/transition/Transition.svelte +96 -66
  60. package/dist/transition/TransitionChild.svelte +31 -11
  61. package/dist/utils/ElementOrComponent.svelte +44 -23
  62. package/dist/utils/Generic.svelte +29 -17
  63. package/dist/utils/StableCollection.svelte +54 -36
  64. package/package.json +10 -10
@@ -1,255 +1,351 @@
1
- <script lang="ts" module>import { mergeProps, RenderFeatures } from "../utils/render.js";
2
- import {
3
- useFloatingPanel,
4
- useFloatingPanelProps,
5
- useResolvedAnchor
6
- } from "../internal/floating.svelte.js";
7
- const DEFAULT_OPTIONS_TAG = "div";
8
- let OptionsRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static;
9
- </script>
1
+ <script lang="ts" module>
2
+ import type { ElementType, Props } from "../utils/types.js"
3
+ import { mergeProps, RenderFeatures, type PropsForFeatures } from "../utils/render.js"
4
+ import {
5
+ useFloatingPanel,
6
+ useFloatingPanelProps,
7
+ useResolvedAnchor,
8
+ type AnchorPropsWithSelection,
9
+ } from "../internal/floating.svelte.js"
10
10
 
11
- <script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG">import { useId } from "../hooks/use-id.js";
12
- import { ListboxStates, useActions, useData, ValueMode } from "./Listbox.svelte";
13
- import { getOwnerDocument } from "../utils/owner.js";
14
- import { State, useOpenClosed } from "../internal/open-closed.js";
15
- import { transitionDataAttributes, useTransition } from "../hooks/use-transition.svelte.js";
16
- import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js";
17
- import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js";
18
- import { useInertOthers } from "../hooks/use-inert-others.svelte.js";
19
- import { useDidElementMove } from "../hooks/use-did-element-move.svelte.js";
20
- import { useFrozenData } from "../internal/frozen.svelte.js";
21
- import { useDisposables } from "../utils/disposables.js";
22
- import { match } from "../utils/match.js";
23
- import { Focus } from "../utils/calculate-active-index.js";
24
- import { focusFrom, Focus as FocusManagementFocus } from "../utils/focus-management.js";
25
- import { useElementSize } from "../hooks/use-element-size.svelte.js";
26
- import { setContext } from "svelte";
27
- import Hidden from "../internal/Hidden.svelte";
28
- import Portal from "../portal/Portal.svelte";
29
- import { stateFromSlot } from "../utils/state.js";
30
- import ElementOrComponent from "../utils/ElementOrComponent.svelte";
31
- const internalId = useId();
32
- let {
33
- as = DEFAULT_OPTIONS_TAG,
34
- ref = $bindable(),
35
- id = `headlessui-listbox-options-${internalId}`,
36
- anchor: rawAnchor,
37
- portal = false,
38
- modal = true,
39
- transition = false,
40
- static: isStatic = false,
41
- unmount = true,
42
- ...theirProps
43
- } = $props();
44
- const anchor = $derived(useResolvedAnchor(rawAnchor));
45
- $effect(() => {
46
- if (anchor) {
47
- portal = true;
48
- }
49
- });
50
- const data = useData("ListboxOptions");
51
- const actions = useActions("ListboxOptions");
52
- const ownerDocument = $derived(getOwnerDocument(data.optionsRef.current));
53
- const usesOpenClosedState = useOpenClosed();
54
- const show = $derived(
55
- usesOpenClosedState !== null ? (usesOpenClosedState.value & State.Open) === State.Open : data.listboxState === ListboxStates.Open
56
- );
57
- const _transition = useTransition({
58
- get enabled() {
59
- return transition;
60
- },
61
- get element() {
62
- return ref;
63
- },
64
- get show() {
65
- return show;
66
- }
67
- });
68
- const { visible, data: transitionData } = $derived(_transition);
69
- useOnDisappear({
70
- get enabled() {
71
- return visible;
72
- },
73
- get ref() {
74
- return data.buttonRef.current;
75
- },
76
- get ondisappear() {
77
- return actions.closeListbox;
78
- }
79
- });
80
- const scrollLockEnabled = $derived(data.__demoMode ? false : modal && data.listboxState === ListboxStates.Open);
81
- useScrollLock({
82
- get enabled() {
83
- return scrollLockEnabled;
84
- },
85
- get ownerDocument() {
86
- return ownerDocument;
11
+ const DEFAULT_OPTIONS_TAG = "div" as const
12
+ type OptionsRenderPropArg = {
13
+ open: boolean
87
14
  }
88
- });
89
- const inertOthersEnabled = $derived(data.__demoMode ? false : modal && data.listboxState === ListboxStates.Open);
90
- useInertOthers({
91
- get enabled() {
92
- return inertOthersEnabled;
93
- },
94
- elements: {
95
- get allowed() {
96
- return [data.buttonRef.current, data.optionsRef.current];
15
+ type OptionsPropsWeControl =
16
+ | "aria-activedescendant"
17
+ | "aria-labelledby"
18
+ | "aria-multiselectable"
19
+ | "aria-orientation"
20
+ | "role"
21
+ | "tabIndex"
22
+
23
+ let OptionsRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
24
+
25
+ export type ListboxOptionsProps<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG> = Props<
26
+ TTag,
27
+ OptionsRenderPropArg,
28
+ OptionsPropsWeControl,
29
+ {
30
+ id?: string
31
+ anchor?: AnchorPropsWithSelection
32
+ portal?: boolean
33
+ modal?: boolean
34
+ transition?: boolean
35
+ } & PropsForFeatures<typeof OptionsRenderFeatures>
36
+ >
37
+
38
+ export type ListboxOptionsChildren = Snippet<[OptionsRenderPropArg]>
39
+ </script>
40
+
41
+ <script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG">
42
+ import { useId } from "../hooks/use-id.js"
43
+ import { ListboxStates, useActions, useData, ValueMode, type ListboxDataContext } from "./Listbox.svelte"
44
+ import { getOwnerDocument } from "../utils/owner.js"
45
+ import { State, useOpenClosed } from "../internal/open-closed.js"
46
+ import { transitionDataAttributes, useTransition } from "../hooks/use-transition.svelte.js"
47
+ import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js"
48
+ import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js"
49
+ import { useInertOthers } from "../hooks/use-inert-others.svelte.js"
50
+ import { useDidElementMove } from "../hooks/use-did-element-move.svelte.js"
51
+ import { useFrozenData } from "../internal/frozen.svelte.js"
52
+ import { useDisposables } from "../utils/disposables.js"
53
+ import { match } from "../utils/match.js"
54
+ import { Focus } from "../utils/calculate-active-index.js"
55
+ import { focusFrom, Focus as FocusManagementFocus } from "../utils/focus-management.js"
56
+ import { useElementSize } from "../hooks/use-element-size.svelte.js"
57
+ import { setContext, type Snippet } from "svelte"
58
+ import Hidden from "../internal/Hidden.svelte"
59
+ import Portal from "../portal/Portal.svelte"
60
+ import { stateFromSlot } from "../utils/state.js"
61
+ import ElementOrComponent from "../utils/ElementOrComponent.svelte"
62
+
63
+ const internalId = useId()
64
+ let {
65
+ as = DEFAULT_OPTIONS_TAG as TTag,
66
+ ref = $bindable(),
67
+ id = `headlessui-listbox-options-${internalId}`,
68
+ anchor: rawAnchor,
69
+ portal = false,
70
+ modal = true,
71
+ transition = false,
72
+ static: isStatic = false,
73
+ unmount = true,
74
+ ...theirProps
75
+ }: { as?: TTag } & ListboxOptionsProps<TTag> = $props()
76
+ const anchor = $derived(useResolvedAnchor(rawAnchor))
77
+
78
+ // Always enable `portal` functionality, when `anchor` is enabled
79
+ $effect(() => {
80
+ if (anchor) {
81
+ portal = true
97
82
  }
83
+ })
84
+
85
+ const data = useData("ListboxOptions")
86
+ const actions = useActions("ListboxOptions")
87
+
88
+ const ownerDocument = $derived(getOwnerDocument(data.optionsRef.current))
89
+
90
+ const usesOpenClosedState = useOpenClosed()
91
+ const show = $derived(
92
+ usesOpenClosedState !== null
93
+ ? (usesOpenClosedState.value & State.Open) === State.Open
94
+ : data.listboxState === ListboxStates.Open
95
+ )
96
+ const _transition = useTransition({
97
+ get enabled() {
98
+ return transition
99
+ },
100
+ get element() {
101
+ return ref
102
+ },
103
+ get show() {
104
+ return show
105
+ },
106
+ })
107
+ const { visible, data: transitionData } = $derived(_transition)
108
+
109
+ // Ensure we close the listbox as soon as the button becomes hidden
110
+ useOnDisappear({
111
+ get enabled() {
112
+ return visible
113
+ },
114
+ get ref() {
115
+ return data.buttonRef.current
116
+ },
117
+ get ondisappear() {
118
+ return actions.closeListbox
119
+ },
120
+ })
121
+
122
+ // Enable scroll locking when the listbox is visible, and `modal` is enabled
123
+ const scrollLockEnabled = $derived(data.__demoMode ? false : modal && data.listboxState === ListboxStates.Open)
124
+ useScrollLock({
125
+ get enabled() {
126
+ return scrollLockEnabled
127
+ },
128
+ get ownerDocument() {
129
+ return ownerDocument
130
+ },
131
+ })
132
+
133
+ // Mark other elements as inert when the listbox is visible, and `modal` is enabled
134
+ const inertOthersEnabled = $derived(data.__demoMode ? false : modal && data.listboxState === ListboxStates.Open)
135
+ useInertOthers({
136
+ get enabled() {
137
+ return inertOthersEnabled
138
+ },
139
+ elements: {
140
+ get allowed() {
141
+ return [data.buttonRef.current, data.optionsRef.current]
142
+ },
143
+ },
144
+ })
145
+
146
+ // We keep track whether the button moved or not, we only check this when the menu state becomes
147
+ // closed. If the button moved, then we want to cancel pending transitions to prevent that the
148
+ // attached `MenuItems` is still transitioning while the button moved away.
149
+ //
150
+ // If we don't cancel these transitions then there will be a period where the `MenuItems` is
151
+ // visible and moving around because it is trying to re-position itself based on the new position.
152
+ //
153
+ // This can be solved by only transitioning the `opacity` instead of everything, but if you _do_
154
+ // want to transition the y-axis for example you will run into the same issue again.
155
+ const didElementMoveEnabled = $derived(data.listboxState !== ListboxStates.Open)
156
+ const didButtonMove = useDidElementMove({
157
+ get enabled() {
158
+ return didElementMoveEnabled
159
+ },
160
+ get element() {
161
+ return data.buttonRef.current
162
+ },
163
+ })
164
+
165
+ // Now that we know that the button did move or not, we can either disable the panel and all of
166
+ // its transitions, or rely on the `visible` state to hide the panel whenever necessary.
167
+ const panelEnabled = $derived(didButtonMove.value ? false : visible)
168
+
169
+ // We should freeze when the listbox is visible but "closed". This means that
170
+ // a transition is currently happening and the component is still visible (for
171
+ // the transition) but closed from a functionality perspective.
172
+ const shouldFreeze = $derived(visible && data.listboxState === ListboxStates.Closed)
173
+
174
+ // Frozen state, the selected value will only update visually when the user re-opens the <Listbox />
175
+ const frozenValue = useFrozenData({
176
+ get freeze() {
177
+ return shouldFreeze
178
+ },
179
+ get data() {
180
+ return data.value
181
+ },
182
+ })
183
+
184
+ const isSelected = (compareValue: unknown) => data.compare(frozenValue, compareValue)
185
+
186
+ const selectedOptionIndex = () => {
187
+ if (anchor == null) return null
188
+ if (!anchor?.to?.includes("selection")) return null
189
+
190
+ // Only compute the selected option index when using `selection` in the
191
+ // `anchor` prop.
192
+ let idx = data.options.findIndex((option) => isSelected(option.dataRef.current.value))
193
+ // Ensure that if no data is selected, we default to the first item.
194
+ if (idx === -1) idx = 0
195
+ return idx
98
196
  }
99
- });
100
- const didElementMoveEnabled = $derived(data.listboxState !== ListboxStates.Open);
101
- const didButtonMove = useDidElementMove({
102
- get enabled() {
103
- return didElementMoveEnabled;
104
- },
105
- get element() {
106
- return data.buttonRef.current;
107
- }
108
- });
109
- const panelEnabled = $derived(didButtonMove.value ? false : visible);
110
- const shouldFreeze = $derived(visible && data.listboxState === ListboxStates.Closed);
111
- const frozenValue = useFrozenData({
112
- get freeze() {
113
- return shouldFreeze;
114
- },
115
- get data() {
116
- return data.value;
117
- }
118
- });
119
- const isSelected = (compareValue) => data.compare(frozenValue, compareValue);
120
- const selectedOptionIndex = () => {
121
- if (anchor == null) return null;
122
- if (!anchor?.to?.includes("selection")) return null;
123
- let idx = data.options.findIndex((option) => isSelected(option.dataRef.current.value));
124
- if (idx === -1) idx = 0;
125
- return idx;
126
- };
127
- const anchorOptions = $derived.by(() => {
128
- if (anchor == null) return void 0;
129
- if (selectedOptionIndex === null) return { ...anchor, inner: void 0 };
130
- let elements = Array.from(data.listRef.current.values());
131
- return {
132
- ...anchor,
133
- inner: {
134
- listRef: { current: elements },
135
- index: selectedOptionIndex
197
+
198
+ const anchorOptions = $derived.by(() => {
199
+ if (anchor == null) return undefined
200
+ if (selectedOptionIndex === null) return { ...anchor, inner: undefined }
201
+
202
+ let elements = Array.from(data.listRef.current.values())
203
+
204
+ return {
205
+ ...anchor,
206
+ inner: {
207
+ listRef: { current: elements },
208
+ index: selectedOptionIndex,
209
+ },
210
+ }
211
+ })
212
+
213
+ const floatingPanel = useFloatingPanel({
214
+ get placement() {
215
+ return anchorOptions ?? null
216
+ },
217
+ })
218
+ const { setFloating, style } = $derived(floatingPanel)
219
+ const getFloatingPanelProps = useFloatingPanelProps()
220
+
221
+ $effect(() => {
222
+ data.optionsRef.current = ref || null
223
+ if (anchor) setFloating(ref)
224
+ })
225
+
226
+ const searchDisposables = useDisposables()
227
+
228
+ const { listboxState, optionsRef } = $derived(data)
229
+ $effect(() => {
230
+ let container = optionsRef.current
231
+ if (!container) return
232
+ if (listboxState !== ListboxStates.Open) return
233
+ if (container === getOwnerDocument(container)?.activeElement) return
234
+
235
+ container?.focus({ preventScroll: true })
236
+ })
237
+
238
+ const handleKeyDown = (event: KeyboardEvent) => {
239
+ searchDisposables.dispose()
240
+
241
+ switch (event.key) {
242
+ // Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menu/#keyboard-interaction-12
243
+
244
+ case " ":
245
+ if (data.searchQuery !== "") {
246
+ event.preventDefault()
247
+ event.stopPropagation()
248
+ return actions.search(event.key)
249
+ }
250
+ // When in type ahead mode, fallthrough
251
+ case "Enter":
252
+ event.preventDefault()
253
+ event.stopPropagation()
254
+
255
+ if (data.activeOptionIndex !== null) {
256
+ let { dataRef } = data.options[data.activeOptionIndex]
257
+ actions.onChange(dataRef.current.value)
258
+ }
259
+ if (data.mode === ValueMode.Single) {
260
+ actions.closeListbox()
261
+ data.buttonRef.current?.focus({ preventScroll: true })
262
+ }
263
+ break
264
+
265
+ case match(data.orientation, { vertical: "ArrowDown", horizontal: "ArrowRight" }):
266
+ event.preventDefault()
267
+ event.stopPropagation()
268
+ return actions.goToOption(Focus.Next)
269
+
270
+ case match(data.orientation, { vertical: "ArrowUp", horizontal: "ArrowLeft" }):
271
+ event.preventDefault()
272
+ event.stopPropagation()
273
+ return actions.goToOption(Focus.Previous)
274
+
275
+ case "Home":
276
+ case "PageUp":
277
+ event.preventDefault()
278
+ event.stopPropagation()
279
+ return actions.goToOption(Focus.First)
280
+
281
+ case "End":
282
+ case "PageDown":
283
+ event.preventDefault()
284
+ event.stopPropagation()
285
+ return actions.goToOption(Focus.Last)
286
+
287
+ case "Escape":
288
+ event.preventDefault()
289
+ event.stopPropagation()
290
+ actions.closeListbox()
291
+ data.buttonRef.current?.focus({ preventScroll: true })
292
+ return
293
+
294
+ case "Tab":
295
+ event.preventDefault()
296
+ event.stopPropagation()
297
+ actions.closeListbox()
298
+ focusFrom(data.buttonRef.current!, event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next)
299
+ break
300
+
301
+ default:
302
+ if (event.key.length === 1) {
303
+ actions.search(event.key)
304
+ searchDisposables.setTimeout(() => actions.clearSearch(), 350)
305
+ }
306
+ break
136
307
  }
137
- };
138
- });
139
- const floatingPanel = useFloatingPanel({
140
- get placement() {
141
- return anchorOptions ?? null;
142
- }
143
- });
144
- const { setFloating, style } = $derived(floatingPanel);
145
- const getFloatingPanelProps = useFloatingPanelProps();
146
- $effect(() => {
147
- data.optionsRef.current = ref || null;
148
- if (anchor) setFloating(ref);
149
- });
150
- const searchDisposables = useDisposables();
151
- const { listboxState, optionsRef } = $derived(data);
152
- $effect(() => {
153
- let container = optionsRef.current;
154
- if (!container) return;
155
- if (listboxState !== ListboxStates.Open) return;
156
- if (container === getOwnerDocument(container)?.activeElement) return;
157
- container?.focus({ preventScroll: true });
158
- });
159
- const handleKeyDown = (event) => {
160
- searchDisposables.dispose();
161
- switch (event.key) {
162
- case " ":
163
- if (data.searchQuery !== "") {
164
- event.preventDefault();
165
- event.stopPropagation();
166
- return actions.search(event.key);
167
- }
168
- case "Enter":
169
- event.preventDefault();
170
- event.stopPropagation();
171
- if (data.activeOptionIndex !== null) {
172
- let { dataRef } = data.options[data.activeOptionIndex];
173
- actions.onChange(dataRef.current.value);
174
- }
175
- if (data.mode === ValueMode.Single) {
176
- actions.closeListbox();
177
- data.buttonRef.current?.focus({ preventScroll: true });
178
- }
179
- break;
180
- case match(data.orientation, { vertical: "ArrowDown", horizontal: "ArrowRight" }):
181
- event.preventDefault();
182
- event.stopPropagation();
183
- return actions.goToOption(Focus.Next);
184
- case match(data.orientation, { vertical: "ArrowUp", horizontal: "ArrowLeft" }):
185
- event.preventDefault();
186
- event.stopPropagation();
187
- return actions.goToOption(Focus.Previous);
188
- case "Home":
189
- case "PageUp":
190
- event.preventDefault();
191
- event.stopPropagation();
192
- return actions.goToOption(Focus.First);
193
- case "End":
194
- case "PageDown":
195
- event.preventDefault();
196
- event.stopPropagation();
197
- return actions.goToOption(Focus.Last);
198
- case "Escape":
199
- event.preventDefault();
200
- event.stopPropagation();
201
- actions.closeListbox();
202
- data.buttonRef.current?.focus({ preventScroll: true });
203
- return;
204
- case "Tab":
205
- event.preventDefault();
206
- event.stopPropagation();
207
- actions.closeListbox();
208
- focusFrom(data.buttonRef.current, event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next);
209
- break;
210
- default:
211
- if (event.key.length === 1) {
212
- actions.search(event.key);
213
- searchDisposables.setTimeout(() => actions.clearSearch(), 350);
214
- }
215
- break;
216
308
  }
217
- };
218
- const labelledby = $derived(data.buttonRef.current?.id);
219
- const slot = $derived({
220
- open: data.listboxState === ListboxStates.Open
221
- });
222
- const buttonSize = useElementSize({
223
- get element() {
224
- return data.buttonRef.current;
225
- },
226
- unit: true
227
- });
228
- const ourProps = $derived(
229
- mergeProps(anchor ? getFloatingPanelProps() : {}, {
230
- id,
231
- "aria-activedescendant": data.activeOptionIndex === null ? void 0 : data.options[data.activeOptionIndex]?.id,
232
- "aria-multiselectable": data.mode === ValueMode.Multi ? true : void 0,
233
- "aria-labelledby": labelledby,
234
- "aria-orientation": data.orientation,
235
- onkeydown: handleKeyDown,
236
- role: "listbox",
237
- // When the `Listbox` is closed, it should not be focusable. This allows us
238
- // to skip focusing the `ListboxOptions` when pressing the tab key on an
239
- // open `Listbox`, and go to the next focusable element.
240
- tabIndex: data.listboxState === ListboxStates.Open ? 0 : void 0,
241
- style: [theirProps.style, style, `--button-width: ${buttonSize.width}`].filter(Boolean).join("; "),
242
- ...transitionDataAttributes(transitionData),
243
- ...stateFromSlot(slot)
309
+
310
+ const labelledby = $derived(data.buttonRef.current?.id)
311
+ const slot = $derived({
312
+ open: data.listboxState === ListboxStates.Open,
313
+ } satisfies OptionsRenderPropArg)
314
+
315
+ const buttonSize = useElementSize({
316
+ get element() {
317
+ return data.buttonRef.current
318
+ },
319
+ unit: true,
244
320
  })
245
- );
246
- const derivedData = {
247
- ...data,
248
- get isSelected() {
249
- return data.mode === ValueMode.Multi ? data.isSelected : isSelected;
321
+
322
+ const ourProps = $derived(
323
+ mergeProps(anchor ? getFloatingPanelProps() : {}, {
324
+ id,
325
+ "aria-activedescendant": data.activeOptionIndex === null ? undefined : data.options[data.activeOptionIndex]?.id,
326
+ "aria-multiselectable": data.mode === ValueMode.Multi ? true : undefined,
327
+ "aria-labelledby": labelledby,
328
+ "aria-orientation": data.orientation,
329
+ onkeydown: handleKeyDown,
330
+ role: "listbox",
331
+ // When the `Listbox` is closed, it should not be focusable. This allows us
332
+ // to skip focusing the `ListboxOptions` when pressing the tab key on an
333
+ // open `Listbox`, and go to the next focusable element.
334
+ tabIndex: data.listboxState === ListboxStates.Open ? 0 : undefined,
335
+ style: [theirProps.style, style, `--button-width: ${buttonSize.width}`].filter(Boolean).join("; "),
336
+ ...transitionDataAttributes(transitionData),
337
+ ...stateFromSlot(slot),
338
+ })
339
+ )
340
+
341
+ const derivedData: ListboxDataContext = {
342
+ ...data,
343
+ get isSelected() {
344
+ return data.mode === ValueMode.Multi ? data.isSelected : isSelected
345
+ },
250
346
  }
251
- };
252
- setContext("ListboxDataContext", derivedData);
347
+
348
+ setContext("ListboxDataContext", derivedData)
253
349
  </script>
254
350
 
255
351
  <Portal enabled={portal ? isStatic || visible : false}>
@@ -1,20 +1,43 @@
1
- <script lang="ts" module>const DEFAULT_SELECTED_OPTION_TAG = "svelte:fragment";
1
+ <script lang="ts" module>
2
+ import type { ElementType, Props } from "../utils/types.js"
3
+ import type { Component } from "svelte"
4
+
5
+ const DEFAULT_SELECTED_OPTION_TAG = "svelte:fragment"
6
+ type SelectedOptionRenderPropArg = {}
7
+ type SelectedOptionPropsWeControl = never
8
+
9
+ export type ListboxSelectedOptionProps<TTag extends ElementType = typeof DEFAULT_SELECTED_OPTION_TAG> = Props<
10
+ TTag,
11
+ SelectedOptionRenderPropArg,
12
+ SelectedOptionPropsWeControl,
13
+ {
14
+ options: Component<any, any>
15
+ placeholder?: Component<any, any>
16
+ }
17
+ >
2
18
  </script>
3
19
 
4
- <script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_SELECTED_OPTION_TAG">import { useData, ValueMode } from "./Listbox.svelte";
5
- import { setContext } from "svelte";
6
- import ElementOrComponent from "../utils/ElementOrComponent.svelte";
7
- let {
8
- ref = $bindable(),
9
- options,
10
- placeholder,
11
- ...theirProps
12
- } = $props();
13
- const data = useData("ListboxSelectedOption");
14
- const shouldShowPlaceholder = $derived(
15
- data.value === void 0 || data.value === null || data.mode === ValueMode.Multi && Array.isArray(data.value) && data.value.length === 0
16
- );
17
- setContext("SelectedOptionContext", true);
20
+ <script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_SELECTED_OPTION_TAG">
21
+ import { useData, ValueMode } from "./Listbox.svelte"
22
+ import { setContext } from "svelte"
23
+ import ElementOrComponent from "../utils/ElementOrComponent.svelte"
24
+
25
+ let {
26
+ ref = $bindable(),
27
+ options,
28
+ placeholder,
29
+ ...theirProps
30
+ }: { as?: TTag } & ListboxSelectedOptionProps<TTag> = $props()
31
+
32
+ const data = useData("ListboxSelectedOption")
33
+
34
+ const shouldShowPlaceholder = $derived(
35
+ data.value === undefined ||
36
+ data.value === null ||
37
+ (data.mode === ValueMode.Multi && Array.isArray(data.value) && data.value.length === 0)
38
+ )
39
+
40
+ setContext("SelectedOptionContext", true)
18
41
  </script>
19
42
 
20
43
  {#snippet children()}