@fluixi-ui/dropdown-menu 0.2.0-alpha.17 → 0.2.0-alpha.19

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.
@@ -1,587 +1,794 @@
1
- import { delegateEvents, createComponent, splitProps, createNativeElement, spread, mergeProps, insert, handleRef } from "@fluixi/dom";
2
- import { createSignal, createMemo, createEffect, createRenderEffect, onCleanup } from "@fluixi/reactive/signal";
3
- import { useRef, Show, onMount, For } from "@fluixi/core";
4
- import { createSafeContext, createControllableBooleanSignal, createId, composeId, composeEventHandlers, mergeRefs, dataAttr, focusWithoutScrolling } from "@fluixi-ui/kit";
5
- import { createCollection, createTypeahead, Portal, createFloatingPosition, createDismissableLayer, createFocusScope, createVirtualizer, getNextEnabledIndex } from "@fluixi-ui/primitives";
1
+ import { createComponent, createNativeElement, delegateEvents, handleRef, insert, mergeProps, splitProps, spread } from "@fluixi/dom";
2
+ import { createEffect, createMemo, createRenderEffect, createSignal, onCleanup } from "@fluixi/reactive/signal";
3
+ import { For, Show, onMount, useRef } from "@fluixi/core";
4
+ import { composeEventHandlers, composeId, createControllableBooleanSignal, createId, createSafeContext, dataAttr, focusWithoutScrolling, mergeRefs } from "@fluixi-ui/kit";
5
+ import { Portal, createCollection, createDismissableLayer, createFloatingPosition, createFocusScope, createTypeahead, createVirtualizer, getNextEnabledIndex } from "@fluixi-ui/primitives";
6
6
  import { fxProps, mergeFx } from "@fluixi-ui/fx";
7
+ //#region src/dropdown-menu.tsx
7
8
  delegateEvents(["keydown", "click"]);
8
- const [MenuProvider, useMenuContext] = createSafeContext("DropdownMenu");
9
+ var [MenuProvider, useMenuContext] = createSafeContext("DropdownMenu");
10
+ /**
11
+ * Root of a dropdown menu opened from a trigger button. Manages open state and
12
+ * provides the floating, keyboard-navigable menu (arrow keys, Home/End, Tab to
13
+ * close, and type-ahead) to its parts.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * import {
18
+ * DropdownMenu, DropdownMenuTrigger, DropdownMenuPortal,
19
+ * DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator,
20
+ * DropdownMenuLabel,
21
+ * } from '@fluixi-ui/dropdown-menu';
22
+ *
23
+ * <DropdownMenu>
24
+ * <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
25
+ * <DropdownMenuPortal>
26
+ * <DropdownMenuContent>
27
+ * <DropdownMenuLabel>Edit</DropdownMenuLabel>
28
+ * <DropdownMenuItem onSelect={cut}>Cut</DropdownMenuItem>
29
+ * <DropdownMenuItem onSelect={copy}>Copy</DropdownMenuItem>
30
+ * <DropdownMenuSeparator />
31
+ * <DropdownMenuItem onSelect={remove}>Delete</DropdownMenuItem>
32
+ * </DropdownMenuContent>
33
+ * </DropdownMenuPortal>
34
+ * </DropdownMenu>
35
+ * ```
36
+ */
9
37
  function DropdownMenu(props) {
10
- const [isOpen, setOpen] = createControllableBooleanSignal({
11
- value: () => props.open,
12
- defaultValue: () => props.defaultOpen ?? false,
13
- onChange: (open) => props.onOpenChange?.(open)
14
- });
15
- const baseId = createId("menu");
16
- const [triggerRef, setTriggerRef] = createSignal(void 0);
17
- const collection = createCollection();
18
- const [focusedIndex, setFocusedIndex] = createSignal(-1);
19
- let activeSubClose;
20
- const focusIndex = (index) => {
21
- const item = collection.items()[index];
22
- if (!item) return;
23
- setFocusedIndex(index);
24
- focusWithoutScrolling(item.ref);
25
- };
26
- const firstEnabled = () => collection.items().findIndex((item) => !item.disabled);
27
- const lastEnabled = () => {
28
- const items = collection.items();
29
- for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) return i;
30
- return -1;
31
- };
32
- const move = (action) => {
33
- const next = getNextEnabledIndex(collection.items(), focusedIndex(), action, true);
34
- if (next !== -1) focusIndex(next);
35
- };
36
- const focusFirst = () => {
37
- const i = firstEnabled();
38
- if (i !== -1) focusIndex(i);
39
- };
40
- const focusLast = () => {
41
- const i = lastEnabled();
42
- if (i !== -1) focusIndex(i);
43
- };
44
- const typeahead = createTypeahead({
45
- // Read text from the live node — a portaled ref may register before its text.
46
- getItems: () => collection.items().map((item) => ({ textValue: item.ref.textContent ?? "", disabled: item.disabled })),
47
- getActiveIndex: () => focusedIndex(),
48
- onMatch: (index) => focusIndex(index)
49
- });
50
- const onContentKeyDown = (event) => {
51
- switch (event.key) {
52
- case "ArrowDown":
53
- event.preventDefault();
54
- move("next");
55
- break;
56
- case "ArrowUp":
57
- event.preventDefault();
58
- move("previous");
59
- break;
60
- case "Home":
61
- event.preventDefault();
62
- focusFirst();
63
- break;
64
- case "End":
65
- event.preventDefault();
66
- focusLast();
67
- break;
68
- case "Tab":
69
- event.preventDefault();
70
- setOpen(false);
71
- break;
72
- default:
73
- typeahead.onKeyDown(event);
74
- }
75
- };
76
- const context = {
77
- isOpen,
78
- setOpen: (open) => setOpen(open),
79
- ids: { trigger: composeId(baseId, "trigger"), content: composeId(baseId, "content") },
80
- triggerRef,
81
- setTriggerRef,
82
- collection,
83
- onContentKeyDown,
84
- focusFirst,
85
- select: () => {
86
- activeSubClose?.();
87
- activeSubClose = void 0;
88
- setOpen(false);
89
- },
90
- placement: props.placement ?? "bottom",
91
- offset: props.offset ?? 4,
92
- openSub: (closeFn) => {
93
- activeSubClose?.();
94
- activeSubClose = closeFn;
95
- }
96
- };
97
- return createMemo(() => createComponent(MenuProvider, { get value() {
98
- return context;
99
- }, get children() {
100
- return props.children;
101
- } }));
38
+ const [isOpen, setOpen] = createControllableBooleanSignal({
39
+ value: () => props.open,
40
+ defaultValue: () => props.defaultOpen ?? false,
41
+ onChange: (open) => props.onOpenChange?.(open)
42
+ });
43
+ const baseId = createId("menu");
44
+ const [triggerRef, setTriggerRef] = createSignal(void 0);
45
+ const collection = createCollection();
46
+ const [focusedIndex, setFocusedIndex] = createSignal(-1);
47
+ let activeSubClose;
48
+ const focusIndex = (index) => {
49
+ const item = collection.items()[index];
50
+ if (!item) return;
51
+ setFocusedIndex(index);
52
+ focusWithoutScrolling(item.ref);
53
+ };
54
+ const firstEnabled = () => collection.items().findIndex((item) => !item.disabled);
55
+ const lastEnabled = () => {
56
+ const items = collection.items();
57
+ for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) return i;
58
+ return -1;
59
+ };
60
+ const move = (action) => {
61
+ const next = getNextEnabledIndex(collection.items(), focusedIndex(), action, true);
62
+ if (next !== -1) focusIndex(next);
63
+ };
64
+ const focusFirst = () => {
65
+ const i = firstEnabled();
66
+ if (i !== -1) focusIndex(i);
67
+ };
68
+ const focusLast = () => {
69
+ const i = lastEnabled();
70
+ if (i !== -1) focusIndex(i);
71
+ };
72
+ const typeahead = createTypeahead({
73
+ getItems: () => collection.items().map((item) => ({
74
+ textValue: item.ref.textContent ?? "",
75
+ disabled: item.disabled
76
+ })),
77
+ getActiveIndex: () => focusedIndex(),
78
+ onMatch: (index) => focusIndex(index)
79
+ });
80
+ const onContentKeyDown = (event) => {
81
+ switch (event.key) {
82
+ case "ArrowDown":
83
+ event.preventDefault();
84
+ move("next");
85
+ break;
86
+ case "ArrowUp":
87
+ event.preventDefault();
88
+ move("previous");
89
+ break;
90
+ case "Home":
91
+ event.preventDefault();
92
+ focusFirst();
93
+ break;
94
+ case "End":
95
+ event.preventDefault();
96
+ focusLast();
97
+ break;
98
+ case "Tab":
99
+ event.preventDefault();
100
+ setOpen(false);
101
+ break;
102
+ default: typeahead.onKeyDown(event);
103
+ }
104
+ };
105
+ const context = {
106
+ isOpen,
107
+ setOpen: (open) => setOpen(open),
108
+ ids: {
109
+ trigger: composeId(baseId, "trigger"),
110
+ content: composeId(baseId, "content")
111
+ },
112
+ triggerRef,
113
+ setTriggerRef,
114
+ collection,
115
+ onContentKeyDown,
116
+ focusFirst,
117
+ select: () => {
118
+ activeSubClose?.();
119
+ activeSubClose = void 0;
120
+ setOpen(false);
121
+ },
122
+ placement: props.placement ?? "bottom",
123
+ offset: props.offset ?? 4,
124
+ openSub: (closeFn) => {
125
+ activeSubClose?.();
126
+ activeSubClose = closeFn;
127
+ }
128
+ };
129
+ return createMemo(() => createComponent(MenuProvider, {
130
+ get value() {
131
+ return context;
132
+ },
133
+ get children() {
134
+ return props.children;
135
+ }
136
+ }));
102
137
  }
