@pzerelles/headlessui-svelte 2.1.2-next.3 → 2.1.2-next.5
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/checkbox/Checkbox.svelte +174 -120
- package/dist/close-button/CloseButton.svelte +12 -6
- package/dist/combobox/Combobox.svelte +50 -3
- package/dist/data-interactive/DataInteractive.svelte +57 -29
- package/dist/description/Description.svelte +32 -21
- package/dist/dialog/Dialog.svelte +69 -34
- package/dist/dialog/DialogBackdrop.svelte +29 -12
- package/dist/dialog/DialogPanel.svelte +49 -26
- package/dist/dialog/DialogTitle.svelte +38 -23
- package/dist/dialog/InternalDialog.svelte +263 -202
- package/dist/field/Field.svelte +49 -26
- package/dist/fieldset/Fieldset.svelte +50 -29
- package/dist/focus-trap/FocusTrap.svelte +419 -290
- package/dist/focus-trap/FocusTrap.svelte.d.ts +2 -14
- package/dist/focus-trap/FocusTrapFeatures.d.ts +14 -0
- package/dist/focus-trap/FocusTrapFeatures.js +15 -0
- package/dist/input/Input.svelte +85 -53
- package/dist/internal/FocusSentinel.svelte +16 -8
- package/dist/internal/ForcePortalRoot.svelte +7 -3
- package/dist/internal/FormFields.svelte +31 -20
- package/dist/internal/FormResolver.svelte +20 -15
- package/dist/internal/Hidden.svelte +44 -27
- package/dist/internal/Hidden.svelte.d.ts +2 -5
- package/dist/internal/HiddenFeatures.d.ts +5 -0
- package/dist/internal/HiddenFeatures.js +9 -0
- package/dist/internal/HoistFormFields.svelte +7 -4
- package/dist/internal/MainTreeProvider.svelte +89 -36
- package/dist/internal/Portal.svelte +18 -14
- package/dist/label/Label.svelte +91 -57
- package/dist/legend/Legend.svelte +18 -3
- package/dist/listbox/Listbox.svelte +588 -409
- package/dist/listbox/Listbox.svelte.d.ts +2 -12
- package/dist/listbox/ListboxButton.svelte +176 -127
- package/dist/listbox/ListboxOption.svelte +166 -125
- package/dist/listbox/ListboxOptions.svelte +340 -244
- package/dist/listbox/ListboxSelectedOption.svelte +38 -15
- package/dist/listbox/ListboxStates.d.ts +12 -0
- package/dist/listbox/ListboxStates.js +15 -0
- package/dist/menu/Menu.svelte +307 -218
- package/dist/menu/MenuButton.svelte +157 -115
- package/dist/menu/MenuHeading.svelte +34 -14
- package/dist/menu/MenuItem.svelte +145 -107
- package/dist/menu/MenuItems.svelte +298 -224
- package/dist/menu/MenuSection.svelte +26 -9
- package/dist/menu/MenuSeparator.svelte +20 -4
- package/dist/portal/InternalPortal.svelte +141 -85
- package/dist/portal/Portal.svelte +5 -2
- package/dist/portal/PortalGroup.svelte +30 -9
- package/dist/switch/Switch.svelte +179 -122
- package/dist/switch/Switch.svelte.d.ts +4 -4
- package/dist/switch/SwitchGroup.svelte +44 -31
- package/dist/tabs/Tab.svelte +195 -143
- package/dist/tabs/TabGroup.svelte +292 -205
- package/dist/tabs/TabList.svelte +31 -11
- package/dist/tabs/TabPanel.svelte +68 -43
- package/dist/tabs/TabPanels.svelte +18 -7
- package/dist/textarea/Textarea.svelte +83 -53
- package/dist/transition/InternalTransitionChild.svelte +259 -170
- package/dist/transition/Transition.svelte +96 -66
- package/dist/transition/TransitionChild.svelte +31 -11
- package/dist/utils/ElementOrComponent.svelte +44 -23
- package/dist/utils/Generic.svelte +29 -17
- package/dist/utils/StableCollection.svelte +54 -36
- package/package.json +10 -10
|
@@ -1,233 +1,307 @@
|
|
|
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 AnchorProps,
|
|
9
|
+
} from "../internal/floating.svelte.js"
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { transitionDataAttributes, useTransition } from "../hooks/use-transition.svelte.js";
|
|
15
|
-
import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js";
|
|
16
|
-
import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js";
|
|
17
|
-
import { useInertOthers } from "../hooks/use-inert-others.svelte.js";
|
|
18
|
-
import { useDidElementMove } from "../hooks/use-did-element-move.svelte.js";
|
|
19
|
-
import { useDisposables } from "../utils/disposables.js";
|
|
20
|
-
import { Focus } from "../utils/calculate-active-index.js";
|
|
21
|
-
import { focusFrom, Focus as FocusManagementFocus, restoreFocusIfNecessary } from "../utils/focus-management.js";
|
|
22
|
-
import { useElementSize } from "../hooks/use-element-size.svelte.js";
|
|
23
|
-
import { tick, untrack } from "svelte";
|
|
24
|
-
import Portal from "../portal/Portal.svelte";
|
|
25
|
-
import { MenuStates, useMenuContext } from "./context.svelte.js";
|
|
26
|
-
import { useTreeWalker } from "../hooks/use-tree-walker.svelte.js";
|
|
27
|
-
import ElementOrComponent from "../utils/ElementOrComponent.svelte";
|
|
28
|
-
const internalId = useId();
|
|
29
|
-
let {
|
|
30
|
-
as = DEFAULT_ITEMS_TAG,
|
|
31
|
-
ref = $bindable(),
|
|
32
|
-
id = `headlessui-menu-items-${internalId}`,
|
|
33
|
-
anchor: rawAnchor,
|
|
34
|
-
portal = false,
|
|
35
|
-
modal = true,
|
|
36
|
-
transition = false,
|
|
37
|
-
...theirProps
|
|
38
|
-
} = $props();
|
|
39
|
-
const anchor = $derived(useResolvedAnchor(rawAnchor));
|
|
40
|
-
const _state = useMenuContext("MenuOptions");
|
|
41
|
-
const floatingPanel = useFloatingPanel({
|
|
42
|
-
get placement() {
|
|
43
|
-
return anchor;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
const { setFloating, style } = $derived(floatingPanel);
|
|
47
|
-
const getFloatingPanelProps = useFloatingPanelProps();
|
|
48
|
-
$effect(() => {
|
|
49
|
-
untrack(() => _state.setItemsElement(ref || null));
|
|
50
|
-
if (anchor) setFloating(ref);
|
|
51
|
-
});
|
|
52
|
-
const ownerDocument = $derived(getOwnerDocument(_state.itemsElement));
|
|
53
|
-
$effect(() => {
|
|
54
|
-
if (anchor) {
|
|
55
|
-
portal = true;
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
const usesOpenClosedState = useOpenClosed();
|
|
59
|
-
const show = $derived(
|
|
60
|
-
usesOpenClosedState !== null ? (usesOpenClosedState.value & State.Open) === State.Open : _state.menuState === MenuStates.Open
|
|
61
|
-
);
|
|
62
|
-
const _transition = useTransition({
|
|
63
|
-
get enabled() {
|
|
64
|
-
return transition;
|
|
65
|
-
},
|
|
66
|
-
get element() {
|
|
67
|
-
return ref;
|
|
68
|
-
},
|
|
69
|
-
get show() {
|
|
70
|
-
return show;
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
const { visible, data: transitionData } = $derived(_transition);
|
|
74
|
-
useOnDisappear({
|
|
75
|
-
get enabled() {
|
|
76
|
-
return visible;
|
|
77
|
-
},
|
|
78
|
-
get ref() {
|
|
79
|
-
return _state.buttonElement;
|
|
80
|
-
},
|
|
81
|
-
get ondisappear() {
|
|
82
|
-
return _state.closeMenu;
|
|
11
|
+
const DEFAULT_ITEMS_TAG = "div" as const
|
|
12
|
+
type ItemsRenderPropArg = {
|
|
13
|
+
open: boolean
|
|
83
14
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
15
|
+
type ItemsPropsWeControl = "aria-activedescendant" | "aria-labelledby" | "role" | "tabIndex"
|
|
16
|
+
|
|
17
|
+
let ItemsRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
|
|
18
|
+
|
|
19
|
+
export type MenuItemsProps<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG> = Props<
|
|
20
|
+
TTag,
|
|
21
|
+
ItemsRenderPropArg,
|
|
22
|
+
ItemsPropsWeControl,
|
|
23
|
+
{
|
|
24
|
+
id?: string
|
|
25
|
+
anchor?: AnchorProps
|
|
26
|
+
portal?: boolean
|
|
27
|
+
modal?: boolean
|
|
28
|
+
transition?: boolean
|
|
29
|
+
} & PropsForFeatures<typeof ItemsRenderFeatures>
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
export type MenuItemsChildren = Snippet<[ItemsRenderPropArg]>
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_ITEMS_TAG">
|
|
36
|
+
import { useId } from "../hooks/use-id.js"
|
|
37
|
+
import { getOwnerDocument } from "../utils/owner.js"
|
|
38
|
+
import { State, useOpenClosed } from "../internal/open-closed.js"
|
|
39
|
+
import { transitionDataAttributes, useTransition } from "../hooks/use-transition.svelte.js"
|
|
40
|
+
import { useOnDisappear } from "../hooks/use-on-disappear.svelte.js"
|
|
41
|
+
import { useScrollLock } from "../hooks/use-scroll-lock.svelte.js"
|
|
42
|
+
import { useInertOthers } from "../hooks/use-inert-others.svelte.js"
|
|
43
|
+
import { useDidElementMove } from "../hooks/use-did-element-move.svelte.js"
|
|
44
|
+
import { useDisposables } from "../utils/disposables.js"
|
|
45
|
+
import { Focus } from "../utils/calculate-active-index.js"
|
|
46
|
+
import { focusFrom, Focus as FocusManagementFocus, restoreFocusIfNecessary } from "../utils/focus-management.js"
|
|
47
|
+
import { useElementSize } from "../hooks/use-element-size.svelte.js"
|
|
48
|
+
import { tick, untrack, type Snippet } from "svelte"
|
|
49
|
+
import Portal from "../portal/Portal.svelte"
|
|
50
|
+
import { MenuStates, useMenuContext } from "./context.svelte.js"
|
|
51
|
+
import { useTreeWalker } from "../hooks/use-tree-walker.svelte.js"
|
|
52
|
+
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
53
|
+
|
|
54
|
+
const internalId = useId()
|
|
55
|
+
let {
|
|
56
|
+
as = DEFAULT_ITEMS_TAG as TTag,
|
|
57
|
+
ref = $bindable(),
|
|
58
|
+
id = `headlessui-menu-items-${internalId}`,
|
|
59
|
+
anchor: rawAnchor,
|
|
60
|
+
portal = false,
|
|
61
|
+
modal = true,
|
|
62
|
+
transition = false,
|
|
63
|
+
...theirProps
|
|
64
|
+
}: { as?: TTag } & MenuItemsProps<TTag> = $props()
|
|
65
|
+
const anchor = $derived(useResolvedAnchor(rawAnchor))
|
|
66
|
+
const _state = useMenuContext("MenuOptions")
|
|
67
|
+
const floatingPanel = useFloatingPanel({
|
|
68
|
+
get placement() {
|
|
69
|
+
return anchor
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
const { setFloating, style } = $derived(floatingPanel)
|
|
73
|
+
const getFloatingPanelProps = useFloatingPanelProps()
|
|
74
|
+
|
|
75
|
+
$effect(() => {
|
|
76
|
+
untrack(() => _state.setItemsElement(ref || null))
|
|
77
|
+
if (anchor) setFloating(ref)
|
|
78
|
+
})
|
|
79
|
+
const ownerDocument = $derived(getOwnerDocument(_state.itemsElement))
|
|
80
|
+
|
|
81
|
+
// Always enable `portal` functionality, when `anchor` is enabled
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (anchor) {
|
|
84
|
+
portal = true
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const usesOpenClosedState = useOpenClosed()
|
|
89
|
+
const show = $derived(
|
|
90
|
+
usesOpenClosedState !== null
|
|
91
|
+
? (usesOpenClosedState.value & State.Open) === State.Open
|
|
92
|
+
: _state.menuState === MenuStates.Open
|
|
93
|
+
)
|
|
94
|
+
const _transition = useTransition({
|
|
95
|
+
get enabled() {
|
|
96
|
+
return transition
|
|
97
|
+
},
|
|
98
|
+
get element() {
|
|
99
|
+
return ref
|
|
100
|
+
},
|
|
101
|
+
get show() {
|
|
102
|
+
return show
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
const { visible, data: transitionData } = $derived(_transition)
|
|
106
|
+
|
|
107
|
+
// Ensure we close the listbox as soon as the button becomes hidden
|
|
108
|
+
useOnDisappear({
|
|
109
|
+
get enabled() {
|
|
110
|
+
return visible
|
|
111
|
+
},
|
|
112
|
+
get ref() {
|
|
113
|
+
return _state.buttonElement
|
|
114
|
+
},
|
|
115
|
+
get ondisappear() {
|
|
116
|
+
return _state.closeMenu
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Enable scroll locking when the listbox is visible, and `modal` is enabled
|
|
121
|
+
const scrollLockEnabled = $derived(_state.__demoMode ? false : modal && _state.menuState === MenuStates.Open)
|
|
122
|
+
useScrollLock({
|
|
123
|
+
get enabled() {
|
|
124
|
+
return scrollLockEnabled
|
|
125
|
+
},
|
|
126
|
+
get ownerDocument() {
|
|
127
|
+
return ownerDocument
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Mark other elements as inert when the listbox is visible, and `modal` is enabled
|
|
132
|
+
const inertOthersEnabled = $derived(_state.__demoMode ? false : modal && _state.menuState === MenuStates.Open)
|
|
133
|
+
useInertOthers({
|
|
134
|
+
get enabled() {
|
|
135
|
+
return inertOthersEnabled
|
|
136
|
+
},
|
|
137
|
+
elements: {
|
|
138
|
+
get allowed() {
|
|
139
|
+
return [_state.buttonElement, _state.itemsElement].filter(Boolean)
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// We keep track whether the button moved or not, we only check this when the menu state becomes
|
|
145
|
+
// closed. If the button moved, then we want to cancel pending transitions to prevent that the
|
|
146
|
+
// attached `MenuItems` is still transitioning while the button moved away.
|
|
147
|
+
//
|
|
148
|
+
// If we don't cancel these transitions then there will be a period where the `MenuItems` is
|
|
149
|
+
// visible and moving around because it is trying to re-position itself based on the new position.
|
|
150
|
+
//
|
|
151
|
+
// This can be solved by only transitioning the `opacity` instead of everything, but if you _do_
|
|
152
|
+
// want to transition the y-axis for example you will run into the same issue again.
|
|
153
|
+
const didElementMoveEnabled = $derived(_state.menuState !== MenuStates.Open)
|
|
154
|
+
const didButtonMove = useDidElementMove({
|
|
155
|
+
get enabled() {
|
|
156
|
+
return didElementMoveEnabled
|
|
157
|
+
},
|
|
158
|
+
get element() {
|
|
159
|
+
return _state.buttonElement
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Now that we know that the button did move or not, we can either disable the panel and all of
|
|
164
|
+
// its transitions, or rely on the `visible` state to hide the panel whenever necessary.
|
|
165
|
+
const panelEnabled = $derived(didButtonMove.value ? false : visible)
|
|
166
|
+
|
|
167
|
+
$effect(() => {
|
|
168
|
+
let container = _state.itemsElement
|
|
169
|
+
if (!container) return
|
|
170
|
+
if (_state.menuState !== MenuStates.Open) return
|
|
171
|
+
if (container === ownerDocument?.activeElement) return
|
|
172
|
+
|
|
173
|
+
container.focus({ preventScroll: true })
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
useTreeWalker({
|
|
177
|
+
get enabled() {
|
|
178
|
+
return _state.menuState === MenuStates.Open
|
|
179
|
+
},
|
|
180
|
+
get container() {
|
|
181
|
+
return _state.itemsElement
|
|
182
|
+
},
|
|
183
|
+
accept: (node) => {
|
|
184
|
+
if (node.getAttribute("role") === "menuitem") return NodeFilter.FILTER_REJECT
|
|
185
|
+
if (node.hasAttribute("role")) return NodeFilter.FILTER_SKIP
|
|
186
|
+
return NodeFilter.FILTER_ACCEPT
|
|
187
|
+
},
|
|
188
|
+
walk: (node) => {
|
|
189
|
+
node.setAttribute("role", "none")
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const searchDisposables = useDisposables()
|
|
194
|
+
|
|
195
|
+
const handleKeyDown = async (event: KeyboardEvent) => {
|
|
196
|
+
searchDisposables.dispose()
|
|
197
|
+
|
|
198
|
+
switch (event.key) {
|
|
199
|
+
// Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menu/#keyboard-interaction-12
|
|
200
|
+
|
|
201
|
+
case " ":
|
|
202
|
+
if (_state.searchQuery !== "") {
|
|
203
|
+
event.preventDefault()
|
|
204
|
+
event.stopPropagation()
|
|
205
|
+
return _state.search(event.key)
|
|
206
|
+
}
|
|
207
|
+
// When in type ahead mode, fallthrough
|
|
208
|
+
case "Enter":
|
|
209
|
+
event.preventDefault()
|
|
210
|
+
event.stopPropagation()
|
|
211
|
+
_state.closeMenu()
|
|
212
|
+
if (_state.activeItemIndex !== null) {
|
|
213
|
+
let { dataRef } = _state.items[_state.activeItemIndex]
|
|
214
|
+
dataRef.current?.domRef.current?.click()
|
|
215
|
+
}
|
|
216
|
+
restoreFocusIfNecessary(_state.buttonElement)
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
case "ArrowDown":
|
|
220
|
+
event.preventDefault()
|
|
221
|
+
event.stopPropagation()
|
|
222
|
+
return _state.goToItem({ focus: Focus.Next })
|
|
223
|
+
|
|
224
|
+
case "ArrowUp":
|
|
225
|
+
event.preventDefault()
|
|
226
|
+
event.stopPropagation()
|
|
227
|
+
return _state.goToItem({ focus: Focus.Previous })
|
|
228
|
+
|
|
229
|
+
case "Home":
|
|
230
|
+
case "PageUp":
|
|
231
|
+
event.preventDefault()
|
|
232
|
+
event.stopPropagation()
|
|
233
|
+
return _state.goToItem({ focus: Focus.First })
|
|
234
|
+
|
|
235
|
+
case "End":
|
|
236
|
+
case "PageDown":
|
|
237
|
+
event.preventDefault()
|
|
238
|
+
event.stopPropagation()
|
|
239
|
+
return _state.goToItem({ focus: Focus.Last })
|
|
240
|
+
|
|
241
|
+
case "Escape":
|
|
242
|
+
event.preventDefault()
|
|
243
|
+
event.stopPropagation()
|
|
244
|
+
_state.closeMenu()
|
|
245
|
+
await tick()
|
|
246
|
+
_state.buttonElement?.focus({ preventScroll: true })
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
case "Tab":
|
|
250
|
+
event.preventDefault()
|
|
251
|
+
event.stopPropagation()
|
|
252
|
+
_state.closeMenu()
|
|
253
|
+
await tick()
|
|
254
|
+
focusFrom(_state.buttonElement!, event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next)
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
default:
|
|
258
|
+
if (event.key.length === 1) {
|
|
259
|
+
_state.search(event.key)
|
|
260
|
+
searchDisposables.setTimeout(() => _state.clearSearch(), 350)
|
|
261
|
+
}
|
|
262
|
+
break
|
|
102
263
|
}
|
|
103
264
|
}
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
const panelEnabled = $derived(didButtonMove.value ? false : visible);
|
|
115
|
-
$effect(() => {
|
|
116
|
-
let container = _state.itemsElement;
|
|
117
|
-
if (!container) return;
|
|
118
|
-
if (_state.menuState !== MenuStates.Open) return;
|
|
119
|
-
if (container === ownerDocument?.activeElement) return;
|
|
120
|
-
container.focus({ preventScroll: true });
|
|
121
|
-
});
|
|
122
|
-
useTreeWalker({
|
|
123
|
-
get enabled() {
|
|
124
|
-
return _state.menuState === MenuStates.Open;
|
|
125
|
-
},
|
|
126
|
-
get container() {
|
|
127
|
-
return _state.itemsElement;
|
|
128
|
-
},
|
|
129
|
-
accept: (node) => {
|
|
130
|
-
if (node.getAttribute("role") === "menuitem") return NodeFilter.FILTER_REJECT;
|
|
131
|
-
if (node.hasAttribute("role")) return NodeFilter.FILTER_SKIP;
|
|
132
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
133
|
-
},
|
|
134
|
-
walk: (node) => {
|
|
135
|
-
node.setAttribute("role", "none");
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
const searchDisposables = useDisposables();
|
|
139
|
-
const handleKeyDown = async (event) => {
|
|
140
|
-
searchDisposables.dispose();
|
|
141
|
-
switch (event.key) {
|
|
142
|
-
case " ":
|
|
143
|
-
if (_state.searchQuery !== "") {
|
|
144
|
-
event.preventDefault();
|
|
145
|
-
event.stopPropagation();
|
|
146
|
-
return _state.search(event.key);
|
|
147
|
-
}
|
|
148
|
-
case "Enter":
|
|
149
|
-
event.preventDefault();
|
|
150
|
-
event.stopPropagation();
|
|
151
|
-
_state.closeMenu();
|
|
152
|
-
if (_state.activeItemIndex !== null) {
|
|
153
|
-
let { dataRef } = _state.items[_state.activeItemIndex];
|
|
154
|
-
dataRef.current?.domRef.current?.click();
|
|
155
|
-
}
|
|
156
|
-
restoreFocusIfNecessary(_state.buttonElement);
|
|
157
|
-
break;
|
|
158
|
-
case "ArrowDown":
|
|
159
|
-
event.preventDefault();
|
|
160
|
-
event.stopPropagation();
|
|
161
|
-
return _state.goToItem({ focus: Focus.Next });
|
|
162
|
-
case "ArrowUp":
|
|
163
|
-
event.preventDefault();
|
|
164
|
-
event.stopPropagation();
|
|
165
|
-
return _state.goToItem({ focus: Focus.Previous });
|
|
166
|
-
case "Home":
|
|
167
|
-
case "PageUp":
|
|
168
|
-
event.preventDefault();
|
|
169
|
-
event.stopPropagation();
|
|
170
|
-
return _state.goToItem({ focus: Focus.First });
|
|
171
|
-
case "End":
|
|
172
|
-
case "PageDown":
|
|
173
|
-
event.preventDefault();
|
|
174
|
-
event.stopPropagation();
|
|
175
|
-
return _state.goToItem({ focus: Focus.Last });
|
|
176
|
-
case "Escape":
|
|
177
|
-
event.preventDefault();
|
|
178
|
-
event.stopPropagation();
|
|
179
|
-
_state.closeMenu();
|
|
180
|
-
await tick();
|
|
181
|
-
_state.buttonElement?.focus({ preventScroll: true });
|
|
182
|
-
break;
|
|
183
|
-
case "Tab":
|
|
184
|
-
event.preventDefault();
|
|
185
|
-
event.stopPropagation();
|
|
186
|
-
_state.closeMenu();
|
|
187
|
-
await tick();
|
|
188
|
-
focusFrom(_state.buttonElement, event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next);
|
|
189
|
-
break;
|
|
190
|
-
default:
|
|
191
|
-
if (event.key.length === 1) {
|
|
192
|
-
_state.search(event.key);
|
|
193
|
-
searchDisposables.setTimeout(() => _state.clearSearch(), 350);
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
const handleKeyUp = (event) => {
|
|
199
|
-
switch (event.key) {
|
|
200
|
-
case " ":
|
|
201
|
-
event.preventDefault();
|
|
202
|
-
break;
|
|
265
|
+
|
|
266
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
267
|
+
switch (event.key) {
|
|
268
|
+
case " ":
|
|
269
|
+
// Required for firefox, event.preventDefault() in handleKeyDown for
|
|
270
|
+
// the Space key doesn't cancel the handleKeyUp, which in turn
|
|
271
|
+
// triggers a *click*.
|
|
272
|
+
event.preventDefault()
|
|
273
|
+
break
|
|
274
|
+
}
|
|
203
275
|
}
|
|
204
|
-
|
|
205
|
-
const slot = $derived({
|
|
206
|
-
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const ourProps = $derived(
|
|
215
|
-
mergeProps(anchor ? getFloatingPanelProps() : {}, {
|
|
216
|
-
"aria-activedescendant": _state.activeItemIndex === null ? void 0 : _state.items[_state.activeItemIndex]?.id,
|
|
217
|
-
"aria-labelledby": _state.buttonElement?.id,
|
|
218
|
-
id,
|
|
219
|
-
onkeydown: handleKeyDown,
|
|
220
|
-
onkeyup: handleKeyUp,
|
|
221
|
-
role: "menu",
|
|
222
|
-
// When the `Menu` is closed, it should not be focusable. This allows us
|
|
223
|
-
// to skip focusing the `MenuItems` when pressing the tab key on an
|
|
224
|
-
// open `Menu`, and go to the next focusable element.
|
|
225
|
-
tabindex: _state.menuState === MenuStates.Open ? 0 : void 0,
|
|
226
|
-
ref,
|
|
227
|
-
style: [theirProps.style, style, `--button-width: ${buttonSize.width}`].filter(Boolean).join("; "),
|
|
228
|
-
...transitionDataAttributes(transitionData)
|
|
276
|
+
|
|
277
|
+
const slot = $derived({
|
|
278
|
+
open: _state.menuState === MenuStates.Open,
|
|
279
|
+
} satisfies ItemsRenderPropArg)
|
|
280
|
+
|
|
281
|
+
const buttonSize = useElementSize({
|
|
282
|
+
get element() {
|
|
283
|
+
return _state.buttonElement
|
|
284
|
+
},
|
|
285
|
+
unit: true,
|
|
229
286
|
})
|
|
230
|
-
|
|
287
|
+
|
|
288
|
+
const ourProps = $derived(
|
|
289
|
+
mergeProps(anchor ? getFloatingPanelProps() : {}, {
|
|
290
|
+
"aria-activedescendant": _state.activeItemIndex === null ? undefined : _state.items[_state.activeItemIndex]?.id,
|
|
291
|
+
"aria-labelledby": _state.buttonElement?.id,
|
|
292
|
+
id,
|
|
293
|
+
onkeydown: handleKeyDown,
|
|
294
|
+
onkeyup: handleKeyUp,
|
|
295
|
+
role: "menu",
|
|
296
|
+
// When the `Menu` is closed, it should not be focusable. This allows us
|
|
297
|
+
// to skip focusing the `MenuItems` when pressing the tab key on an
|
|
298
|
+
// open `Menu`, and go to the next focusable element.
|
|
299
|
+
tabindex: _state.menuState === MenuStates.Open ? 0 : undefined,
|
|
300
|
+
ref,
|
|
301
|
+
style: [theirProps.style, style, `--button-width: ${buttonSize.width}`].filter(Boolean).join("; "),
|
|
302
|
+
...transitionDataAttributes(transitionData),
|
|
303
|
+
})
|
|
304
|
+
)
|
|
231
305
|
</script>
|
|
232
306
|
|
|
233
307
|
<Portal enabled={portal ? theirProps.static || visible : false}>
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte"
|
|
3
|
+
import type { ElementType, Props } from "../utils/types.js"
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SECTION_TAG = "div" as const
|
|
6
|
+
type SectionRenderPropArg = {}
|
|
7
|
+
type SectionPropsWeControl = "role" | "aria-labelledby"
|
|
8
|
+
|
|
9
|
+
export type MenuSectionProps<TTag extends ElementType = typeof DEFAULT_SECTION_TAG> = Props<
|
|
10
|
+
TTag,
|
|
11
|
+
SectionRenderPropArg,
|
|
12
|
+
SectionPropsWeControl
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
export type MenuSectionChildren = Snippet<[SectionRenderPropArg]>
|
|
2
16
|
</script>
|
|
3
17
|
|
|
4
|
-
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_SECTION_TAG">
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_SECTION_TAG">
|
|
19
|
+
import { useLabels } from "../label/context.svelte.js"
|
|
20
|
+
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
21
|
+
|
|
22
|
+
const labelledby = useLabels()
|
|
23
|
+
|
|
24
|
+
let { ref = $bindable(), ...theirProps }: { as?: TTag } & MenuSectionProps<TTag> = $props()
|
|
25
|
+
const ourProps = $derived({
|
|
26
|
+
"aria-labelledby": labelledby,
|
|
27
|
+
role: "group",
|
|
28
|
+
})
|
|
12
29
|
</script>
|
|
13
30
|
|
|
14
31
|
<ElementOrComponent {ourProps} {theirProps} defaultTag={DEFAULT_SECTION_TAG} name="MenuSection" bind:ref />
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte"
|
|
3
|
+
import type { ElementType, Props } from "../utils/types.js"
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SEPARATOR_TAG = "div" as const
|
|
6
|
+
type SeparatorRenderPropArg = {}
|
|
7
|
+
type SeparatorPropsWeControl = "role"
|
|
8
|
+
|
|
9
|
+
export type MenuSeparatorProps<TTag extends ElementType = typeof DEFAULT_SEPARATOR_TAG> = Props<
|
|
10
|
+
TTag,
|
|
11
|
+
SeparatorRenderPropArg,
|
|
12
|
+
SeparatorPropsWeControl
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
export type MenuSeparatorChildren = Snippet<[SeparatorRenderPropArg]>
|
|
2
16
|
</script>
|
|
3
17
|
|
|
4
|
-
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_SEPARATOR_TAG">
|
|
5
|
-
|
|
6
|
-
|
|
18
|
+
<script lang="ts" generics="TTag extends ElementType = typeof DEFAULT_SEPARATOR_TAG">
|
|
19
|
+
import ElementOrComponent from "../utils/ElementOrComponent.svelte"
|
|
20
|
+
|
|
21
|
+
let { ref = $bindable(), ...theirProps }: { as?: TTag } & MenuSeparatorProps<TTag> = $props()
|
|
22
|
+
const ourProps = { role: "separator" }
|
|
7
23
|
</script>
|
|
8
24
|
|
|
9
25
|
<ElementOrComponent {ourProps} {theirProps} defaultTag={DEFAULT_SEPARATOR_TAG} name="MenuSeparator" bind:ref />
|