import { nextTick, ref, type Ref } from 'vue' import { useEventListener } from './useEventListener' export interface UseDropdownStateOptions { closeOnOutsideClick?: boolean closeOnEscape?: boolean /** Close the panel when the page scrolls or the viewport resizes. Default `false`. */ closeOnScroll?: boolean insideRefs?: Array> /** * Element that counts as "inside" for outside-click closing, typically a * `useTemplateRef()`. Without one, bind the returned `rootRef` instead. */ rootRef?: Ref /** * Element focus returns to when the panel closes with `close(true)` (Escape). * Pass one when the trigger lives outside `rootRef`; otherwise bind the * returned `triggerRef` to the trigger element. */ triggerRef?: Ref onOpen?: () => void onClose?: () => void } export interface UseDropdownStateReturn { isOpen: Ref rootRef: Ref /** Bind to the trigger element so Escape can return focus to it. */ triggerRef: Ref open: () => void /** Closes the panel; `returnFocus` moves focus back to `triggerRef`. */ close: (returnFocus?: boolean) => void toggle: () => void handleClickOutside: (event: MouseEvent) => void handleKeydown: (event: KeyboardEvent) => void } /** * Shared disclosure state for dropdown-style SDK components: open/close state, * outside-click and Escape closing, optional scroll/resize closing, and focus * return to the trigger. `close(returnFocus)` mirrors `useMenuKeyboard`'s * contract, so the two compose directly. */ export function useDropdownState(options: UseDropdownStateOptions = {}): UseDropdownStateReturn { const isOpen = ref(false) const rootRef = options.rootRef ?? ref(null) const triggerRef = options.triggerRef ?? ref(null) const closeOnOutsideClick = options.closeOnOutsideClick ?? true const closeOnEscape = options.closeOnEscape ?? true const closeOnScroll = options.closeOnScroll ?? false function open(): void { if (isOpen.value) return isOpen.value = true options.onOpen?.() } function close(returnFocus = false): void { if (!isOpen.value) return isOpen.value = false options.onClose?.() if (returnFocus) { void nextTick(() => triggerRef.value?.focus()) } } function toggle(): void { if (isOpen.value) { close() } else { open() } } function handleClickOutside(event: MouseEvent): void { if (!closeOnOutsideClick || !isOpen.value || !rootRef.value) return const target = event.target if ( target instanceof Node && !rootRef.value.contains(target) && !options.insideRefs?.some(ref => ref.value?.contains(target)) ) { close() } } function handleKeydown(event: KeyboardEvent): void { if (closeOnEscape && event.key === 'Escape') { close(true) } } function handleViewportChange(): void { if (isOpen.value) close() } if (closeOnOutsideClick) { useEventListener(() => document, 'click', handleClickOutside) } if (closeOnEscape) { useEventListener(() => document, 'keydown', handleKeydown) } if (closeOnScroll) { useEventListener(() => window, 'scroll', handleViewportChange, true) useEventListener(() => window, 'resize', handleViewportChange) } return { isOpen, rootRef, triggerRef, open, close, toggle, handleClickOutside, handleKeydown, } }