138
+ /** The button that opens the menu; `aria-haspopup="menu"`. */
103
139
  function DropdownMenuTrigger(props) {
104
- const ctx = useMenuContext();
105
- const [local, others] = splitProps(props, ["onClick", "onKeyDown", "ref", "children", "variant", "fx", "style"]);
106
- return (() => {
107
- const _el$ = createNativeElement("button");
108
- spread({ element: _el$, props: mergeProps(fxProps(
109
- () => local.fx,
110
- () => local.style
111
- ), others, { ref: mergeRefs(ctx.setTriggerRef, local.ref), type: "button", id: () => ctx.ids.trigger, "data-field-variant": () => local.variant && local.variant !== "default" ? local.variant : void 0, "aria-haspopup": "menu", "aria-expanded": () => ctx.isOpen() ? "true" : "false", "aria-controls": () => ctx.ids.content, "data-state": () => ctx.isOpen() ? "open" : "closed", onClick: composeEventHandlers([
112
- local.onClick,
113
- () => ctx.setOpen(!ctx.isOpen())
114
- ]), onKeyDown: composeEventHandlers([
115
- local.onKeyDown,
116
- (event) => {
117
- if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
118
- event.preventDefault();
119
- ctx.setOpen(true);
120
- }
121
- }
122
- ]) }) });
123
- insert(_el$, local.children);
124
- return _el$;
125
- })();
140
+ const ctx = useMenuContext();
141
+ const [local, others] = splitProps(props, [
142
+ "onClick",
143
+ "onKeyDown",
144
+ "ref",
145
+ "children",
146
+ "variant",
147
+ "fx",
148
+ "style"
149
+ ]);
150
+ return (() => {
151
+ const _el$ = createNativeElement("button");
152
+ spread({
153
+ element: _el$,
154
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
155
+ ref: mergeRefs(ctx.setTriggerRef, local.ref),
156
+ type: "button",
157
+ id: () => ctx.ids.trigger,
158
+ "data-field-variant": () => local.variant && local.variant !== "default" ? local.variant : void 0,
159
+ "aria-haspopup": "menu",
160
+ "aria-expanded": () => ctx.isOpen() ? "true" : "false",
161
+ "aria-controls": () => ctx.ids.content,
162
+ "data-state": () => ctx.isOpen() ? "open" : "closed",
163
+ onClick: composeEventHandlers([local.onClick, () => ctx.setOpen(!ctx.isOpen())]),
164
+ onKeyDown: composeEventHandlers([local.onKeyDown, (event) => {
165
+ if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
166
+ event.preventDefault();
167
+ ctx.setOpen(true);
168
+ }
169
+ }])
170
+ })
171
+ });
172
+ insert(_el$, local.children, null);
173
+ return _el$;
174
+ })();
126
175
  }
176
+ /** Portals the menu content to the document body while open. */
127
177
  function DropdownMenuPortal(props) {
128
- const ctx = useMenuContext();
129
- const ref = props.ref || useRef(null);
130
- createEffect(() => {
131
- if (!ctx.isOpen() && ref.current) {
132
- ref.current.remove();
133
- }
134
- });
135
- return createMemo(() => createComponent(Show, { get when() {
136
- return ctx.isOpen();
137
- }, get children() {
138
- return createMemo(() => createComponent(Portal, { get ref() {
139
- return ref;
140
- }, get children() {
141
- return props.children;
142
- } }));
143
- } }));
178
+ const ctx = useMenuContext();
179
+ const ref = props.ref || useRef(null);
180
+ createEffect(() => {
181
+ if (!ctx.isOpen() && ref.current) ref.current.remove();
182
+ });
183
+ return createMemo(() => createComponent(Show, {
184
+ get when() {
185
+ return ctx.isOpen();
186
+ },
187
+ get children() {
188
+ return createMemo(() => createComponent(Portal, {
189
+ get ref() {
190
+ return ref;
191
+ },
192
+ get children() {
193
+ return props.children;
194
+ }
195
+ }));
196
+ }
197
+ }));
144
198
  }
