@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,312 +1,408 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 Portal from "../portal/Portal.svelte";
|
|
28
|
-
import ElementOrComponent from "../utils/ElementOrComponent.svelte";
|
|
29
|
-
const internalId = useId();
|
|
30
|
-
let {
|
|
31
|
-
ref = $bindable(),
|
|
32
|
-
id = `headlessui-listbox-options-${internalId}`,
|
|
33
|
-
anchor: rawAnchor,
|
|
34
|
-
portal = false,
|
|
35
|
-
modal = true,
|
|
36
|
-
transition = false,
|
|
37
|
-
...theirProps
|
|
38
|
-
} = $props();
|
|
39
|
-
const resolvedAnchor = useResolvedAnchor({
|
|
40
|
-
get anchor() {
|
|
41
|
-
return rawAnchor;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
const { anchor } = $derived(resolvedAnchor);
|
|
45
|
-
let localOptionsElement = $state();
|
|
46
|
-
$effect(() => {
|
|
47
|
-
if (anchor) {
|
|
48
|
-
portal = true;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
const data = useData("ListboxOptions");
|
|
52
|
-
const actions = useActions("ListboxOptions");
|
|
53
|
-
const ownerDocument = $derived(getOwnerDocument(data.optionsElement));
|
|
54
|
-
const usesOpenClosedState = useOpenClosed();
|
|
55
|
-
const show = $derived(
|
|
56
|
-
usesOpenClosedState !== null ? (usesOpenClosedState.value & State.Open) === State.Open : data.listboxState === ListboxStates.Open
|
|
57
|
-
);
|
|
58
|
-
const _transition = useTransition({
|
|
59
|
-
get enabled() {
|
|
60
|
-
return transition;
|
|
61
|
-
},
|
|
62
|
-
get element() {
|
|
63
|
-
return localOptionsElement;
|
|
64
|
-
},
|
|
65
|
-
get show() {
|
|
66
|
-
return show;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
const { visible, data: transitionData } = $derived(_transition);
|
|
70
|
-
useOnDisappear({
|
|
71
|
-
get enabled() {
|
|
72
|
-
return visible;
|
|
73
|
-
},
|
|
74
|
-
get ref() {
|
|
75
|
-
return data.buttonElement;
|
|
76
|
-
},
|
|
77
|
-
get ondisappear() {
|
|
78
|
-
return actions.closeListbox;
|
|
11
|
+
const DEFAULT_OPTIONS_TAG = "div" as const
|
|
12
|
+
type OptionsRenderPropArg = {
|
|
13
|
+
open: boolean
|
|
79
14
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG">
|
|
40
|
+
import { useId } from "../hooks/use-id.js"
|
|
41
|
+
import { ListboxStates, useActions, useData, ValueMode, type ListboxDataContext } from "./Listbox.svelte"
|
|
42
|
+
import { getOwnerDocument } from "../utils/owner.js"
|
|
43
|
+
import { State, useOpenClosed } from "../internal/open-closed.js"
|
|
44
|
+
import { transitionDataAttributes, useTransition } from "../hooks/use-transition.svelte.js"
|
|
45
|
+
import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js"
|
|
46
|
+
import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js"
|
|
47
|
+
import { useInertOthers } from "../hooks/use-inert-others.svelte.js"
|
|
48
|
+
import { useDidElementMove } from "../hooks/use-did-element-move.svelte.js"
|
|
49
|
+
import { useFrozenData } from "../internal/frozen.svelte.js"
|
|
50
|
+
import { useDisposables } from "../utils/disposables.js"
|
|
51
|
+
import { match } from "../utils/match.js"
|
|
52
|
+
import { Focus } from "../utils/calculate-active-index.js"
|
|
53
|
+
import { focusFrom, Focus as FocusManagementFocus } from "../utils/focus-management.js"
|
|
54
|
+
import { useElementSize } from "../hooks/use-element-size.svelte.js"
|
|
55
|
+
import { setContext, type Snippet } from "svelte"
|
|
56
|
+
import Portal from "../portal/Portal.svelte"
|
|
57
|
+
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
58
|
+
|
|
59
|
+
const internalId = useId()
|
|
60
|
+
let {
|
|
61
|
+
ref = $bindable(),
|
|
62
|
+
id = `headlessui-listbox-options-${internalId}`,
|
|
63
|
+
anchor: rawAnchor,
|
|
64
|
+
portal = false,
|
|
65
|
+
modal = true,
|
|
66
|
+
transition = false,
|
|
67
|
+
...theirProps
|
|
68
|
+
}: { as?: TTag } & ListboxOptionsProps<TTag> = $props()
|
|
69
|
+
const resolvedAnchor = useResolvedAnchor({
|
|
70
|
+
get anchor() {
|
|
71
|
+
return rawAnchor
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
const { anchor } = $derived(resolvedAnchor)
|
|
75
|
+
|
|
76
|
+
let localOptionsElement = $state<HTMLElement | null>()
|
|
77
|
+
|
|
78
|
+
// Always enable `portal` functionality, when `anchor` is enabled
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (anchor) {
|
|
81
|
+
portal = true
|
|
98
82
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const data = useData("ListboxOptions")
|
|
86
|
+
const actions = useActions("ListboxOptions")
|
|
87
|
+
|
|
88
|
+
const ownerDocument = $derived(getOwnerDocument(data.optionsElement))
|
|
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 localOptionsElement
|
|
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.buttonElement
|
|
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.buttonElement, data.optionsElement]
|
|
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.buttonElement
|
|
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.data, compareValue)
|
|
185
|
+
|
|
186
|
+
const selectedOptionIndex = $derived.by(() => {
|
|
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
|
|
196
|
+
})
|
|
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.listElements.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, styles } = $derived(floatingPanel)
|
|
219
|
+
const getFloatingPanelProps = useFloatingPanelProps()
|
|
220
|
+
|
|
221
|
+
$effect(() => {
|
|
222
|
+
localOptionsElement = ref
|
|
223
|
+
data.optionsElement = ref ?? null
|
|
224
|
+
if (anchor) setFloating(ref ?? null)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const searchDisposables = useDisposables()
|
|
228
|
+
|
|
229
|
+
const { listboxState, optionsElement } = $derived(data)
|
|
230
|
+
$effect(() => {
|
|
231
|
+
if (!optionsElement) return
|
|
232
|
+
if (listboxState !== ListboxStates.Open) return
|
|
233
|
+
if (optionsElement === getOwnerDocument(optionsElement)?.activeElement) return
|
|
234
|
+
|
|
235
|
+
optionsElement?.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.buttonElement?.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.buttonElement?.focus({ preventScroll: true })
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
case "Tab":
|
|
295
|
+
event.preventDefault()
|
|
296
|
+
event.stopPropagation()
|
|
297
|
+
actions.closeListbox()
|
|
298
|
+
focusFrom(data.buttonElement, 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
|
|
137
307
|
}
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
const floatingPanel = useFloatingPanel({
|
|
141
|
-
get placement() {
|
|
142
|
-
return anchorOptions ?? null;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
const { setFloating, styles } = $derived(floatingPanel);
|
|
146
|
-
const getFloatingPanelProps = useFloatingPanelProps();
|
|
147
|
-
$effect(() => {
|
|
148
|
-
localOptionsElement = ref;
|
|
149
|
-
data.optionsElement = ref ?? null;
|
|
150
|
-
if (anchor) setFloating(ref ?? null);
|
|
151
|
-
});
|
|
152
|
-
const searchDisposables = useDisposables();
|
|
153
|
-
const { listboxState, optionsElement } = $derived(data);
|
|
154
|
-
$effect(() => {
|
|
155
|
-
if (!optionsElement) return;
|
|
156
|
-
if (listboxState !== ListboxStates.Open) return;
|
|
157
|
-
if (optionsElement === getOwnerDocument(optionsElement)?.activeElement) return;
|
|
158
|
-
optionsElement?.focus({ preventScroll: true });
|
|
159
|
-
});
|
|
160
|
-
const handleKeyDown = (event) => {
|
|
161
|
-
searchDisposables.dispose();
|
|
162
|
-
switch (event.key) {
|
|
163
|
-
case " ":
|
|
164
|
-
if (data.searchQuery !== "") {
|
|
165
|
-
event.preventDefault();
|
|
166
|
-
event.stopPropagation();
|
|
167
|
-
return actions.search(event.key);
|
|
168
|
-
}
|
|
169
|
-
case "Enter":
|
|
170
|
-
event.preventDefault();
|
|
171
|
-
event.stopPropagation();
|
|
172
|
-
if (data.activeOptionIndex !== null) {
|
|
173
|
-
let { dataRef } = data.options[data.activeOptionIndex];
|
|
174
|
-
actions.onChange(dataRef.current.value);
|
|
175
|
-
}
|
|
176
|
-
if (data.mode === ValueMode.Single) {
|
|
177
|
-
actions.closeListbox();
|
|
178
|
-
data.buttonElement?.focus({ preventScroll: true });
|
|
179
|
-
}
|
|
180
|
-
break;
|
|
181
|
-
case match(data.orientation, { vertical: "ArrowDown", horizontal: "ArrowRight" }):
|
|
182
|
-
event.preventDefault();
|
|
183
|
-
event.stopPropagation();
|
|
184
|
-
return actions.goToOption(Focus.Next);
|
|
185
|
-
case match(data.orientation, { vertical: "ArrowUp", horizontal: "ArrowLeft" }):
|
|
186
|
-
event.preventDefault();
|
|
187
|
-
event.stopPropagation();
|
|
188
|
-
return actions.goToOption(Focus.Previous);
|
|
189
|
-
case "Home":
|
|
190
|
-
case "PageUp":
|
|
191
|
-
event.preventDefault();
|
|
192
|
-
event.stopPropagation();
|
|
193
|
-
return actions.goToOption(Focus.First);
|
|
194
|
-
case "End":
|
|
195
|
-
case "PageDown":
|
|
196
|
-
event.preventDefault();
|
|
197
|
-
event.stopPropagation();
|
|
198
|
-
return actions.goToOption(Focus.Last);
|
|
199
|
-
case "Escape":
|
|
200
|
-
event.preventDefault();
|
|
201
|
-
event.stopPropagation();
|
|
202
|
-
actions.closeListbox();
|
|
203
|
-
data.buttonElement?.focus({ preventScroll: true });
|
|
204
|
-
return;
|
|
205
|
-
case "Tab":
|
|
206
|
-
event.preventDefault();
|
|
207
|
-
event.stopPropagation();
|
|
208
|
-
actions.closeListbox();
|
|
209
|
-
focusFrom(data.buttonElement, event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next);
|
|
210
|
-
break;
|
|
211
|
-
default:
|
|
212
|
-
if (event.key.length === 1) {
|
|
213
|
-
actions.search(event.key);
|
|
214
|
-
searchDisposables.setTimeout(() => actions.clearSearch(), 350);
|
|
215
|
-
}
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
const labelledby = $derived(data.buttonElement?.id);
|
|
220
|
-
const slot = $derived({
|
|
221
|
-
open: data.listboxState === ListboxStates.Open
|
|
222
|
-
});
|
|
223
|
-
const buttonSize = useElementSize({
|
|
224
|
-
get element() {
|
|
225
|
-
return data.buttonElement;
|
|
226
|
-
},
|
|
227
|
-
unit: true
|
|
228
|
-
});
|
|
229
|
-
$effect(() => {
|
|
230
|
-
transitionData;
|
|
231
|
-
});
|
|
232
|
-
const ourProps = $derived({
|
|
233
|
-
...mergeProps(anchor ? getFloatingPanelProps() : {}, {
|
|
234
|
-
id,
|
|
235
|
-
"aria-activedescendant": data.activeOptionIndex === null ? void 0 : data.options[data.activeOptionIndex]?.id,
|
|
236
|
-
"aria-multiselectable": data.mode === ValueMode.Multi ? true : void 0,
|
|
237
|
-
"aria-labelledby": labelledby,
|
|
238
|
-
"aria-orientation": data.orientation,
|
|
239
|
-
role: "listbox",
|
|
240
|
-
// When the `Listbox` is closed, it should not be focusable. This allows us
|
|
241
|
-
// to skip focusing the `ListboxOptions` when pressing the tab key on an
|
|
242
|
-
// open `Listbox`, and go to the next focusable element.
|
|
243
|
-
tabindex: data.listboxState === ListboxStates.Open ? 0 : void 0,
|
|
244
|
-
style: [theirProps.style, styles, `--button-width: ${buttonSize.width}`].filter(Boolean).join("; ")
|
|
245
|
-
}),
|
|
246
|
-
...transitionDataAttributes(transitionData)
|
|
247
|
-
});
|
|
248
|
-
const derivedData = $derived({
|
|
249
|
-
...data,
|
|
250
|
-
get isSelected() {
|
|
251
|
-
return data.mode === ValueMode.Multi ? data.isSelected : isSelected;
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
setContext("ListboxDataContext", {
|
|
255
|
-
get value() {
|
|
256
|
-
return data.value;
|
|
257
|
-
},
|
|
258
|
-
get disabled() {
|
|
259
|
-
return data.disabled;
|
|
260
|
-
},
|
|
261
|
-
get invalid() {
|
|
262
|
-
return data.invalid;
|
|
263
|
-
},
|
|
264
|
-
get mode() {
|
|
265
|
-
return data.mode;
|
|
266
|
-
},
|
|
267
|
-
get orientation() {
|
|
268
|
-
return data.orientation;
|
|
269
|
-
},
|
|
270
|
-
get activeOptionIndex() {
|
|
271
|
-
return data.activeOptionIndex;
|
|
272
|
-
},
|
|
273
|
-
get closeOnSelect() {
|
|
274
|
-
return data.closeOnSelect;
|
|
275
|
-
},
|
|
276
|
-
get compare() {
|
|
277
|
-
return data.compare;
|
|
278
|
-
},
|
|
279
|
-
get isSelected() {
|
|
280
|
-
return data.mode === ValueMode.Multi ? data.isSelected : isSelected;
|
|
281
|
-
},
|
|
282
|
-
get optionsProps() {
|
|
283
|
-
return data.optionsProps;
|
|
284
|
-
},
|
|
285
|
-
get listElements() {
|
|
286
|
-
return data.listElements;
|
|
287
|
-
},
|
|
288
|
-
get buttonElement() {
|
|
289
|
-
return data.buttonElement;
|
|
290
|
-
},
|
|
291
|
-
get optionsElement() {
|
|
292
|
-
return data.optionsElement;
|
|
293
|
-
},
|
|
294
|
-
get listboxState() {
|
|
295
|
-
return data.listboxState;
|
|
296
|
-
},
|
|
297
|
-
get options() {
|
|
298
|
-
return data.options;
|
|
299
|
-
},
|
|
300
|
-
get searchQuery() {
|
|
301
|
-
return data.searchQuery;
|
|
302
|
-
},
|
|
303
|
-
get activationTrigger() {
|
|
304
|
-
return data.activationTrigger;
|
|
305
|
-
},
|
|
306
|
-
get __demoMode() {
|
|
307
|
-
return data.__demoMode;
|
|
308
308
|
}
|
|
309
|
-
|
|
309
|
+
|
|
310
|
+
const labelledby = $derived(data.buttonElement?.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.buttonElement
|
|
318
|
+
},
|
|
319
|
+
unit: true,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
$effect(() => {
|
|
323
|
+
transitionData
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const ourProps = $derived({
|
|
327
|
+
...mergeProps(anchor ? getFloatingPanelProps() : {}, {
|
|
328
|
+
id,
|
|
329
|
+
"aria-activedescendant": data.activeOptionIndex === null ? undefined : data.options[data.activeOptionIndex]?.id,
|
|
330
|
+
"aria-multiselectable": data.mode === ValueMode.Multi ? true : undefined,
|
|
331
|
+
"aria-labelledby": labelledby,
|
|
332
|
+
"aria-orientation": data.orientation,
|
|
333
|
+
role: "listbox",
|
|
334
|
+
// When the `Listbox` is closed, it should not be focusable. This allows us
|
|
335
|
+
// to skip focusing the `ListboxOptions` when pressing the tab key on an
|
|
336
|
+
// open `Listbox`, and go to the next focusable element.
|
|
337
|
+
tabindex: data.listboxState === ListboxStates.Open ? 0 : undefined,
|
|
338
|
+
style: [theirProps.style, styles, `--button-width: ${buttonSize.width}`].filter(Boolean).join("; "),
|
|
339
|
+
}),
|
|
340
|
+
...transitionDataAttributes(transitionData),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const derivedData: ListboxDataContext = $derived({
|
|
344
|
+
...data,
|
|
345
|
+
get isSelected() {
|
|
346
|
+
return data.mode === ValueMode.Multi ? data.isSelected : isSelected
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
setContext("ListboxDataContext", {
|
|
351
|
+
get value() {
|
|
352
|
+
return data.value
|
|
353
|
+
},
|
|
354
|
+
get disabled() {
|
|
355
|
+
return data.disabled
|
|
356
|
+
},
|
|
357
|
+
get invalid() {
|
|
358
|
+
return data.invalid
|
|
359
|
+
},
|
|
360
|
+
get mode() {
|
|
361
|
+
return data.mode
|
|
362
|
+
},
|
|
363
|
+
get orientation() {
|
|
364
|
+
return data.orientation
|
|
365
|
+
},
|
|
366
|
+
get activeOptionIndex() {
|
|
367
|
+
return data.activeOptionIndex
|
|
368
|
+
},
|
|
369
|
+
get closeOnSelect() {
|
|
370
|
+
return data.closeOnSelect
|
|
371
|
+
},
|
|
372
|
+
get compare() {
|
|
373
|
+
return data.compare
|
|
374
|
+
},
|
|
375
|
+
get isSelected() {
|
|
376
|
+
return data.mode === ValueMode.Multi ? data.isSelected : isSelected
|
|
377
|
+
},
|
|
378
|
+
get optionsProps() {
|
|
379
|
+
return data.optionsProps
|
|
380
|
+
},
|
|
381
|
+
get listElements() {
|
|
382
|
+
return data.listElements
|
|
383
|
+
},
|
|
384
|
+
get buttonElement() {
|
|
385
|
+
return data.buttonElement
|
|
386
|
+
},
|
|
387
|
+
get optionsElement() {
|
|
388
|
+
return data.optionsElement
|
|
389
|
+
},
|
|
390
|
+
get listboxState() {
|
|
391
|
+
return data.listboxState
|
|
392
|
+
},
|
|
393
|
+
get options() {
|
|
394
|
+
return data.options
|
|
395
|
+
},
|
|
396
|
+
get searchQuery() {
|
|
397
|
+
return data.searchQuery
|
|
398
|
+
},
|
|
399
|
+
get activationTrigger() {
|
|
400
|
+
return data.activationTrigger
|
|
401
|
+
},
|
|
402
|
+
get __demoMode() {
|
|
403
|
+
return data.__demoMode
|
|
404
|
+
},
|
|
405
|
+
})
|
|
310
406
|
</script>
|
|
311
407
|
|
|
312
408
|
<Portal enabled={portal ? theirProps.static || visible : false}>
|