UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

1,326 lines (1,316 loc) 44.4 kB
import { Popper } from './LMWVDFW6.js'; import { createSelectableList } from './GLKC2QFF.js'; import { createSelectableItem, createListState } from './H6DSIDEC.js'; import { createDomCollectionItem, useOptionalDomCollectionContext, createDomCollection } from './7CVNMTYF.js'; import { useLocale } from './XHJPQEZP.js'; import { createFocusScope } from './ISKHZMHS.js'; import { createHideOutside } from './455ZADO2.js'; import { DismissableLayer } from './BASJUNIE.js'; import { createDisclosureState } from './7LCANGHD.js'; import { ButtonRoot } from './7OVKXYPU.js'; import { createToggleState } from './YGDQXQ2B.js'; import { createRegisterId } from './E4R2EMM4.js'; import { createControllableSignal } from './BLN63FDC.js'; import { createTagName } from './ET5T45DO.js'; import { Polymorphic } from './6Y7B2NEO.js'; import { createContext, useContext, splitProps, createMemo, createEffect, on, onCleanup, createUniqueId, createSignal, Show } from 'solid-js'; import { createComponent, mergeProps, memo, Portal, isServer } from 'solid-js/web'; import { mergeDefaultProps, mergeRefs, createGenerateId, composeEventHandlers, callHandler, scrollIntoViewport, contains, focusWithoutScrolling, removeItemFromArray, isPointInPolygon } from '@kobalte/utils'; import createPreventScroll from 'solid-prevent-scroll'; import { combineStyle } from '@solid-primitives/props'; import createPresence from 'solid-presence'; var MenubarContext = createContext(); function useOptionalMenubarContext() { return useContext(MenubarContext); } function useMenubarContext() { const context = useOptionalMenubarContext(); if (context === void 0) { throw new Error("[kobalte]: `useMenubarContext` must be used within a `Menubar` component"); } return context; } var NavigationMenuContext = createContext(); function useOptionalNavigationMenuContext() { return useContext(NavigationMenuContext); } function useNavigationMenuContext() { const context = useOptionalNavigationMenuContext(); if (context === void 0) { throw new Error("[kobalte]: `useNavigationMenuContext` must be used within a `NavigationMenu` component"); } return context; } var MenuContext = createContext(); function useOptionalMenuContext() { return useContext(MenuContext); } function useMenuContext() { const context = useOptionalMenuContext(); if (context === void 0) { throw new Error("[kobalte]: `useMenuContext` must be used within a `Menu` component"); } return context; } var MenuItemContext = createContext(); function useMenuItemContext() { const context = useContext(MenuItemContext); if (context === void 0) { throw new Error("[kobalte]: `useMenuItemContext` must be used within a `Menu.Item` component"); } return context; } var MenuRootContext = createContext(); function useMenuRootContext() { const context = useContext(MenuRootContext); if (context === void 0) { throw new Error("[kobalte]: `useMenuRootContext` must be used within a `MenuRoot` component"); } return context; } // src/menu/menu-item-base.tsx function MenuItemBase(props) { let ref; const rootContext = useMenuRootContext(); const menuContext = useMenuContext(); const mergedProps = mergeDefaultProps({ id: rootContext.generateId(`item-${createUniqueId()}`) }, props); const [local, others] = splitProps(mergedProps, ["ref", "textValue", "disabled", "closeOnSelect", "checked", "indeterminate", "onSelect", "onPointerMove", "onPointerLeave", "onPointerDown", "onPointerUp", "onClick", "onKeyDown", "onMouseDown", "onFocus"]); const [labelId, setLabelId] = createSignal(); const [descriptionId, setDescriptionId] = createSignal(); const [labelRef, setLabelRef] = createSignal(); const selectionManager = () => menuContext.listState().selectionManager(); const key = () => others.id; const isHighlighted = () => selectionManager().focusedKey() === key(); const onSelect = () => { local.onSelect?.(); if (local.closeOnSelect) { setTimeout(() => { menuContext.close(true); }); } }; createDomCollectionItem({ getItem: () => ({ ref: () => ref, type: "item", key: key(), textValue: local.textValue ?? labelRef()?.textContent ?? ref?.textContent ?? "", disabled: local.disabled ?? false }) }); const selectableItem = createSelectableItem({ key, selectionManager, shouldSelectOnPressUp: true, allowsDifferentPressOrigin: true, disabled: () => local.disabled }, () => ref); const onPointerMove = (e) => { callHandler(e, local.onPointerMove); if (e.pointerType !== "mouse") { return; } if (local.disabled) { menuContext.onItemLeave(e); } else { menuContext.onItemEnter(e); if (!e.defaultPrevented) { focusWithoutScrolling(e.currentTarget); menuContext.listState().selectionManager().setFocused(true); menuContext.listState().selectionManager().setFocusedKey(key()); } } }; const onPointerLeave = (e) => { callHandler(e, local.onPointerLeave); if (e.pointerType !== "mouse") { return; } menuContext.onItemLeave(e); }; const onPointerUp = (e) => { callHandler(e, local.onPointerUp); if (!local.disabled && e.button === 0) { onSelect(); } }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); if (e.repeat) { return; } if (local.disabled) { return; } switch (e.key) { case "Enter": case " ": onSelect(); break; } }; const ariaChecked = createMemo(() => { if (local.indeterminate) { return "mixed"; } if (local.checked == null) { return void 0; } return local.checked; }); const dataset = createMemo(() => ({ "data-indeterminate": local.indeterminate ? "" : void 0, "data-checked": local.checked && !local.indeterminate ? "" : void 0, "data-disabled": local.disabled ? "" : void 0, "data-highlighted": isHighlighted() ? "" : void 0 })); const context = { isChecked: () => local.checked, dataset, setLabelRef, generateId: createGenerateId(() => others.id), registerLabel: createRegisterId(setLabelId), registerDescription: createRegisterId(setDescriptionId) }; return createComponent(MenuItemContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs((el) => ref = el, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get tabIndex() { return selectableItem.tabIndex(); }, get ["aria-checked"]() { return ariaChecked(); }, get ["aria-disabled"]() { return local.disabled; }, get ["aria-labelledby"]() { return labelId(); }, get ["aria-describedby"]() { return descriptionId(); }, get ["data-key"]() { return selectableItem.dataKey(); }, get onPointerDown() { return composeEventHandlers([local.onPointerDown, selectableItem.onPointerDown]); }, get onPointerUp() { return composeEventHandlers([onPointerUp, selectableItem.onPointerUp]); }, get onClick() { return composeEventHandlers([local.onClick, selectableItem.onClick]); }, get onKeyDown() { return composeEventHandlers([onKeyDown, selectableItem.onKeyDown]); }, get onMouseDown() { return composeEventHandlers([local.onMouseDown, selectableItem.onMouseDown]); }, get onFocus() { return composeEventHandlers([local.onFocus, selectableItem.onFocus]); }, onPointerMove, onPointerLeave }, dataset, others)); } }); } // src/menu/menu-checkbox-item.tsx function MenuCheckboxItem(props) { const mergedProps = mergeDefaultProps({ closeOnSelect: false }, props); const [local, others] = splitProps(mergedProps, ["checked", "defaultChecked", "onChange", "onSelect"]); const state = createToggleState({ isSelected: () => local.checked, defaultIsSelected: () => local.defaultChecked, onSelectedChange: (checked) => local.onChange?.(checked), isDisabled: () => others.disabled }); const onSelect = () => { local.onSelect?.(); state.toggle(); }; return createComponent(MenuItemBase, mergeProps({ role: "menuitemcheckbox", get checked() { return state.isSelected(); }, onSelect }, others)); } var MENUBAR_KEYS = { next: (dir, orientation) => dir === "ltr" ? orientation === "horizontal" ? "ArrowRight" : "ArrowDown" : orientation === "horizontal" ? "ArrowLeft" : "ArrowUp", previous: (dir, orientation) => MENUBAR_KEYS.next(dir === "ltr" ? "rtl" : "ltr", orientation) }; var MENU_KEYS = { first: (orientation) => orientation === "horizontal" ? "ArrowDown" : "ArrowRight", last: (orientation) => orientation === "horizontal" ? "ArrowUp" : "ArrowLeft" }; function MenuTrigger(props) { const rootContext = useMenuRootContext(); const context = useMenuContext(); const optionalMenubarContext = useOptionalMenubarContext(); const { direction } = useLocale(); const mergedProps = mergeDefaultProps({ id: rootContext.generateId("trigger") }, props); const [local, others] = splitProps(mergedProps, ["ref", "id", "disabled", "onPointerDown", "onClick", "onKeyDown", "onMouseOver", "onFocus"]); let key = () => rootContext.value(); if (optionalMenubarContext !== void 0) { key = () => rootContext.value() ?? local.id; if (optionalMenubarContext.lastValue() === void 0) optionalMenubarContext.setLastValue(key); } const tagName = createTagName(() => context.triggerRef(), () => "button"); const isNativeLink = createMemo(() => { return tagName() === "a" && context.triggerRef()?.getAttribute("href") != null; }); createEffect(on(() => optionalMenubarContext?.value(), (value) => { if (!isNativeLink()) return; if (value === key()) context.triggerRef()?.focus(); })); const handleClick = () => { if (optionalMenubarContext !== void 0) { if (!context.isOpen()) { if (!optionalMenubarContext.autoFocusMenu()) { optionalMenubarContext.setAutoFocusMenu(true); } context.open(false); } else { if (optionalMenubarContext.value() === key()) optionalMenubarContext.closeMenu(); } } else context.toggle(true); }; const onPointerDown = (e) => { callHandler(e, local.onPointerDown); e.currentTarget.dataset.pointerType = e.pointerType; if (!local.disabled && e.pointerType !== "touch" && e.button === 0) { handleClick(); } }; const onClick = (e) => { callHandler(e, local.onClick); if (!local.disabled) { if (e.currentTarget.dataset.pointerType === "touch") handleClick(); } }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); if (local.disabled) { return; } if (isNativeLink()) { switch (e.key) { case "Enter": case " ": return; } } switch (e.key) { case "Enter": case " ": case MENU_KEYS.first(rootContext.orientation()): e.stopPropagation(); e.preventDefault(); scrollIntoViewport(e.currentTarget); context.open("first"); optionalMenubarContext?.setAutoFocusMenu(true); optionalMenubarContext?.setValue(key); break; case MENU_KEYS.last(rootContext.orientation()): e.stopPropagation(); e.preventDefault(); context.open("last"); break; case MENUBAR_KEYS.next(direction(), rootContext.orientation()): if (optionalMenubarContext === void 0) break; e.stopPropagation(); e.preventDefault(); optionalMenubarContext.nextMenu(); break; case MENUBAR_KEYS.previous(direction(), rootContext.orientation()): if (optionalMenubarContext === void 0) break; e.stopPropagation(); e.preventDefault(); optionalMenubarContext.previousMenu(); break; } }; const onMouseOver = (e) => { callHandler(e, local.onMouseOver); if (context.triggerRef()?.dataset.pointerType === "touch") return; if (!local.disabled && optionalMenubarContext !== void 0 && optionalMenubarContext.value() !== void 0) { optionalMenubarContext.setValue(key); } }; const onFocus = (e) => { callHandler(e, local.onFocus); if (optionalMenubarContext !== void 0 && e.currentTarget.dataset.pointerType !== "touch") optionalMenubarContext.setValue(key); }; createEffect(() => onCleanup(context.registerTriggerId(local.id))); return createComponent(ButtonRoot, mergeProps({ ref(r$) { const _ref$ = mergeRefs(context.setTriggerRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get ["data-kb-menu-value-trigger"]() { return rootContext.value(); }, get id() { return local.id; }, get disabled() { return local.disabled; }, "aria-haspopup": "true", get ["aria-expanded"]() { return context.isOpen(); }, get ["aria-controls"]() { return memo(() => !!context.isOpen())() ? context.contentId() : void 0; }, get ["data-highlighted"]() { return key() !== void 0 && optionalMenubarContext?.value() === key() ? true : void 0; }, get tabIndex() { return optionalMenubarContext !== void 0 ? optionalMenubarContext.value() === key() || optionalMenubarContext.lastValue() === key() ? 0 : -1 : void 0; }, onPointerDown, onMouseOver, onClick, onKeyDown, onFocus, role: optionalMenubarContext !== void 0 ? "menuitem" : void 0 }, () => context.dataset(), others)); } function MenuContentBase(props) { let ref; const rootContext = useMenuRootContext(); const context = useMenuContext(); const optionalMenubarContext = useOptionalMenubarContext(); const optionalNavigationMenuContext = useOptionalNavigationMenuContext(); const { direction } = useLocale(); const mergedProps = mergeDefaultProps({ id: rootContext.generateId(`content-${createUniqueId()}`) }, props); const [local, others] = splitProps(mergedProps, ["ref", "id", "style", "onOpenAutoFocus", "onCloseAutoFocus", "onEscapeKeyDown", "onFocusOutside", "onPointerEnter", "onPointerMove", "onKeyDown", "onMouseDown", "onFocusIn", "onFocusOut"]); let lastPointerX = 0; const isRootModalContent = () => { return context.parentMenuContext() == null && optionalMenubarContext === void 0 && rootContext.isModal(); }; const selectableList = createSelectableList({ selectionManager: context.listState().selectionManager, collection: context.listState().collection, autoFocus: context.autoFocus, deferAutoFocus: true, // ensure all menu items are mounted and collection is not empty before trying to autofocus. shouldFocusWrap: true, disallowTypeAhead: () => !context.listState().selectionManager().isFocused(), orientation: () => rootContext.orientation() === "horizontal" ? "vertical" : "horizontal" }, () => ref); createFocusScope({ trapFocus: () => isRootModalContent() && context.isOpen(), onMountAutoFocus: (event) => { if (optionalMenubarContext === void 0) local.onOpenAutoFocus?.(event); }, onUnmountAutoFocus: local.onCloseAutoFocus }, () => ref); const onKeyDown = (e) => { if (!contains(e.currentTarget, e.target)) { return; } if (e.key === "Tab" && context.isOpen()) { e.preventDefault(); } if (optionalMenubarContext !== void 0) { if (e.currentTarget.getAttribute("aria-haspopup") !== "true") switch (e.key) { case MENUBAR_KEYS.next(direction(), rootContext.orientation()): e.stopPropagation(); e.preventDefault(); context.close(true); optionalMenubarContext.setAutoFocusMenu(true); optionalMenubarContext.nextMenu(); break; case MENUBAR_KEYS.previous(direction(), rootContext.orientation()): if (e.currentTarget.hasAttribute("data-closed")) break; e.stopPropagation(); e.preventDefault(); context.close(true); optionalMenubarContext.setAutoFocusMenu(true); optionalMenubarContext.previousMenu(); break; } } }; const onEscapeKeyDown = (e) => { local.onEscapeKeyDown?.(e); optionalMenubarContext?.setAutoFocusMenu(false); context.close(true); }; const onFocusOutside = (e) => { local.onFocusOutside?.(e); if (rootContext.isModal()) { e.preventDefault(); } }; const onPointerEnter = (e) => { callHandler(e, local.onPointerEnter); if (!context.isOpen()) { return; } context.parentMenuContext()?.listState().selectionManager().setFocused(false); context.parentMenuContext()?.listState().selectionManager().setFocusedKey(void 0); }; const onPointerMove = (e) => { callHandler(e, local.onPointerMove); if (e.pointerType !== "mouse") { return; } const target = e.target; const pointerXHasChanged = lastPointerX !== e.clientX; if (contains(e.currentTarget, target) && pointerXHasChanged) { context.setPointerDir(e.clientX > lastPointerX ? "right" : "left"); lastPointerX = e.clientX; } }; createEffect(() => onCleanup(context.registerContentId(local.id))); onCleanup(() => context.setContentRef(void 0)); const commonAttributes = { ref: mergeRefs((el) => { context.setContentRef(el); ref = el; }, local.ref), role: "menu", get id() { return local.id; }, get tabIndex() { return selectableList.tabIndex(); }, get "aria-labelledby"() { return context.triggerId(); }, onKeyDown: composeEventHandlers([local.onKeyDown, selectableList.onKeyDown, onKeyDown]), onMouseDown: composeEventHandlers([local.onMouseDown, selectableList.onMouseDown]), onFocusIn: composeEventHandlers([local.onFocusIn, selectableList.onFocusIn]), onFocusOut: composeEventHandlers([local.onFocusOut, selectableList.onFocusOut]), onPointerEnter, onPointerMove, get "data-orientation"() { return rootContext.orientation(); } }; return createComponent(Show, { get when() { return context.contentPresent(); }, get children() { return createComponent(Show, { get when() { return optionalNavigationMenuContext === void 0 || context.parentMenuContext() != null; }, get fallback() { return createComponent(Polymorphic, mergeProps({ as: "div" }, () => context.dataset(), commonAttributes, others)); }, get children() { return createComponent(Popper.Positioner, { get children() { return createComponent(DismissableLayer, mergeProps({ get disableOutsidePointerEvents() { return memo(() => !!isRootModalContent())() && context.isOpen(); }, get excludedElements() { return [context.triggerRef]; }, bypassTopMostLayerCheck: true, get style() { return combineStyle({ "--kb-menu-content-transform-origin": "var(--kb-popper-content-transform-origin)", position: "relative" }, local.style); }, onEscapeKeyDown, onFocusOutside, get onDismiss() { return context.close; } }, () => context.dataset(), commonAttributes, others)); } }); } }); } }); } // src/menu/menu-content.tsx function MenuContent(props) { let ref; const rootContext = useMenuRootContext(); const context = useMenuContext(); const [local, others] = splitProps(props, ["ref"]); createPreventScroll({ element: () => ref ?? null, enabled: () => context.contentPresent() && rootContext.preventScroll() }); return createComponent(MenuContentBase, mergeProps({ ref(r$) { const _ref$ = mergeRefs((el) => { ref = el; }, local.ref); typeof _ref$ === "function" && _ref$(r$); } }, others)); } var MenuGroupContext = createContext(); function useMenuGroupContext() { const context = useContext(MenuGroupContext); if (context === void 0) { throw new Error("[kobalte]: `useMenuGroupContext` must be used within a `Menu.Group` component"); } return context; } // src/menu/menu-group.tsx function MenuGroup(props) { const rootContext = useMenuRootContext(); const mergedProps = mergeDefaultProps({ id: rootContext.generateId(`group-${createUniqueId()}`) }, props); const [labelId, setLabelId] = createSignal(); const context = { generateId: createGenerateId(() => mergedProps.id), registerLabelId: createRegisterId(setLabelId) }; return createComponent(MenuGroupContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", role: "group", get ["aria-labelledby"]() { return labelId(); } }, mergedProps)); } }); } function MenuGroupLabel(props) { const context = useMenuGroupContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("label") }, props); const [local, others] = splitProps(mergedProps, ["id"]); createEffect(() => onCleanup(context.registerLabelId(local.id))); return createComponent(Polymorphic, mergeProps({ as: "span", get id() { return local.id; }, "aria-hidden": "true" }, others)); } function MenuIcon(props) { const context = useMenuContext(); const mergedProps = mergeDefaultProps({ children: "\u25BC" }, props); return createComponent(Polymorphic, mergeProps({ as: "span", "aria-hidden": "true" }, () => context.dataset(), mergedProps)); } function MenuItem(props) { return createComponent(MenuItemBase, mergeProps({ role: "menuitem", closeOnSelect: true }, props)); } function MenuItemDescription(props) { const context = useMenuItemContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("description") }, props); const [local, others] = splitProps(mergedProps, ["id"]); createEffect(() => onCleanup(context.registerDescription(local.id))); return createComponent(Polymorphic, mergeProps({ as: "div", get id() { return local.id; } }, () => context.dataset(), others)); } function MenuItemIndicator(props) { const context = useMenuItemContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("indicator") }, props); const [local, others] = splitProps(mergedProps, ["forceMount"]); return createComponent(Show, { get when() { return local.forceMount || context.isChecked(); }, get children() { return createComponent(Polymorphic, mergeProps({ as: "div" }, () => context.dataset(), others)); } }); } function MenuItemLabel(props) { const context = useMenuItemContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("label") }, props); const [local, others] = splitProps(mergedProps, ["ref", "id"]); createEffect(() => onCleanup(context.registerLabel(local.id))); return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs(context.setLabelRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get id() { return local.id; } }, () => context.dataset(), others)); } function MenuPortal(props) { const context = useMenuContext(); return createComponent(Show, { get when() { return context.contentPresent(); }, get children() { return createComponent(Portal, props); } }); } var MenuRadioGroupContext = createContext(); function useMenuRadioGroupContext() { const context = useContext(MenuRadioGroupContext); if (context === void 0) { throw new Error("[kobalte]: `useMenuRadioGroupContext` must be used within a `Menu.RadioGroup` component"); } return context; } // src/menu/menu-radio-group.tsx function MenuRadioGroup(props) { const rootContext = useMenuRootContext(); const defaultId = rootContext.generateId(`radiogroup-${createUniqueId()}`); const mergedProps = mergeDefaultProps({ id: defaultId }, props); const [local, others] = splitProps(mergedProps, ["value", "defaultValue", "onChange", "disabled"]); const [selected, setSelected] = createControllableSignal({ value: () => local.value, defaultValue: () => local.defaultValue, onChange: (value) => local.onChange?.(value) }); const context = { isDisabled: () => local.disabled, isSelectedValue: (value) => value === selected(), setSelectedValue: (value) => setSelected(value) }; return createComponent(MenuRadioGroupContext.Provider, { value: context, get children() { return createComponent(MenuGroup, others); } }); } function MenuRadioItem(props) { const context = useMenuRadioGroupContext(); const mergedProps = mergeDefaultProps({ closeOnSelect: false }, props); const [local, others] = splitProps(mergedProps, ["value", "onSelect"]); const onSelect = () => { local.onSelect?.(); context.setSelectedValue(local.value); }; return createComponent(MenuItemBase, mergeProps({ role: "menuitemradio", get checked() { return context.isSelectedValue(local.value); }, onSelect }, others)); } function getPointerGraceArea(placement, event, contentEl) { const basePlacement = placement.split("-")[0]; const contentRect = contentEl.getBoundingClientRect(); const polygon = []; const pointerX = event.clientX; const pointerY = event.clientY; switch (basePlacement) { case "top": polygon.push([pointerX, pointerY + 5]); polygon.push([contentRect.left, contentRect.bottom]); polygon.push([contentRect.left, contentRect.top]); polygon.push([contentRect.right, contentRect.top]); polygon.push([contentRect.right, contentRect.bottom]); break; case "right": polygon.push([pointerX - 5, pointerY]); polygon.push([contentRect.left, contentRect.top]); polygon.push([contentRect.right, contentRect.top]); polygon.push([contentRect.right, contentRect.bottom]); polygon.push([contentRect.left, contentRect.bottom]); break; case "bottom": polygon.push([pointerX, pointerY - 5]); polygon.push([contentRect.right, contentRect.top]); polygon.push([contentRect.right, contentRect.bottom]); polygon.push([contentRect.left, contentRect.bottom]); polygon.push([contentRect.left, contentRect.top]); break; case "left": polygon.push([pointerX + 5, pointerY]); polygon.push([contentRect.right, contentRect.bottom]); polygon.push([contentRect.left, contentRect.bottom]); polygon.push([contentRect.left, contentRect.top]); polygon.push([contentRect.right, contentRect.top]); break; } return polygon; } function isPointerInGraceArea(event, area) { if (!area) { return false; } return isPointInPolygon([event.clientX, event.clientY], area); } // src/menu/menu.tsx function Menu(props) { const rootContext = useMenuRootContext(); const parentDomCollectionContext = useOptionalDomCollectionContext(); const parentMenuContext = useOptionalMenuContext(); const optionalMenubarContext = useOptionalMenubarContext(); const optionalNavigationMenuContext = useOptionalNavigationMenuContext(); const mergedProps = mergeDefaultProps({ placement: rootContext.orientation() === "horizontal" ? "bottom-start" : "right-start" }, props); const [local, others] = splitProps(mergedProps, ["open", "defaultOpen", "onOpenChange"]); let pointerGraceTimeoutId = 0; let pointerGraceIntent = null; let pointerDir = "right"; const [triggerId, setTriggerId] = createSignal(); const [contentId, setContentId] = createSignal(); const [triggerRef, setTriggerRef] = createSignal(); const [contentRef, setContentRef] = createSignal(); const [focusStrategy, setFocusStrategy] = createSignal(true); const [currentPlacement, setCurrentPlacement] = createSignal(others.placement); const [nestedMenus, setNestedMenus] = createSignal([]); const [items, setItems] = createSignal([]); const { DomCollectionProvider } = createDomCollection({ items, onItemsChange: setItems }); const disclosureState = createDisclosureState({ open: () => local.open, defaultOpen: () => local.defaultOpen, onOpenChange: (isOpen) => local.onOpenChange?.(isOpen) }); const { present: contentPresent } = createPresence({ show: () => rootContext.forceMount() || disclosureState.isOpen(), element: () => contentRef() ?? null }); const listState = createListState({ selectionMode: "none", dataSource: items }); const open = (focusStrategy2) => { setFocusStrategy(focusStrategy2); disclosureState.open(); }; const close = (recursively = false) => { disclosureState.close(); if (recursively && parentMenuContext) { parentMenuContext.close(true); } }; const toggle = (focusStrategy2) => { setFocusStrategy(focusStrategy2); disclosureState.toggle(); }; const _focusContent = () => { const content = contentRef(); if (content) { focusWithoutScrolling(content); listState.selectionManager().setFocused(true); listState.selectionManager().setFocusedKey(void 0); } }; const focusContent = () => { if (optionalNavigationMenuContext != null) setTimeout(() => _focusContent()); else _focusContent(); }; const registerNestedMenu = (element) => { setNestedMenus((prev) => [...prev, element]); const parentUnregister = parentMenuContext?.registerNestedMenu(element); return () => { setNestedMenus((prev) => removeItemFromArray(prev, element)); parentUnregister?.(); }; }; const isPointerMovingToSubmenu = (e) => { const isMovingTowards = pointerDir === pointerGraceIntent?.side; return isMovingTowards && isPointerInGraceArea(e, pointerGraceIntent?.area); }; const onItemEnter = (e) => { if (isPointerMovingToSubmenu(e)) { e.preventDefault(); } }; const onItemLeave = (e) => { if (isPointerMovingToSubmenu(e)) { return; } focusContent(); }; const onTriggerLeave = (e) => { if (isPointerMovingToSubmenu(e)) { e.preventDefault(); } }; createHideOutside({ isDisabled: () => { return !(parentMenuContext == null && disclosureState.isOpen() && rootContext.isModal()); }, targets: () => [contentRef(), ...nestedMenus()].filter(Boolean) }); createEffect(() => { const contentEl = contentRef(); if (!contentEl || !parentMenuContext) { return; } const parentUnregister = parentMenuContext.registerNestedMenu(contentEl); onCleanup(() => { parentUnregister(); }); }); createEffect(() => { if (parentMenuContext !== void 0) return; optionalMenubarContext?.registerMenu(rootContext.value(), [contentRef(), ...nestedMenus()]); }); createEffect(() => { if (parentMenuContext !== void 0 || optionalMenubarContext === void 0) return; if (optionalMenubarContext.value() === rootContext.value()) { triggerRef()?.focus(); if (optionalMenubarContext.autoFocusMenu()) open(true); } else close(); }); createEffect(() => { if (parentMenuContext !== void 0 || optionalMenubarContext === void 0) return; if (disclosureState.isOpen()) optionalMenubarContext.setValue(rootContext.value()); }); onCleanup(() => { if (parentMenuContext !== void 0) return; optionalMenubarContext?.unregisterMenu(rootContext.value()); }); const dataset = createMemo(() => ({ "data-expanded": disclosureState.isOpen() ? "" : void 0, "data-closed": !disclosureState.isOpen() ? "" : void 0 })); const context = { dataset, isOpen: disclosureState.isOpen, contentPresent, nestedMenus, currentPlacement, pointerGraceTimeoutId: () => pointerGraceTimeoutId, autoFocus: focusStrategy, listState: () => listState, parentMenuContext: () => parentMenuContext, triggerRef, contentRef, triggerId, contentId, setTriggerRef, setContentRef, open, close, toggle, focusContent, onItemEnter, onItemLeave, onTriggerLeave, setPointerDir: (dir) => pointerDir = dir, setPointerGraceTimeoutId: (id) => pointerGraceTimeoutId = id, setPointerGraceIntent: (intent) => pointerGraceIntent = intent, registerNestedMenu, registerItemToParentDomCollection: parentDomCollectionContext?.registerItem, registerTriggerId: createRegisterId(setTriggerId), registerContentId: createRegisterId(setContentId) }; return createComponent(DomCollectionProvider, { get children() { return createComponent(MenuContext.Provider, { value: context, get children() { return createComponent(Show, { when: optionalNavigationMenuContext === void 0, get fallback() { return others.children; }, get children() { return createComponent(Popper, mergeProps({ anchorRef: triggerRef, contentRef, onCurrentPlacementChange: setCurrentPlacement }, others)); } }); } }); } }); } // src/menu/menu-sub.tsx function MenuSub(props) { const { direction } = useLocale(); return createComponent(Menu, mergeProps({ get placement() { return direction() === "rtl" ? "left-start" : "right-start"; }, flip: true }, props)); } var SUB_CLOSE_KEYS = { close: (dir, orientation) => { if (dir === "ltr") { return [orientation === "horizontal" ? "ArrowLeft" : "ArrowUp"]; } return [orientation === "horizontal" ? "ArrowRight" : "ArrowDown"]; } }; function MenuSubContent(props) { const context = useMenuContext(); const rootContext = useMenuRootContext(); const [local, others] = splitProps(props, ["onFocusOutside", "onKeyDown"]); const { direction } = useLocale(); const onOpenAutoFocus = (e) => { e.preventDefault(); }; const onCloseAutoFocus = (e) => { e.preventDefault(); }; const onFocusOutside = (e) => { local.onFocusOutside?.(e); const target = e.target; if (!contains(context.triggerRef(), target)) { context.close(); } }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); const isKeyDownInside = contains(e.currentTarget, e.target); const isCloseKey = SUB_CLOSE_KEYS.close(direction(), rootContext.orientation()).includes(e.key); const isSubMenu = context.parentMenuContext() != null; if (isKeyDownInside && isCloseKey && isSubMenu) { context.close(); focusWithoutScrolling(context.triggerRef()); } }; return createComponent(MenuContentBase, mergeProps({ onOpenAutoFocus, onCloseAutoFocus, onFocusOutside, onKeyDown }, others)); } var SELECTION_KEYS = ["Enter", " "]; var SUB_OPEN_KEYS = { open: (dir, orientation) => { if (dir === "ltr") { return [...SELECTION_KEYS, orientation === "horizontal" ? "ArrowRight" : "ArrowDown"]; } return [...SELECTION_KEYS, orientation === "horizontal" ? "ArrowLeft" : "ArrowUp"]; } }; function MenuSubTrigger(props) { let ref; const rootContext = useMenuRootContext(); const context = useMenuContext(); const mergedProps = mergeDefaultProps({ id: rootContext.generateId(`sub-trigger-${createUniqueId()}`) }, props); const [local, others] = splitProps(mergedProps, ["ref", "id", "textValue", "disabled", "onPointerMove", "onPointerLeave", "onPointerDown", "onPointerUp", "onClick", "onKeyDown", "onMouseDown", "onFocus"]); let openTimeoutId = null; const clearOpenTimeout = () => { if (isServer) { return; } if (openTimeoutId) { window.clearTimeout(openTimeoutId); } openTimeoutId = null; }; const { direction } = useLocale(); const key = () => local.id; const parentSelectionManager = () => { const parentMenuContext = context.parentMenuContext(); if (parentMenuContext == null) { throw new Error("[kobalte]: `Menu.SubTrigger` must be used within a `Menu.Sub` component"); } return parentMenuContext.listState().selectionManager(); }; const collection = () => context.listState().collection(); const isHighlighted = () => parentSelectionManager().focusedKey() === key(); const selectableItem = createSelectableItem({ key, selectionManager: parentSelectionManager, shouldSelectOnPressUp: true, allowsDifferentPressOrigin: true, disabled: () => local.disabled }, () => ref); const onClick = (e) => { callHandler(e, local.onClick); if (!context.isOpen() && !local.disabled) { context.open(true); } }; const onPointerMove = (e) => { callHandler(e, local.onPointerMove); if (e.pointerType !== "mouse") { return; } const parentMenuContext = context.parentMenuContext(); parentMenuContext?.onItemEnter(e); if (e.defaultPrevented) { return; } if (local.disabled) { parentMenuContext?.onItemLeave(e); return; } if (!context.isOpen() && !openTimeoutId) { context.parentMenuContext()?.setPointerGraceIntent(null); openTimeoutId = window.setTimeout(() => { context.open(false); clearOpenTimeout(); }, 100); } parentMenuContext?.onItemEnter(e); if (!e.defaultPrevented) { if (context.listState().selectionManager().isFocused()) { context.listState().selectionManager().setFocused(false); context.listState().selectionManager().setFocusedKey(void 0); } focusWithoutScrolling(e.currentTarget); parentMenuContext?.listState().selectionManager().setFocused(true); parentMenuContext?.listState().selectionManager().setFocusedKey(key()); } }; const onPointerLeave = (e) => { callHandler(e, local.onPointerLeave); if (e.pointerType !== "mouse") { return; } clearOpenTimeout(); const parentMenuContext = context.parentMenuContext(); const contentEl = context.contentRef(); if (contentEl) { parentMenuContext?.setPointerGraceIntent({ area: getPointerGraceArea(context.currentPlacement(), e, contentEl), // Safe because sub menu always open "left" or "right". side: context.currentPlacement().split("-")[0] }); window.clearTimeout(parentMenuContext?.pointerGraceTimeoutId()); const pointerGraceTimeoutId = window.setTimeout(() => { parentMenuContext?.setPointerGraceIntent(null); }, 300); parentMenuContext?.setPointerGraceTimeoutId(pointerGraceTimeoutId); } else { parentMenuContext?.onTriggerLeave(e); if (e.defaultPrevented) { return; } parentMenuContext?.setPointerGraceIntent(null); } parentMenuContext?.onItemLeave(e); }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); if (e.repeat) { return; } if (local.disabled) { return; } if (SUB_OPEN_KEYS.open(direction(), rootContext.orientation()).includes(e.key)) { e.stopPropagation(); e.preventDefault(); parentSelectionManager().setFocused(false); parentSelectionManager().setFocusedKey(void 0); if (!context.isOpen()) { context.open("first"); } context.focusContent(); context.listState().selectionManager().setFocused(true); context.listState().selectionManager().setFocusedKey(collection().getFirstKey()); } }; createEffect(() => { if (context.registerItemToParentDomCollection == null) { throw new Error("[kobalte]: `Menu.SubTrigger` must be used within a `Menu.Sub` component"); } const unregister = context.registerItemToParentDomCollection({ ref: () => ref, type: "item", key: key(), textValue: local.textValue ?? ref?.textContent ?? "", disabled: local.disabled ?? false }); onCleanup(unregister); }); createEffect(on(() => context.parentMenuContext()?.pointerGraceTimeoutId(), (pointerGraceTimer) => { onCleanup(() => { window.clearTimeout(pointerGraceTimer); context.parentMenuContext()?.setPointerGraceIntent(null); }); })); createEffect(() => onCleanup(context.registerTriggerId(local.id))); onCleanup(() => { clearOpenTimeout(); }); return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs((el) => { context.setTriggerRef(el); ref = el; }, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get id() { return local.id; }, role: "menuitem", get tabIndex() { return selectableItem.tabIndex(); }, "aria-haspopup": "true", get ["aria-expanded"]() { return context.isOpen(); }, get ["aria-controls"]() { return memo(() => !!context.isOpen())() ? context.contentId() : void 0; }, get ["aria-disabled"]() { return local.disabled; }, get ["data-key"]() { return selectableItem.dataKey(); }, get ["data-highlighted"]() { return isHighlighted() ? "" : void 0; }, get ["data-disabled"]() { return local.disabled ? "" : void 0; }, get onPointerDown() { return composeEventHandlers([local.onPointerDown, selectableItem.onPointerDown]); }, get onPointerUp() { return composeEventHandlers([local.onPointerUp, selectableItem.onPointerUp]); }, get onClick() { return composeEventHandlers([onClick, selectableItem.onClick]); }, get onKeyDown() { return composeEventHandlers([onKeyDown, selectableItem.onKeyDown]); }, get onMouseDown() { return composeEventHandlers([local.onMouseDown, selectableItem.onMouseDown]); }, get onFocus() { return composeEventHandlers([local.onFocus, selectableItem.onFocus]); }, onPointerMove, onPointerLeave }, () => context.dataset(), others)); } function MenuRoot(props) { const optionalMenubarContext = useOptionalMenubarContext(); const defaultId = `menu-${createUniqueId()}`; const mergedProps = mergeDefaultProps({ id: defaultId, modal: true }, props); const [local, others] = splitProps(mergedProps, ["id", "modal", "preventScroll", "forceMount", "open", "defaultOpen", "onOpenChange", "value", "orientation"]); const disclosureState = createDisclosureState({ open: () => local.open, defaultOpen: () => local.defaultOpen, onOpenChange: (isOpen) => local.onOpenChange?.(isOpen) }); const context = { isModal: () => local.modal ?? true, preventScroll: () => local.preventScroll ?? context.isModal(), forceMount: () => local.forceMount ?? false, generateId: createGenerateId(() => local.id), value: () => local.value, orientation: () => local.orientation ?? optionalMenubarContext?.orientation() ?? "horizontal" }; return createComponent(MenuRootContext.Provider, { value: context, get children() { return createComponent(Menu, mergeProps({ get open() { return disclosureState.isOpen(); }, get onOpenChange() { return disclosureState.setIsOpen; } }, others)); } }); } export { MenuCheckboxItem, MenuContent, MenuGroup, MenuGroupLabel, MenuIcon, MenuItem, MenuItemDescription, MenuItemIndicator, MenuItemLabel, MenuPortal, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, MenubarContext, NavigationMenuContext, useMenuContext, useMenuRootContext, useMenubarContext, useNavigationMenuContext, useOptionalMenuContext };