199
+ /** The floating `role="menu"` surface; handles focus and keyboard navigation. */
145
200
  function DropdownMenuContent(props) {
146
- const ctx = useMenuContext();
147
- const [local, others] = splitProps(props, ["ref", "children", "style", "fx", "style"]);
148
- const [element, setElement] = createSignal(void 0);
149
- const position = createFloatingPosition(ctx.triggerRef, element, {
150
- placement: ctx.placement,
151
- offset: ctx.offset
152
- });
153
- createDismissableLayer(element, {
154
- shouldExcludeTarget: (target) => {
155
- const trigger = ctx.triggerRef();
156
- return !!trigger && (trigger === target || trigger.contains(target));
157
- },
158
- onDismiss: () => ctx.setOpen(false)
159
- });
160
- createFocusScope(element, { trap: false, autoFocus: false, restoreFocus: true });
161
- createRenderEffect(() => {
162
- const el = element();
163
- if (!el) return;
164
- el.style.position = "absolute";
165
- el.style.left = `${Math.round(position.x())}px`;
166
- el.style.top = `${Math.round(position.y())}px`;
167
- });
168
- onMount(() => queueMicrotask(() => ctx.focusFirst()));
169
- const setRef = (node) => {
170
- setElement(node);
171
- handleRef(local.ref, node);
172
- };
173
- return (() => {
174
- const _el$ = createNativeElement("div");
175
- spread({ element: _el$, props: mergeProps(fxProps(
176
- () => local.fx,
177
- () => local.style
178
- ), others, { ref: setRef, role: "menu", id: () => ctx.ids.content, "aria-labelledby": () => ctx.ids.trigger, "data-state": "open", "data-placement": () => position.placement(), tabindex: -1, onKeyDown: ctx.onContentKeyDown }) });
179
- insert(_el$, local.children);
180
- return _el$;
181
- })();
201
+ const ctx = useMenuContext();
202
+ const [local, others] = splitProps(props, [
203
+ "ref",
204
+ "children",
205
+ "style",
206
+ "fx",
207
+ "style"
208
+ ]);
209
+ const [element, setElement] = createSignal(void 0);
210
+ const position = createFloatingPosition(ctx.triggerRef, element, {
211
+ placement: ctx.placement,
212
+ offset: ctx.offset
213
+ });
214
+ createDismissableLayer(element, {
215
+ shouldExcludeTarget: (target) => {
216
+ const trigger = ctx.triggerRef();
217
+ if (trigger && (trigger === target || trigger.contains(target))) return true;
218
+ return !!(target instanceof Element ? target : target.parentElement)?.closest(`[data-menu-owner="${ctx.ids.content}"]`);
219
+ },
220
+ onDismiss: () => ctx.setOpen(false)
221
+ });
222
+ createFocusScope(element, {
223
+ trap: false,
224
+ autoFocus: false,
225
+ restoreFocus: true
226
+ });
227
+ createRenderEffect(() => {
228
+ const el = element();
229
+ if (!el) return;
230
+ el.style.position = "absolute";
231
+ el.style.left = `${Math.round(position.x())}px`;
232
+ el.style.top = `${Math.round(position.y())}px`;
233
+ });
234
+ onMount(() => queueMicrotask(() => ctx.focusFirst()));
235
+ const setRef = (node) => {
236
+ setElement(node);
237
+ handleRef(local.ref, node);
238
+ };
239
+ return (() => {
240
+ const _el$ = createNativeElement("div");
241
+ spread({
242
+ element: _el$,
243
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
244
+ ref: setRef,
245
+ role: "menu",
246
+ id: () => ctx.ids.content,
247
+ "aria-labelledby": () => ctx.ids.trigger,
248
+ "data-state": "open",
249
+ "data-placement": () => position.placement(),
250
+ tabindex: -1,
251
+ onKeyDown: ctx.onContentKeyDown
252
+ })
253
+ });
254
+ insert(_el$, local.children, null);
255
+ return _el$;
256
+ })();
182
257
  }
