@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
JavaScript
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 };