@yamada-ui/react
Version:
React UI components of the Yamada, by the Yamada, for the Yamada built with React and Emotion
469 lines (465 loc) • 14.3 kB
JavaScript
"use client";
import { createContext as createContext$1 } from "../../utils/context.js";
import { runKeyAction } from "../../utils/dom.js";
import { useUpdateEffect } from "../../utils/effect.js";
import { assignRef, mergeRefs } from "../../utils/ref.js";
import { utils_exports } from "../../utils/index.js";
import { useControllableState } from "../../hooks/use-controllable-state/index.js";
import { createDescendants } from "../../hooks/use-descendants/index.js";
import { useDisclosure } from "../../hooks/use-disclosure/use-disclosure.js";
import { useCallback, useId, useRef } from "react";
//#region src/components/menu/use-menu.ts
const { DescendantsContext: MenuDescendantsContext, useDescendant: useMenuDescendant, useDescendantRegister: useMenuDescendantRegister, useDescendants: useMenuDescendants } = createDescendants();
const [MenuContext, useMenuContext] = createContext$1({ name: "MenuContext" });
const [MenuGroupContext, useMenuGroupContext] = createContext$1({ name: "MenuGroupContext" });
const [MainMenuContext, useMainMenuContext] = createContext$1({
name: "MainMenuContext",
strict: false
});
const [MenuOptionGroupContext, useMenuOptionGroupContext] = createContext$1({ name: "MenuOptionGroupContext" });
const useMenu = ({ closeOnSelect, defaultOpen, disabled = false, open: openProp, subMenuDirection = "end", onClose: onCloseProp, onOpen: onOpenProp, onSelect: onSelectProp } = {}) => {
const triggerId = useId();
const contentId = useId();
const descendants = useMenuDescendants();
const updateRef = useRef(utils_exports.noop);
const onCloseRef = useRef(utils_exports.noop);
const contentRef = useRef(null);
const { open, onClose, onOpen } = useDisclosure({
defaultOpen,
open: openProp,
onClose: onCloseProp,
onOpen: onOpenProp
});
const onCloseSubMenu = useCallback(() => onCloseRef.current(), []);
const onActiveDescendant = useCallback((descendant, options = { preventScroll: true }) => {
if (!contentRef.current || !descendant || disabled) return;
contentRef.current.setAttribute("aria-activedescendant", descendant.id);
descendants.active(descendant, options);
}, [descendants, disabled]);
const { mainCloseOnSelect, subMenu, getSubMenuProps, onMainSelect } = useSubMenu({
descendants,
disabled,
open,
subMenuDirection,
onActiveDescendant,
onClose,
onOpen
});
closeOnSelect ??= mainCloseOnSelect ?? true;
const onSelect = useCallback((value, closeOnSelectProp = closeOnSelect) => {
if (disabled) return;
onSelectProp?.(value);
onMainSelect?.(value, closeOnSelectProp);
if (!closeOnSelectProp) return;
onClose();
}, [
closeOnSelect,
disabled,
onClose,
onMainSelect,
onSelectProp
]);
const onClick = useCallback((ev) => {
if (disabled) return;
ev.preventDefault();
if (!open) onOpen();
else onClose();
}, [
disabled,
onClose,
onOpen,
open
]);
const onContextMenu = useCallback((ev) => {
if (disabled) return;
ev.preventDefault();
onOpen();
updateRef.current();
}, [disabled, onOpen]);
const onKeyDown = useCallback((ev) => {
if (disabled) return;
runKeyAction(ev, {
ArrowDown: () => {
onOpen();
setTimeout(() => {
onActiveDescendant(descendants.enabledFirstValue());
});
},
ArrowUp: () => {
onOpen();
setTimeout(() => {
onActiveDescendant(descendants.enabledLastValue());
});
},
Enter: () => {
onOpen();
setTimeout(() => {
onActiveDescendant(descendants.enabledFirstValue());
});
},
Space: () => {
onOpen();
setTimeout(() => {
onActiveDescendant(descendants.enabledFirstValue());
});
}
});
}, [
descendants,
disabled,
onActiveDescendant,
onOpen
]);
const getTriggerProps = useCallback((props = {}) => ({ ...getSubMenuProps({
id: triggerId,
"aria-controls": open ? contentId : void 0,
"aria-disabled": (0, utils_exports.ariaAttr)(disabled),
"aria-expanded": open,
"aria-haspopup": "menu",
"data-trigger": (0, utils_exports.dataAttr)(true),
role: "button",
tabIndex: disabled ? -1 : 0,
...props,
onClick: (0, utils_exports.handlerAll)(props.onClick, onClick),
onKeyDown: (0, utils_exports.handlerAll)(props.onKeyDown, onKeyDown)
}) }), [
contentId,
disabled,
getSubMenuProps,
onClick,
onKeyDown,
open,
triggerId
]);
const getContextTriggerProps = useCallback((props = {}) => ({
id: triggerId,
"aria-controls": open ? contentId : void 0,
"aria-disabled": (0, utils_exports.ariaAttr)(disabled),
"aria-expanded": open,
"aria-haspopup": "menu",
"data-trigger": (0, utils_exports.dataAttr)(true),
role: "application",
...props,
onContextMenu: (0, utils_exports.handlerAll)(props.onContextMenu, onContextMenu)
}), [
contentId,
disabled,
onContextMenu,
open,
triggerId
]);
const getContentProps = useCallback(({ ref, "aria-labelledby": ariaLabelledby,...props } = {}) => ({
id: contentId,
"aria-labelledby": (0, utils_exports.cx)(ariaLabelledby, triggerId),
role: "menu",
...props,
ref: mergeRefs(ref, contentRef)
}), [contentId, triggerId]);
const getSeparatorProps = useCallback((props) => ({
role: "separator",
...props
}), []);
return {
closeOnSelect,
descendants,
open,
subMenu,
subMenuDirection,
updateRef,
getContentProps,
getContextTriggerProps,
getSeparatorProps,
getTriggerProps,
onActiveDescendant,
onClose,
onCloseRef,
onCloseSubMenu,
onOpen,
onSelect
};
};
const useSubMenu = ({ descendants, disabled = false, open, subMenuDirection = "end", onActiveDescendant, onClose, onOpen }) => {
const uuid = useId();
const { closeOnSelect: mainCloseOnSelect, descendants: mainDescendants, onActiveDescendant: onActiveMainDescendant, onCloseRef, onSelect: onMainSelect } = useMainMenuContext() ?? {};
const subMenu = !!mainDescendants && !!onActiveMainDescendant;
const createRegister = useMenuDescendantRegister(mainDescendants);
const triggerRef = useRef(null);
const dataDisabled = useCallback((node) => {
node ??= triggerRef.current;
if (!node) return false;
return (0, utils_exports.isTruthyDataAttr)(node.getAttribute("data-disabled"));
}, []);
const ariaDisabled = useCallback((node) => {
node ??= triggerRef.current;
if (!node) return false;
return (0, utils_exports.isTruthyDataAttr)(node.getAttribute("aria-disabled"));
}, []);
const onClick = useCallback((ev) => {
if (!subMenu) return;
ev.defaultPrevented = disabled || dataDisabled() || ariaDisabled();
}, [
ariaDisabled,
dataDisabled,
disabled,
subMenu
]);
const onMouseEnter = useCallback(() => {
if (!subMenu || disabled || dataDisabled() || ariaDisabled()) return;
onOpen();
}, [
ariaDisabled,
dataDisabled,
disabled,
onOpen,
subMenu
]);
const onMouseMove = useCallback((ev) => {
if (!subMenu || disabled || dataDisabled() || ariaDisabled()) return;
onActiveMainDescendant(descendants.value(triggerRef.current));
ev.defaultPrevented = true;
}, [
ariaDisabled,
dataDisabled,
descendants,
disabled,
onActiveMainDescendant,
subMenu
]);
const onKeyDown = useCallback((ev) => {
if (!subMenu || disabled) return;
const currentDescendant = open ? descendants : mainDescendants;
const onActiveCurrentDescendant = open ? onActiveDescendant : onActiveMainDescendant;
runKeyAction(ev, {
ArrowDown: () => {
onActiveCurrentDescendant(currentDescendant.enabledNextValue(triggerRef.current));
ev.defaultPrevented = true;
},
ArrowUp: () => {
onActiveCurrentDescendant(currentDescendant.enabledPrevValue(triggerRef.current));
ev.defaultPrevented = true;
},
End: () => {
onActiveCurrentDescendant(currentDescendant.enabledLastValue());
ev.defaultPrevented = true;
},
Home: () => {
onActiveCurrentDescendant(currentDescendant.enabledFirstValue());
ev.defaultPrevented = true;
},
[subMenuDirection === "end" ? "ArrowRight" : "ArrowLeft"]: () => {
onOpen();
setTimeout(() => {
onActiveDescendant(descendants.enabledFirstValue());
});
ev.defaultPrevented = true;
}
});
}, [
subMenu,
disabled,
open,
descendants,
mainDescendants,
onActiveDescendant,
onActiveMainDescendant,
subMenuDirection,
onOpen
]);
assignRef(onCloseRef, onClose);
return {
mainCloseOnSelect,
subMenu,
getSubMenuProps: useCallback(({ id = uuid, ref,...props } = {}) => {
const getDisabled = (node) => disabled || dataDisabled(node) || ariaDisabled(node);
const register = createRegister({
id,
disabled: getDisabled
});
return {
role: subMenu ? "menuitem" : "button",
...props,
ref: mergeRefs(ref, triggerRef, register),
onClick: (0, utils_exports.handlerAll)(onClick, props.onClick),
onKeyDown: (0, utils_exports.handlerAll)(onKeyDown, props.onKeyDown),
onMouseEnter: (0, utils_exports.handlerAll)(onMouseEnter, props.onMouseEnter),
onMouseMove: (0, utils_exports.handlerAll)(onMouseMove, props.onMouseMove)
};
}, [
uuid,
subMenu,
createRegister,
onClick,
onKeyDown,
onMouseEnter,
onMouseMove,
disabled,
dataDisabled,
ariaDisabled
]),
onMainSelect
};
};
const useMenuGroup = ({ "aria-labelledby": ariaLabelledbyProp,...rest }) => {
const labelId = useId();
return {
getGroupProps: useCallback(({ "aria-labelledby": ariaLabelledby,...props } = {}) => ({
"aria-labelledby": (0, utils_exports.cx)(ariaLabelledbyProp, ariaLabelledby, labelId),
role: "group",
...rest,
...props
}), [
ariaLabelledbyProp,
labelId,
rest
]),
getLabelProps: useCallback((props) => ({
id: labelId,
role: "presentation",
...props
}), [labelId])
};
};
const useMenuItem = ({ id, "aria-disabled": ariaDisabled, "data-disabled": dataDisabled, "data-trigger": dataTrigger, closeOnSelect, disabled = false, value,...rest }) => {
const trigger = (0, utils_exports.isTruthyDataAttr)(dataTrigger);
const { subMenu, subMenuDirection, onActiveDescendant, onClose, onCloseSubMenu, onSelect } = useMenuContext();
const uuid = useId();
const itemRef = useRef(null);
const subMenuTrigger = subMenu && trigger;
id ??= uuid;
const { descendants, register } = useMenuDescendant({
id,
disabled: disabled || subMenuTrigger
});
const onActive = useCallback(() => {
if (disabled) return;
onActiveDescendant(descendants.value(itemRef.current));
}, [
descendants,
disabled,
onActiveDescendant
]);
const onKeyDown = useCallback((ev) => {
runKeyAction(ev, {
ArrowDown: () => {
onActiveDescendant(descendants.enabledNextValue(itemRef.current));
},
ArrowUp: () => {
onActiveDescendant(descendants.enabledPrevValue(itemRef.current));
},
End: () => {
onActiveDescendant(descendants.enabledLastValue());
},
Enter: () => onSelect(value, closeOnSelect),
Home: () => {
onActiveDescendant(descendants.enabledFirstValue());
},
Space: () => onSelect(value, closeOnSelect),
[subMenuDirection === "end" ? "ArrowLeft" : "ArrowRight"]: () => {
if (!subMenu) return;
onClose();
descendants.firstValue()?.node.focus();
}
});
}, [
closeOnSelect,
descendants,
onActiveDescendant,
onClose,
onSelect,
subMenu,
subMenuDirection,
value
]);
return {
subMenuTrigger,
getItemProps: useCallback(({ ref,...props } = {}) => ({
id,
"aria-disabled": ariaDisabled ?? (0, utils_exports.ariaAttr)(disabled),
"data-disabled": dataDisabled ?? (0, utils_exports.dataAttr)(disabled),
role: "menuitem",
tabIndex: -1,
...rest,
...props,
ref: mergeRefs(ref, rest.ref, itemRef, register),
onClick: (0, utils_exports.handlerAll)(props.onClick, rest.onClick, () => onSelect(value, closeOnSelect)),
onFocus: (0, utils_exports.handlerAll)(props.onFocus, rest.onFocus, onActive),
onKeyDown: (0, utils_exports.handlerAll)(props.onKeyDown, rest.onKeyDown, onKeyDown),
onMouseMove: (0, utils_exports.handlerAll)(props.onMouseMove, rest.onMouseMove, () => {
onCloseSubMenu();
onActive();
})
}), [
id,
ariaDisabled,
disabled,
dataDisabled,
rest,
register,
onActive,
onKeyDown,
onSelect,
value,
closeOnSelect,
onCloseSubMenu
])
};
};
const useMenuOptionGroup = ({ type = "checkbox", defaultValue = type === "checkbox" ? [] : "", value: valueProp, onChange: onChangeProp }) => {
const [value, setValue] = useControllableState({
defaultValue,
value: valueProp,
onChange: onChangeProp
});
const radio = type === "radio";
const onChange = useCallback((selectedValue) => {
setValue((prev) => {
if (radio && (0, utils_exports.isString)(prev)) return selectedValue;
else if (!radio && (0, utils_exports.isArray)(prev)) return prev.includes(selectedValue) ? prev.filter((value$1) => value$1 !== selectedValue) : prev.concat(selectedValue);
else return prev;
});
}, [radio, setValue]);
useUpdateEffect(() => {
setValue(valueProp);
}, [valueProp]);
return {
type,
value,
onChange
};
};
const useMenuOptionItem = ({ disabled, value,...rest }) => {
const { type, value: selectedValue, onChange } = useMenuOptionGroupContext();
const { getItemProps } = useMenuItem({
disabled,
value,
...rest
});
const radio = type === "radio" && (0, utils_exports.isString)(selectedValue);
const checkbox = type === "checkbox" && (0, utils_exports.isArray)(selectedValue);
const selected = radio ? value === selectedValue : checkbox ? selectedValue.includes(value) : false;
return {
type,
selected,
getIndicatorProps: useCallback(({ style,...props } = {}) => ({
style: {
opacity: selected ? 1 : 0,
...style
},
...props
}), [selected]),
getOptionItemProps: useCallback((props = {}) => getItemProps({
role: radio ? "menuitemradio" : "menuitemcheckbox",
...props,
onClick: (0, utils_exports.handlerAll)(props.onClick, () => !disabled ? onChange?.(value) : utils_exports.noop)
}), [
disabled,
getItemProps,
onChange,
radio,
value
])
};
};
//#endregion
export { MainMenuContext, MenuContext, MenuDescendantsContext, MenuGroupContext, MenuOptionGroupContext, useMainMenuContext, useMenu, useMenuContext, useMenuDescendant, useMenuDescendants, useMenuGroup, useMenuGroupContext, useMenuItem, useMenuOptionGroup, useMenuOptionGroupContext, useMenuOptionItem, useSubMenu };
//# sourceMappingURL=use-menu.js.map