258
+ /**
259
+ * A `role="menu"` surface that virtualizes a large `items` array. Because
260
+ * off-screen rows aren't in the DOM, navigation uses **active-descendant** (the
261
+ * menu keeps focus and highlights the active row) instead of roving focus — the
262
+ * active row is scrolled into view. Choosing a row runs its `onSelect` and closes.
263
+ */
183
264
  function DropdownMenuVirtualContent(props) {
184
- const ctx = useMenuContext();
185
- const [local, others] = splitProps(props, ["ref", "items", "rowHeight", "maxHeight", "style", "fx", "style"]);
186
- const [element, setElement] = createSignal(void 0);
187
- const [scrollEl, setScrollEl] = createSignal(void 0);
188
- const [activeIndex, setActiveIndex] = createSignal(-1);
189
- const items = () => local.items;
190
- const rowHeight = () => local.rowHeight ?? 32;
191
- const itemId = (index) => composeId(ctx.ids.content, `item-${index}`);
192
- const position = createFloatingPosition(ctx.triggerRef, element, {
193
- placement: ctx.placement,
194
- offset: ctx.offset
195
- });
196
- createDismissableLayer(element, {
197
- shouldExcludeTarget: (target) => {
198
- const trigger = ctx.triggerRef();
199
- return !!trigger && (trigger === target || trigger.contains(target));
200
- },
201
- onDismiss: () => ctx.setOpen(false)
202
- });
203
- createFocusScope(element, { trap: false, autoFocus: false, restoreFocus: true });
204
- const virtualizer = createVirtualizer({
205
- count: () => items().length,
206
- estimateSize: () => rowHeight(),
207
- overscan: 6
208
- });
209
- const scrollIntoView = (index) => {
210
- const el = scrollEl();
211
- if (!el || index < 0) return;
212
- const top = index * rowHeight();
213
- const bottom = top + rowHeight();
214
- if (top < el.scrollTop) el.scrollTop = top;
215
- else if (bottom > el.scrollTop + el.clientHeight) el.scrollTop = bottom - el.clientHeight;
216
- };
217
- const move = (action) => {
218
- const next = getNextEnabledIndex(items(), activeIndex(), action, true);
219
- if (next !== -1) {
220
- setActiveIndex(next);
221
- scrollIntoView(next);
222
- }
223
- };
224
- const typeahead = createTypeahead({
225
- getItems: () => items().map((item) => ({ textValue: item.label, disabled: item.disabled })),
226
- getActiveIndex: () => activeIndex(),
227
- onMatch: (index) => {
228
- setActiveIndex(index);
229
- scrollIntoView(index);
230
- }
231
- });
232
- const run = (index) => {
233
- const item = items()[index];
234
- if (!item || item.disabled) return;
235
- item.onSelect();
236
- ctx.setOpen(false);
237
- };
238
- const onKeyDown = (event) => {
239
- switch (event.key) {
240
- case "ArrowDown":
241
- event.preventDefault();
242
- move(activeIndex() === -1 ? "first" : "next");
243
- break;
244
- case "ArrowUp":
245
- event.preventDefault();
246
- move(activeIndex() === -1 ? "last" : "previous");
247
- break;
248
- case "Home":
249
- event.preventDefault();
250
- move("first");
251
- break;
252
- case "End":
253
- event.preventDefault();
254
- move("last");
255
- break;
256
- case "Enter":
257
- case " ":
258
- event.preventDefault();
259
- if (activeIndex() !== -1) run(activeIndex());
260
- break;
261
- case "Tab":
262
- event.preventDefault();
263
- ctx.setOpen(false);
264
- break;
265
- default:
266
- typeahead.onKeyDown(event);
267
- }
268
- };
269
- createRenderEffect(() => {
270
- const el = element();
271
- if (!el) return;
272
- el.style.position = "absolute";
273
- el.style.left = `${Math.round(position.x())}px`;
274
- el.style.top = `${Math.round(position.y())}px`;
275
- });
276
- onMount(
277
- () => queueMicrotask(() => {
278
- element()?.focus();
279
- move("first");
280
- })
281
- );
282
- const setRef = (node) => {
283
- setElement(node);
284
- setScrollEl(node);
285
- virtualizer.scrollRef(node);
286
- handleRef(local.ref, node);
287
- };
288
- return (() => {
289
- const _el$ = createNativeElement("div");
290
- spread({ element: _el$, props: mergeProps(fxProps(
291
- () => mergeFx({ overflow: "auto", maxHeight: local.maxHeight ?? 260 }, local.fx),
292
- () => local.style
293
- ), others, { ref: setRef, role: "menu", id: () => ctx.ids.content, "aria-labelledby": () => ctx.ids.trigger, "aria-activedescendant": () => activeIndex() !== -1 ? itemId(activeIndex()) : void 0, "data-state": "open", "data-placement": () => position.placement(), tabindex: -1, onKeyDown }) });
294
- insert(_el$, (() => {
295
- const _el$2 = createNativeElement("div");
296
- spread({ element: _el$2, props: { style: () => `position:relative;height:${virtualizer.getTotalSize()}px` } });
297
- insert(_el$2, createMemo(() => createComponent(For, { get each() {
298
- return virtualizer.getVirtualItems;
299
- }, get children() {
300
- return (vItem) => {
301
- const item = () => items()[vItem.index];
302
- const active = () => activeIndex() === vItem.index;
303
- const disabled = () => item().disabled ?? false;
304
- return (() => {
305
- const _el$3 = createNativeElement("div");
306
- spread({ element: _el$3, props: { role: "menuitem", id: () => itemId(vItem.index), "aria-disabled": () => disabled() ? "true" : void 0, "data-active": () => dataAttr(active()), "data-disabled": () => dataAttr(disabled()), style: () => `position:absolute;top:${vItem.start}px;left:0;width:100%;height:${vItem.size}px;box-sizing:border-box`, onClick: () => run(vItem.index), onPointerMove: () => {
307
- if (!disabled()) setActiveIndex(vItem.index);
308
- } } });
309
- insert(_el$3, () => item().label, null);
310
- return _el$3;
311
- })();
312
- };
313
- } })), null);
314
- return _el$2;
315
- })());
316
- return _el$;
317
- })();
265
+ const ctx = useMenuContext();
266
+ const [local, others] = splitProps(props, [
267
+ "ref",
268
+ "items",
269
+ "rowHeight",
270
+ "maxHeight",
271
+ "style",
272
+ "fx",
273
+ "style"
274
+ ]);
275
+ const [element, setElement] = createSignal(void 0);
276
+ const [scrollEl, setScrollEl] = createSignal(void 0);
277
+ const [activeIndex, setActiveIndex] = createSignal(-1);
278
+ const items = () => local.items;
279
+ const rowHeight = () => local.rowHeight ?? 32;
280
+ const itemId = (index) => composeId(ctx.ids.content, `item-${index}`);
281
+ const position = createFloatingPosition(ctx.triggerRef, element, {
282
+ placement: ctx.placement,
283
+ offset: ctx.offset
284
+ });
285
+ createDismissableLayer(element, {
286
+ shouldExcludeTarget: (target) => {
287
+ const trigger = ctx.triggerRef();
288
+ if (trigger && (trigger === target || trigger.contains(target))) return true;
289
+ return !!(target instanceof Element ? target : target.parentElement)?.closest(`[data-menu-owner="${ctx.ids.content}"]`);
290
+ },
291
+ onDismiss: () => ctx.setOpen(false)
292
+ });
293
+ createFocusScope(element, {
294
+ trap: false,
295
+ autoFocus: false,
296
+ restoreFocus: true
297
+ });
298
+ const virtualizer = createVirtualizer({
299
+ count: () => items().length,
300
+ estimateSize: () => rowHeight(),
301
+ overscan: 6
302
+ });
303
+ const scrollIntoView = (index) => {
304
+ const el = scrollEl();
305
+ if (!el || index < 0) return;
306
+ const top = index * rowHeight();
307
+ const bottom = top + rowHeight();
308
+ if (top < el.scrollTop) el.scrollTop = top;
309
+ else if (bottom > el.scrollTop + el.clientHeight) el.scrollTop = bottom - el.clientHeight;
310
+ };
311
+ const move = (action) => {
312
+ const next = getNextEnabledIndex(items(), activeIndex(), action, true);
313
+ if (next !== -1) {
314
+ setActiveIndex(next);
315
+ scrollIntoView(next);
316
+ }
317
+ };
318
+ const typeahead = createTypeahead({
319
+ getItems: () => items().map((item) => ({
320
+ textValue: item.label,
321
+ disabled: item.disabled
322
+ })),
323
+ getActiveIndex: () => activeIndex(),
324
+ onMatch: (index) => {
325
+ setActiveIndex(index);
326
+ scrollIntoView(index);
327
+ }
328
+ });
329
+ const run = (index) => {
330
+ const item = items()[index];
331
+ if (!item || item.disabled) return;
332
+ item.onSelect();
333
+ ctx.setOpen(false);
334
+ };
335
+ const onKeyDown = (event) => {
336
+ switch (event.key) {
337
+ case "ArrowDown":
338
+ event.preventDefault();
339
+ move(activeIndex() === -1 ? "first" : "next");
340
+ break;
341
+ case "ArrowUp":
342
+ event.preventDefault();
343
+ move(activeIndex() === -1 ? "last" : "previous");
344
+ break;
345
+ case "Home":
346
+ event.preventDefault();
347
+ move("first");
348
+ break;
349
+ case "End":
350
+ event.preventDefault();
351
+ move("last");
352
+ break;
353
+ case "Enter":
354
+ case " ":
355
+ event.preventDefault();
356
+ if (activeIndex() !== -1) run(activeIndex());
357
+ break;
358
+ case "Tab":
359
+ event.preventDefault();
360
+ ctx.setOpen(false);
361
+ break;
362
+ default: typeahead.onKeyDown(event);
363
+ }
364
+ };
365
+ createRenderEffect(() => {
366
+ const el = element();
367
+ if (!el) return;
368
+ el.style.position = "absolute";
369
+ el.style.left = `${Math.round(position.x())}px`;
370
+ el.style.top = `${Math.round(position.y())}px`;
371
+ });
372
+ onMount(() => queueMicrotask(() => {
373
+ element()?.focus();
374
+ move("first");
375
+ }));
376
+ const setRef = (node) => {
377
+ setElement(node);
378
+ setScrollEl(node);
379
+ virtualizer.scrollRef(node);
380
+ handleRef(local.ref, node);
381
+ };
382
+ return (() => {
383
+ const _el$ = createNativeElement("div");
384
+ spread({
385
+ element: _el$,
386
+ props: mergeProps(fxProps(() => mergeFx({
387
+ overflow: "auto",
388
+ maxHeight: local.maxHeight ?? 260
389
+ }, local.fx), () => local.style), others, {
390
+ ref: setRef,
391
+ role: "menu",
392
+ id: () => ctx.ids.content,
393
+ "aria-labelledby": () => ctx.ids.trigger,
394
+ "aria-activedescendant": () => activeIndex() !== -1 ? itemId(activeIndex()) : void 0,
395
+ "data-state": "open",
396
+ "data-placement": () => position.placement(),
397
+ tabindex: -1,
398
+ onKeyDown
399
+ })
400
+ });
401
+ insert(_el$, (() => {
402
+ const _el$ = createNativeElement("div");
403
+ spread({
404
+ element: _el$,
405
+ props: { style: () => `position:relative;height:${virtualizer.getTotalSize()}px` }
406
+ });
407
+ insert(_el$, createMemo(() => createComponent(For, {
408
+ get each() {
409
+ return virtualizer.getVirtualItems;
410
+ },
411
+ get children() {
412
+ return (vItem) => {
413
+ const item = () => items()[vItem.index];
414
+ const active = () => activeIndex() === vItem.index;
415
+ const disabled = () => item().disabled ?? false;
416
+ return (() => {
417
+ const _el$ = createNativeElement("div");
418
+ spread({
419
+ element: _el$,
420
+ props: {
421
+ role: "menuitem",
422
+ id: () => itemId(vItem.index),
423
+ "aria-disabled": () => disabled() ? "true" : void 0,
424
+ "data-active": () => dataAttr(active()),
425
+ "data-disabled": () => dataAttr(disabled()),
426
+ style: () => `position:absolute;top:${vItem.start}px;left:0;width:100%;height:${vItem.size}px;box-sizing:border-box`,
427
+ onClick: () => run(vItem.index),
428
+ onPointerMove: () => {
429
+ if (!disabled()) setActiveIndex(vItem.index);
430
+ }
431
+ }
432
+ });
433
+ insert(_el$, () => item().label, null);
434
+ return _el$;
435
+ })();
436
+ };
437
+ }
438
+ })), null);
439
+ return _el$;
440
+ })());
441
+ return _el$;
442
+ })();
318
443
  }
