import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system"; import type {PopoverProps} from "@nextui-org/popover"; import type {MenuTriggerType} from "@react-types/menu"; import type {Ref} from "react"; import {useMenuTriggerState} from "@react-stately/menu"; import {useMenuTrigger} from "@react-aria/menu"; import {dropdown} from "@nextui-org/theme"; import {clsx} from "@nextui-org/shared-utils"; import {ReactRef, mergeRefs} from "@nextui-org/react-utils"; import {useMemo, useRef} from "react"; import {mergeProps} from "@react-aria/utils"; import {MenuProps} from "@nextui-org/menu"; interface Props extends HTMLNextUIProps<"div"> { /** * Type of overlay that is opened by the trigger. */ type?: "menu" | "listbox"; /** * Ref to the DOM node. */ ref?: ReactRef; /** * How the menu is triggered. * @default 'press' */ trigger?: MenuTriggerType; /** * Whether menu trigger is disabled. * @default false */ isDisabled?: boolean; /** * Whether the Menu closes when a selection is made. * @default true */ closeOnSelect?: boolean; } export type UseDropdownProps = Props & Omit; export function useDropdown(props: UseDropdownProps) { const { as, triggerRef: triggerRefProp, isOpen, defaultOpen, onOpenChange, type = "menu", trigger = "press", placement = "bottom", isDisabled = false, closeOnSelect = true, shouldBlockScroll = true, classNames: classNamesProp, disableAnimation = false, onClose, className, ...otherProps } = props; const Component = as || "div"; const triggerRef = useRef(null); const menuTriggerRef = triggerRefProp || triggerRef; const menuRef = useRef(null); const popoverRef = useRef(null); const state = useMenuTriggerState({ trigger, isOpen, defaultOpen, onOpenChange: (isOpen) => { onOpenChange?.(isOpen); if (!isOpen) { onClose?.(); } }, }); const {menuTriggerProps, menuProps} = useMenuTrigger( {type, trigger, isDisabled}, state, menuTriggerRef, ); const classNames = useMemo( () => dropdown({ className, }), [className], ); const onMenuAction = (menuCloseOnSelect?: boolean) => { if (menuCloseOnSelect !== undefined && !menuCloseOnSelect) { return; } if (closeOnSelect) { state.close(); } }; const getPopoverProps: PropGetter = (props = {}) => ({ state, placement, ref: popoverRef, disableAnimation, shouldBlockScroll, scrollRef: menuRef, triggerRef: menuTriggerRef, ...mergeProps(otherProps, props), classNames: { ...classNamesProp, ...props.classNames, content: clsx(classNames, classNamesProp?.content, props.className), }, }); const getMenuTriggerProps: PropGetter = ( originalProps = {}, _ref: Ref | null | undefined = null, ) => { // These props are not needed for the menu trigger since it is handled by the popover trigger. // eslint-disable-next-line @typescript-eslint/no-unused-vars const {onKeyDown, onPress, onPressStart, ...otherMenuTriggerProps} = menuTriggerProps; return { ...mergeProps(otherMenuTriggerProps, {isDisabled}, originalProps), ref: mergeRefs(_ref, triggerRef), }; }; const getMenuProps = ( props?: Partial>, _ref: Ref | null | undefined = null, ) => { return { ref: mergeRefs(_ref, menuRef), menuProps, closeOnSelect, ...mergeProps(props, { onAction: () => onMenuAction(props?.closeOnSelect), onClose: state.close, }), } as MenuProps; }; return { Component, menuRef, menuProps, classNames, closeOnSelect, onClose: state.close, autoFocus: state.focusStrategy || true, disableAnimation, getPopoverProps, getMenuProps, getMenuTriggerProps, }; } export type UseDropdownReturn = ReturnType;