import { useState, useEffect, useMemo, useRef, useCallback } from 'react' import { cn } from '@/lib/utils' import { ArrowUp, ArrowDown } from '@phosphor-icons/react' import { type SortOption, type SortDirection, getDefaultDirection, SORT_OPTIONS, getSortOptionLabel } from '../utils/noteListHelpers' interface SortItem { value: SortOption label: string } type SortMenuAction = | { type: 'close' } | { type: 'focus'; index: number } function buildSortItems(customProperties?: string[]): SortItem[] { const builtInItems = SORT_OPTIONS.map(({ value, label }) => ({ value, label })) const customItems = (customProperties ?? []).map((key) => ({ value: `property:${key}` as SortOption, label: key, })) return [...builtInItems, ...customItems] } function resolveFocusedIndex(groupLabel: string, current: SortOption, sortItems: SortItem[]) { const activeElement = document.activeElement as HTMLElement | null const activeIndex = Number(activeElement?.dataset.sortItemIndex ?? -1) if (activeElement?.dataset.sortGroupLabel === groupLabel && activeIndex >= 0) return activeIndex const currentIndex = sortItems.findIndex((item) => item.value === current) return currentIndex >= 0 ? currentIndex : 0 } function focusSortItem(sortButtonRefs: React.MutableRefObject>, index: number) { sortButtonRefs.current[index]?.focus() } function resolveSortMenuAction(key: string, focusIndex: number, itemCount: number): SortMenuAction | null { const lastIndex = itemCount - 1 switch (key) { case 'Escape': return { type: 'close' } case 'ArrowDown': return { type: 'focus', index: Math.min(lastIndex, focusIndex + 1) } case 'ArrowUp': return { type: 'focus', index: Math.max(0, focusIndex - 1) } case 'Home': return { type: 'focus', index: 0 } case 'End': return { type: 'focus', index: lastIndex } default: return null } } function selectOnKeyboard( event: React.KeyboardEvent, value: SortOption, direction: SortDirection, onSelect: (opt: SortOption, dir: SortDirection) => void, ) { if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() onSelect(value, direction) } function getDirectionButtonClass(isActive: boolean, activeDirection: SortDirection, buttonDirection: SortDirection) { return cn( 'flex items-center rounded p-0.5 hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', isActive && activeDirection === buttonDirection ? 'text-foreground' : 'text-muted-foreground opacity-40', ) } function useSortDropdownState({ groupLabel, current, sortItems, onChange, }: { groupLabel: string current: SortOption sortItems: SortItem[] onChange: (groupLabel: string, option: SortOption, direction: SortDirection) => void }) { const [open, setOpen] = useState(false) const containerRef = useRef(null) const triggerRef = useRef(null) const sortButtonRefs = useRef>([]) useEffect(() => { if (!open) return function handlePointerDown(event: MouseEvent) { if (containerRef.current?.contains(event.target as Node)) return setOpen(false) } document.addEventListener('mousedown', handlePointerDown) return () => document.removeEventListener('mousedown', handlePointerDown) }, [open]) useEffect(() => { if (!open) return focusSortItem(sortButtonRefs, resolveFocusedIndex(groupLabel, current, sortItems)) }, [current, groupLabel, open, sortItems]) const closeMenu = useCallback(() => { setOpen(false) triggerRef.current?.focus() }, []) const handleSelect = useCallback((option: SortOption, nextDirection: SortDirection) => { onChange(groupLabel, option, nextDirection) closeMenu() }, [closeMenu, groupLabel, onChange]) const handleMenuKeyDown = useCallback((event: React.KeyboardEvent) => { const action = resolveSortMenuAction( event.key, resolveFocusedIndex(groupLabel, current, sortItems), sortItems.length, ) if (!action) return event.preventDefault() if (action.type === 'close') { closeMenu() return } focusSortItem(sortButtonRefs, action.index) }, [closeMenu, current, groupLabel, sortItems]) return { open, setOpen, containerRef, triggerRef, sortButtonRefs, handleSelect, handleMenuKeyDown, } } function SortDropdownTrigger({ triggerRef, open, current, groupLabel, direction, onToggle, }: { triggerRef: React.RefObject open: boolean current: SortOption groupLabel: string direction: SortDirection onToggle: () => void }) { const DirectionIcon = direction === 'asc' ? ArrowUp : ArrowDown return ( ) } function SortDropdownMenu({ open, groupLabel, current, direction, sortItems, sortButtonRefs, onKeyDown, onSelect, }: { open: boolean groupLabel: string current: SortOption direction: SortDirection sortItems: SortItem[] sortButtonRefs: React.MutableRefObject> onKeyDown: (event: React.KeyboardEvent) => void onSelect: (option: SortOption, nextDirection: SortDirection) => void }) { if (!open) return null const hasCustom = sortItems.length > SORT_OPTIONS.length const builtInOptionCount = SORT_OPTIONS.length return (
{sortItems.map((item, index) => ( { sortButtonRefs.current[index] = node }} showSeparator={hasCustom && index === builtInOptionCount} onSelect={onSelect} /> ))}
) } export function SortDropdown({ groupLabel, current, direction, customProperties, onChange }: { groupLabel: string current: SortOption direction: SortDirection customProperties?: string[] onChange: (groupLabel: string, option: SortOption, direction: SortDirection) => void }) { const sortItems = useMemo(() => buildSortItems(customProperties), [customProperties]) const { open, setOpen, containerRef, triggerRef, sortButtonRefs, handleSelect, handleMenuKeyDown, } = useSortDropdownState({ groupLabel, current, sortItems, onChange, }) return (
setOpen((value) => !value)} />
) } function SortRow({ index, groupLabel, value, label, current, direction, buttonRef, showSeparator, onSelect }: { index: number groupLabel: string value: SortOption label: string current: SortOption direction: SortDirection buttonRef: (node: HTMLButtonElement | null) => void showSeparator: boolean onSelect: (opt: SortOption, dir: SortDirection) => void }) { const isActive = value === current const defaultDirection = isActive ? direction : getDefaultDirection(value) const itemData = { 'data-sort-group-label': groupLabel, 'data-sort-item-index': String(index), } return ( <> {showSeparator &&
}
} itemData={itemData} /> } itemData={itemData} />
) } function SortDirectionButton({ value, direction, activeDirection, isActive, onSelect, icon, itemData, }: { value: SortOption direction: SortDirection activeDirection: SortDirection isActive: boolean onSelect: (opt: SortOption, dir: SortDirection) => void icon: React.ReactNode itemData: Record }) { return ( ) }