444
+ /** A menu action; runs `onSelect` and closes the menu when chosen. */
319
445
  function DropdownMenuItem(props) {
320
- const ctx = useMenuContext();
321
- const [local, others] = splitProps(props, ["disabled", "onSelect", "onClick", "onPointerEnter", "ref", "children", "fx", "style"]);
322
- const setRef = (node) => {
323
- const remove = ctx.collection.register({
324
- ref: node,
325
- textValue: node.textContent ?? "",
326
- disabled: local.disabled
327
- });
328
- onCleanup(remove);
329
- handleRef(local.ref, node);
330
- };
331
- const select = () => {
332
- if (local.disabled) return;
333
- local.onSelect?.();
334
- ctx.select();
335
- };
336
- return (() => {
337
- const _el$ = createNativeElement("div");
338
- spread({ element: _el$, props: mergeProps(fxProps(
339
- () => local.fx,
340
- () => local.style
341
- ), others, { ref: setRef, role: "menuitem", tabindex: -1, "aria-disabled": () => local.disabled ? "true" : void 0, "data-disabled": () => dataAttr(Boolean(local.disabled)), onClick: composeEventHandlers([
342
- local.onClick,
343
- () => select()
344
- ]), onKeyDown: (event) => {
345
- if (event.key === "Enter" || event.key === " ") {
346
- event.preventDefault();
347
- select();
348
- }
349
- }, onPointerEnter: composeEventHandlers([
350
- local.onPointerEnter,
351
- (e) => {
352
- if (local.disabled) return;
353
- e.currentTarget.focus();
354
- }
355
- ]) }) });
356
- insert(_el$, local.children);
357
- return _el$;
358
- })();
446
+ return menuItem(props, "menuitem");
359
447
  }
448
+ /**
449
+ * One choice of a setting (a theme, a sort order): a `menuitemradio` whose `aria-checked` says
450
+ * which one is current, and which skins mark with a check.
451
+ */
452
+ function DropdownMenuRadioItem(props) {
453
+ const [local, others] = splitProps(props, ["checked"]);
454
+ return menuItem(others, "menuitemradio", () => local.checked === true);
455
+ }
456
+ function menuItem(props, role, checked) {
457
+ const ctx = useMenuContext();
458
+ const [local, others] = splitProps(props, [
459
+ "disabled",
460
+ "onSelect",
461
+ "closeOnSelect",
462
+ "onClick",
463
+ "onPointerEnter",
464
+ "ref",
465
+ "children",
466
+ "fx",
467
+ "style"
468
+ ]);
469
+ const setRef = (node) => {
470
+ const remove = ctx.collection.register({
471
+ ref: node,
472
+ textValue: node.textContent ?? "",
473
+ disabled: local.disabled
474
+ });
475
+ onCleanup(remove);
476
+ handleRef(local.ref, node);
477
+ };
478
+ const select = () => {
479
+ if (local.disabled) return;
480
+ local.onSelect?.();
481
+ if (local.closeOnSelect !== false) ctx.select();
482
+ };
483
+ return (() => {
484
+ const _el$ = createNativeElement("div");
485
+ spread({
486
+ element: _el$,
487
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
488
+ ref: setRef,
489
+ role,
490
+ tabindex: -1,
491
+ "aria-checked": checked ? () => checked() ? "true" : "false" : void 0,
492
+ "data-state": checked ? () => checked() ? "checked" : "unchecked" : void 0,
493
+ "aria-disabled": () => local.disabled ? "true" : void 0,
494
+ "data-disabled": () => dataAttr(Boolean(local.disabled)),
495
+ onClick: composeEventHandlers([local.onClick, () => select()]),
496
+ onKeyDown: (event) => {
497
+ if (event.key === "Enter" || event.key === " ") {
498
+ event.preventDefault();
499
+ select();
500
+ }
501
+ },
502
+ onPointerEnter: composeEventHandlers([local.onPointerEnter, (e) => {
503
+ if (local.disabled) return;
504
+ e.currentTarget.focus();
505
+ }])
506
+ })
507
+ });
508
+ insert(_el$, local.children, null);
509
+ return _el$;
510
+ })();
511
+ }
512
+ /** A visual divider between groups of menu items. */
360
513
  function DropdownMenuSeparator(props) {
361
- return (() => {
362
- const _el$ = createNativeElement("div");
363
- spread({ element: _el$, props: mergeProps(props, { role: "separator", "aria-orientation": "horizontal" }) });
364
- return _el$;
365
- })();
514
+ return (() => {
515
+ const _el$ = createNativeElement("div");
516
+ spread({
517
+ element: _el$,
518
+ props: mergeProps(props, {
519
+ role: "separator",
520
+ "aria-orientation": "horizontal"
521
+ })
522
+ });
523
+ return _el$;
524
+ })();
366
525
  }
526
+ /** A non-interactive heading labelling a group of menu items. */
367
527
  function DropdownMenuLabel(props) {
368
- const [local, others] = splitProps(props, ["children", "fx", "style"]);
369
- return (() => {
370
- const _el$ = createNativeElement("div");
371
- spread({ element: _el$, props: mergeProps(fxProps(
372
- () => local.fx,
373
- () => local.style
374
- ), others, { "data-menu-label": "" }) });
375
- insert(_el$, local.children);
376
- return _el$;
377
- })();
528
+ const [local, others] = splitProps(props, [
529
+ "children",
530
+ "fx",
531
+ "style"
532
+ ]);
533
+ return (() => {
534
+ const _el$ = createNativeElement("div");
535
+ spread({
536
+ element: _el$,
537
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, { "data-menu-label": "" })
538
+ });
539
+ insert(_el$, local.children, null);
540
+ return _el$;
541
+ })();
378
542
  }
379
- const [SubMenuProvider, useSubMenuContext] = createSafeContext("DropdownMenuSub");
543
+ var [SubMenuProvider, useSubMenuContext] = createSafeContext("DropdownMenuSub");
544
+ /** Wraps a sub-trigger + sub-content pair to create a nested flyout menu. */
380
545
  function DropdownMenuSub(props) {
381
- const rootCtx = useMenuContext();
382
- const baseId = createId("menu-sub");
383
- const [isOpen, setIsOpen] = createSignal(false);
384
- const [triggerRef, setTriggerRef] = createSignal(void 0);
385
- const subCollection = createCollection();
386
- const [focusedIndex, setFocusedIndex] = createSignal(-1);
387
- let openTimer;
388
- let closeTimer;
389
- onCleanup(() => {
390
- clearTimeout(openTimer);
391
- clearTimeout(closeTimer);
392
- });
393
- const focusIndex = (index) => {
394
- const item = subCollection.items()[index];
395
- if (!item) return;
396
- setFocusedIndex(index);
397
- focusWithoutScrolling(item.ref);
398
- };
399
- const focusFirst = () => {
400
- const i = subCollection.items().findIndex((item) => !item.disabled);
401
- if (i !== -1) focusIndex(i);
402
- };
403
- const openSub = () => {
404
- rootCtx.openSub(() => setIsOpen(false));
405
- setIsOpen(true);
406
- };
407
- createEffect(() => {
408
- if (!rootCtx.isOpen()) setIsOpen(false);
409
- });
410
- const context = {
411
- isOpen,
412
- setOpen: (open) => {
413
- if (open) openSub();
414
- else setIsOpen(false);
415
- },
416
- ids: { trigger: composeId(baseId, "trigger"), content: composeId(baseId, "content") },
417
- triggerRef,
418
- setTriggerRef,
419
- collection: subCollection,
420
- focusedIndex,
421
- focusIndex,
422
- focusFirst,
423
- scheduleOpen: () => {
424
- clearTimeout(closeTimer);
425
- openTimer = setTimeout(openSub, 200);
426
- },
427
- scheduleClose: (movingTowardSub = false) => {
428
- clearTimeout(openTimer);
429
- closeTimer = setTimeout(() => setIsOpen(false), movingTowardSub ? 500 : 200);
430
- },
431
- cancelClose: () => clearTimeout(closeTimer),
432
- closeRoot: () => rootCtx.select()
433
- };
434
- return createMemo(() => createComponent(SubMenuProvider, { get value() {
435
- return context;
436
- }, get children() {
437
- return props.children;
438
- } }));
546
+ const rootCtx = useMenuContext();
547
+ const baseId = createId("menu-sub");
548
+ const [isOpen, setIsOpen] = createSignal(false);
549
+ const [triggerRef, setTriggerRef] = createSignal(void 0);
550
+ const subCollection = createCollection();
551
+ const [focusedIndex, setFocusedIndex] = createSignal(-1);
552
+ let openTimer;
553
+ let closeTimer;
554
+ onCleanup(() => {
555
+ clearTimeout(openTimer);
556
+ clearTimeout(closeTimer);
557
+ });
558
+ const focusIndex = (index) => {
559
+ const item = subCollection.items()[index];
560
+ if (!item) return;
561
+ setFocusedIndex(index);
562
+ focusWithoutScrolling(item.ref);
563
+ };
564
+ const focusFirst = () => {
565
+ const i = subCollection.items().findIndex((item) => !item.disabled);
566
+ if (i !== -1) focusIndex(i);
567
+ };
568
+ const openSub = () => {
569
+ rootCtx.openSub(() => setIsOpen(false));
570
+ setIsOpen(true);
571
+ };
572
+ createEffect(() => {
573
+ if (!rootCtx.isOpen()) setIsOpen(false);
574
+ });
575
+ const context = {
576
+ isOpen,
577
+ setOpen: (open) => {
578
+ if (open) openSub();
579
+ else setIsOpen(false);
580
+ },
581
+ ids: {
582
+ trigger: composeId(baseId, "trigger"),
583
+ content: composeId(baseId, "content")
584
+ },
585
+ triggerRef,
586
+ setTriggerRef,
587
+ collection: subCollection,
588
+ focusedIndex,
589
+ focusIndex,
590
+ focusFirst,
591
+ scheduleOpen: () => {
592
+ clearTimeout(closeTimer);
593
+ openTimer = setTimeout(openSub, 200);
594
+ },
595
+ scheduleClose: (movingTowardSub = false) => {
596
+ clearTimeout(openTimer);
597
+ closeTimer = setTimeout(() => setIsOpen(false), movingTowardSub ? 500 : 200);
598
+ },
599
+ cancelClose: () => clearTimeout(closeTimer),
600
+ closeRoot: () => rootCtx.select()
601
+ };
602
+ return createMemo(() => createComponent(SubMenuProvider, {
603
+ get value() {
604
+ return context;
605
+ },
606
+ get children() {
607
+ return props.children;
608
+ }
609
+ }));
439
610
  }
611
+ /** A `menuitem` inside a menu that opens a nested flyout on hover or `ArrowRight`. */
440
612
  function DropdownMenuSubTrigger(props) {
441
- const ctx = useMenuContext();
442
- const sub = useSubMenuContext();
443
- const [local, others] = splitProps(props, ["disabled", "onClick", "onKeyDown", "onPointerEnter", "onPointerLeave", "ref", "children", "fx", "style"]);
444
- const setRef = (node) => {
445
- sub.setTriggerRef(node);
446
- const remove = ctx.collection.register({ ref: node, textValue: node.textContent ?? "", disabled: local.disabled });
447
- onCleanup(remove);
448
- handleRef(local.ref, node);
449
- };
450
- const open = () => {
451
- sub.setOpen(true);
452
- queueMicrotask(() => sub.focusFirst());
453
- };
454
- return (() => {
455
- const _el$ = createNativeElement("div");
456
- spread({ element: _el$, props: mergeProps(fxProps(
457
- () => local.fx,
458
- () => local.style
459
- ), others, { ref: setRef, role: "menuitem", tabindex: -1, "aria-haspopup": "menu", "aria-expanded": () => sub.isOpen() ? "true" : "false", "aria-controls": () => sub.ids.content, "data-state": () => sub.isOpen() ? "open" : "closed", "data-disabled": () => dataAttr(Boolean(local.disabled)), "aria-disabled": () => local.disabled ? "true" : void 0, onPointerEnter: composeEventHandlers([
460
- local.onPointerEnter,
461
- (e) => {
462
- if (local.disabled) return;
463
- e.currentTarget.focus();
464
- sub.scheduleOpen();
465
- }
466
- ]), onPointerLeave: composeEventHandlers([
467
- local.onPointerLeave,
468
- (e) => sub.scheduleClose(e.movementX > 0)
469
- ]), onKeyDown: composeEventHandlers([
470
- local.onKeyDown,
471
- (e) => {
472
- if (local.disabled) return;
473
- if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
474
- e.preventDefault();
475
- e.stopPropagation();
476
- open();
477
- }
478
- }
479
- ]) }) });
480
- insert(_el$, local.children);
481
- return _el$;
482
- })();
613
+ const ctx = useMenuContext();
614
+ const sub = useSubMenuContext();
615
+ const [local, others] = splitProps(props, [
616
+ "disabled",
617
+ "onClick",
618
+ "onKeyDown",
619
+ "onPointerEnter",
620
+ "onPointerLeave",
621
+ "ref",
622
+ "children",
623
+ "fx",
624
+ "style"
625
+ ]);
626
+ const setRef = (node) => {
627
+ sub.setTriggerRef(node);
628
+ const remove = ctx.collection.register({
629
+ ref: node,
630
+ textValue: node.textContent ?? "",
631
+ disabled: local.disabled
632
+ });
633
+ onCleanup(remove);
634
+ handleRef(local.ref, node);
635
+ };
636
+ const open = () => {
637
+ sub.setOpen(true);
638
+ queueMicrotask(() => sub.focusFirst());
639
+ };
640
+ return (() => {
641
+ const _el$ = createNativeElement("div");
642
+ spread({
643
+ element: _el$,
644
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
645
+ ref: setRef,
646
+ role: "menuitem",
647
+ tabindex: -1,
648
+ "aria-haspopup": "menu",
649
+ "aria-expanded": () => sub.isOpen() ? "true" : "false",
650
+ "aria-controls": () => sub.ids.content,
651
+ "data-state": () => sub.isOpen() ? "open" : "closed",
652
+ "data-disabled": () => dataAttr(Boolean(local.disabled)),
653
+ "aria-disabled": () => local.disabled ? "true" : void 0,
654
+ onPointerEnter: composeEventHandlers([local.onPointerEnter, (e) => {
655
+ if (local.disabled) return;
656
+ e.currentTarget.focus();
657
+ sub.scheduleOpen();
658
+ }]),
659
+ onPointerLeave: composeEventHandlers([local.onPointerLeave, (e) => sub.scheduleClose(e.movementX > 0)]),
660
+ onKeyDown: composeEventHandlers([local.onKeyDown, (e) => {
661
+ if (local.disabled) return;
662
+ if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
663
+ e.preventDefault();
664
+ e.stopPropagation();
665
+ open();
666
+ }
667
+ }])
668
+ })
669
+ });
670
+ insert(_el$, local.children, null);
671
+ return _el$;
672
+ })();
483
673
  }
674
+ /** The floating `role="menu"` surface for a nested submenu. Self-portals while open. */
484
675
  function DropdownMenuSubContent(props) {
485
- const sub = useSubMenuContext();
486
- const [local, others] = splitProps(props, ["ref", "children", "style", "fx", "style"]);
487
- const [element, setElement] = createSignal(void 0);
488
- const portalRef = useRef(null);
489
- createEffect(() => {
490
- if (!sub.isOpen() && portalRef.current) portalRef.current.remove();
491
- });
492
- const position = createFloatingPosition(sub.triggerRef, element, {
493
- placement: "right-start",
494
- offset: -2
495
- });
496
- createRenderEffect(() => {
497
- const el = element();
498
- if (!el) return;
499
- el.style.position = "absolute";
500
- el.style.left = `${Math.round(position.x())}px`;
501
- el.style.top = `${Math.round(position.y())}px`;
502
- });
503
- onMount(() => queueMicrotask(() => sub.focusFirst()));
504
- const move = (action) => {
505
- const next = getNextEnabledIndex(sub.collection.items(), sub.focusedIndex(), action, true);
506
- if (next !== -1) sub.focusIndex(next);
507
- };
508
- const focusLast = () => {
509
- const items = sub.collection.items();
510
- for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) {
511
- sub.focusIndex(i);
512
- return;
513
- }
514
- };
515
- const typeahead = createTypeahead({
516
- getItems: () => sub.collection.items().map((item) => ({ textValue: item.ref.textContent ?? "", disabled: item.disabled })),
517
- getActiveIndex: () => sub.focusedIndex(),
518
- onMatch: (index) => sub.focusIndex(index)
519
- });
520
- const onContentKeyDown = (event) => {
521
- switch (event.key) {
522
- case "ArrowDown":
523
- event.preventDefault();
524
- move("next");
525
- break;
526
- case "ArrowUp":
527
- event.preventDefault();
528
- move("previous");
529
- break;
530
- case "Home":
531
- event.preventDefault();
532
- sub.focusFirst();
533
- break;
534
- case "End":
535
- event.preventDefault();
536
- focusLast();
537
- break;
538
- case "ArrowLeft":
539
- case "Escape":
540
- event.preventDefault();
541
- event.stopPropagation();
542
- sub.setOpen(false);
543
- focusWithoutScrolling(sub.triggerRef());
544
- break;
545
- case "Tab":
546
- event.preventDefault();
547
- sub.closeRoot();
548
- break;
549
- default:
550
- typeahead.onKeyDown(event);
551
- }
552
- };
553
- const setRef = (node) => {
554
- setElement(node);
555
- handleRef(local.ref, node);
556
- };
557
- return createMemo(() => createComponent(Show, { get when() {
558
- return sub.isOpen();
559
- }, get children() {
560
- return createMemo(() => createComponent(Portal, { get ref() {
561
- return portalRef;
562
- }, get children() {
563
- return (() => {
564
- const _el$ = createNativeElement("div");
565
- spread({ element: _el$, props: mergeProps(fxProps(
566
- () => local.fx,
567
- () => local.style
568
- ), others, { ref: setRef, role: "menu", id: () => sub.ids.content, "aria-labelledby": () => sub.ids.trigger, "data-state": "open", "data-placement": () => position.placement(), tabindex: -1, onKeyDown: onContentKeyDown, onPointerEnter: () => sub.cancelClose(), onPointerLeave: () => sub.scheduleClose() }) });
569
- insert(_el$, local.children);
570
- return _el$;
571
- })();
572
- } }));
573
- } }));
676
+ const sub = useSubMenuContext();
677
+ const root = useMenuContext();
678
+ const [local, others] = splitProps(props, [
679
+ "ref",
680
+ "children",
681
+ "style",
682
+ "fx",
683
+ "style"
684
+ ]);
685
+ const [element, setElement] = createSignal(void 0);
686
+ const portalRef = useRef(null);
687
+ createEffect(() => {
688
+ if (!sub.isOpen() && portalRef.current) portalRef.current.remove();
689
+ });
690
+ const position = createFloatingPosition(sub.triggerRef, element, {
691
+ placement: "right-start",
692
+ offset: -2
693
+ });
694
+ createRenderEffect(() => {
695
+ const el = element();
696
+ if (!el) return;
697
+ el.style.position = "absolute";
698
+ el.style.left = `${Math.round(position.x())}px`;
699
+ el.style.top = `${Math.round(position.y())}px`;
700
+ });
701
+ onMount(() => queueMicrotask(() => sub.focusFirst()));
702
+ const move = (action) => {
703
+ const next = getNextEnabledIndex(sub.collection.items(), sub.focusedIndex(), action, true);
704
+ if (next !== -1) sub.focusIndex(next);
705
+ };
706
+ const focusLast = () => {
707
+ const items = sub.collection.items();
708
+ for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) {
709
+ sub.focusIndex(i);
710
+ return;
711
+ }
712
+ };
713
+ const typeahead = createTypeahead({
714
+ getItems: () => sub.collection.items().map((item) => ({
715
+ textValue: item.ref.textContent ?? "",
716
+ disabled: item.disabled
717
+ })),
718
+ getActiveIndex: () => sub.focusedIndex(),
719
+ onMatch: (index) => sub.focusIndex(index)
720
+ });
721
+ const onContentKeyDown = (event) => {
722
+ switch (event.key) {
723
+ case "ArrowDown":
724
+ event.preventDefault();
725
+ move("next");
726
+ break;
727
+ case "ArrowUp":
728
+ event.preventDefault();
729
+ move("previous");
730
+ break;
731
+ case "Home":
732
+ event.preventDefault();
733
+ sub.focusFirst();
734
+ break;
735
+ case "End":
736
+ event.preventDefault();
737
+ focusLast();
738
+ break;
739
+ case "ArrowLeft":
740
+ case "Escape":
741
+ event.preventDefault();
742
+ event.stopPropagation();
743
+ sub.setOpen(false);
744
+ focusWithoutScrolling(sub.triggerRef());
745
+ break;
746
+ case "Tab":
747
+ event.preventDefault();
748
+ sub.closeRoot();
749
+ break;
750
+ default: typeahead.onKeyDown(event);
751
+ }
752
+ };
753
+ const setRef = (node) => {
754
+ setElement(node);
755
+ handleRef(local.ref, node);
756
+ };
757
+ return createMemo(() => createComponent(Show, {
758
+ get when() {
759
+ return sub.isOpen();
760
+ },
761
+ get children() {
762
+ return createMemo(() => createComponent(Portal, {
763
+ get ref() {
764
+ return portalRef;
765
+ },
766
+ get children() {
767
+ return (() => {
768
+ const _el$ = createNativeElement("div");
769
+ spread({
770
+ element: _el$,
771
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
772
+ ref: setRef,
773
+ role: "menu",
774
+ id: () => sub.ids.content,
775
+ "data-menu-owner": () => root.ids.content,
776
+ "aria-labelledby": () => sub.ids.trigger,
777
+ "data-state": "open",
778
+ "data-placement": () => position.placement(),
779
+ tabindex: -1,
780
+ onKeyDown: onContentKeyDown,
781
+ onPointerEnter: () => sub.cancelClose(),
782
+ onPointerLeave: () => sub.scheduleClose()
783
+ })
784
+ });
785
+ insert(_el$, local.children, null);
786
+ return _el$;
787
+ })();
788
+ }
789
+ }));
790
+ }
791
+ }));
574
792
  }
575
- export {
576
- DropdownMenu,
577
- DropdownMenuContent,
578
- DropdownMenuItem,
579
- DropdownMenuLabel,
580
- DropdownMenuPortal,
581
- DropdownMenuSeparator,
582
- DropdownMenuSub,
583
- DropdownMenuSubContent,
584
- DropdownMenuSubTrigger,
585
- DropdownMenuTrigger,
586
- DropdownMenuVirtualContent
587
- };
793
+ //#endregion
794
+ export { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, DropdownMenuVirtualContent };