diff --git a/app/(geist)/geist/dropdown-menu/_sections/dropdown-menu-default.tsx b/app/(geist)/geist/dropdown-menu/_sections/dropdown-menu-default.tsx index e5ccc35..65ce4ce 100644 --- a/app/(geist)/geist/dropdown-menu/_sections/dropdown-menu-default.tsx +++ b/app/(geist)/geist/dropdown-menu/_sections/dropdown-menu-default.tsx @@ -12,7 +12,11 @@ export default function DropdownMenuDefault() { Open - console.log(false)}>One + One + Two + One + Two + One Two diff --git a/components/ui/primitives/popper/popper-content.tsx b/components/ui/primitives/popper/popper-content.tsx index fd52dd0..88a0659 100644 --- a/components/ui/primitives/popper/popper-content.tsx +++ b/components/ui/primitives/popper/popper-content.tsx @@ -8,17 +8,66 @@ import { useOutsideClick } from '@/hooks/use-ui'; import { cn } from '@/utils/lib'; import React from 'react'; import ReactFocusLock from 'react-focus-lock'; +import { POPPER_ITEM_SELECTOR } from '../selectors'; export function PopperContent(props: PopperContentProps) { const { children, className, ...etc } = props; - const { isOpen, popperStyle, closePopper, id } = usePopper(); + const { + isOpen, + popperStyle, + closePopper, + activeTrigger, + highlightedIndex, + setHighlightedIndex, + highlight, + id, + } = usePopper(); const ref = useOutsideClick({ action: closePopper }); + function handleKeyDown(event: React.KeyboardEvent) { + if (!ref.current) return; + if (event.key === 'Escape') { + closePopper(); + } + if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + event.preventDefault(); + const direction: 'next' | 'previous' = + event.code === 'ArrowUp' ? 'previous' : 'next'; + + const menuItems = Array.from( + ref.current.querySelectorAll(POPPER_ITEM_SELECTOR), + ); + + let nextIndex; + switch (direction) { + case 'next': + nextIndex = + highlightedIndex === undefined || + highlightedIndex === menuItems.length - 1 + ? menuItems.indexOf(menuItems[menuItems.length - 1]) + : highlightedIndex + 1; + break; + default: + nextIndex = + highlightedIndex === undefined || highlightedIndex === 0 + ? 0 + : highlightedIndex - 1; + break; + } + console.log(highlightedIndex); + setHighlightedIndex(nextIndex); + highlight(menuItems[nextIndex] as HTMLElement); + } + } + return createPortal( - + activeTrigger?.focus()}> {isOpen && popperStyle && ( - + activeTrigger?.focus()} + > {children} diff --git a/components/ui/primitives/popper/popper-context.tsx b/components/ui/primitives/popper/popper-context.tsx index 8957607..c8d848e 100644 --- a/components/ui/primitives/popper/popper-context.tsx +++ b/components/ui/primitives/popper/popper-context.tsx @@ -3,7 +3,10 @@ import React, { CSSProperties, useState } from 'react'; import { PopperContextProps } from './popper.types'; import { useRestrict } from '@/hooks/use-ui'; -import { POPPER_CONTENT_SELECTOR } from '@/components/ui/primitives/selectors'; +import { + POPPER_CONTENT_SELECTOR, + POPPER_ITEM_SELECTOR, +} from '@/components/ui/primitives/selectors'; const PopperContext = React.createContext(null); @@ -17,7 +20,9 @@ export function usePopper() { export function PopperProvider({ children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = React.useState(false); const [popperStyle, setPopperStyle] = useState({}); - const [highlightedIndex, setHighlightedIndex] = React.useState(-1); + const [highlightedIndex, setHighlightedIndex] = React.useState< + number | undefined + >(undefined); const [highlightedItem, setHighlightedItem] = React.useState(null); @@ -29,8 +34,15 @@ export function PopperProvider({ children }: { children: React.ReactNode }) { function highlight(element: HTMLElement | null) { if (element) { + const rootElement = element.closest( + POPPER_CONTENT_SELECTOR, + ) as HTMLElement; + const items = Array.from( + rootElement.querySelectorAll(POPPER_ITEM_SELECTOR), + ); setHighlightedItem(element); element?.focus(); + setHighlightedIndex(items.indexOf(element)); } else { setHighlightedItem(null); (document.querySelector(POPPER_CONTENT_SELECTOR) as HTMLElement).focus(); @@ -46,17 +58,17 @@ export function PopperProvider({ children }: { children: React.ReactNode }) { setIsOpen((prevState) => !prevState); activeTrigger.current = event.currentTarget; - (document.querySelector(POPPER_CONTENT_SELECTOR) as HTMLElement)?.focus(); } function closePopper() { - setIsOpen(false); activeTrigger.current?.focus(); + setIsOpen(false); + setHighlightedItem(null); + setHighlightedIndex(undefined); } useRestrict({ condition: isOpen }); - return ( {children} diff --git a/components/ui/primitives/popper/popper-trigger.tsx b/components/ui/primitives/popper/popper-trigger.tsx index 6b1f5b9..8cc02ce 100644 --- a/components/ui/primitives/popper/popper-trigger.tsx +++ b/components/ui/primitives/popper/popper-trigger.tsx @@ -3,9 +3,10 @@ import { PopperTriggerProps } from '@/components/ui/primitives/popper/popper.types'; import { usePopper } from '@/components/ui/primitives/popper/popper-context'; import { Button } from '@/components/ui/button'; +import { chain } from '@/utils/chain'; export function PopperTrigger(props: PopperTriggerProps) { - const { children } = props; + const { children, onMouseDown, onClick, ...etc } = props; const { openPopper, id } = usePopper(); function handleMouseDown(event: React.MouseEvent) { @@ -13,7 +14,13 @@ export function PopperTrigger(props: PopperTriggerProps) { } return ( - ); diff --git a/components/ui/primitives/popper/popper.types.ts b/components/ui/primitives/popper/popper.types.ts index a69fbf5..d6e1a5c 100644 --- a/components/ui/primitives/popper/popper.types.ts +++ b/components/ui/primitives/popper/popper.types.ts @@ -5,6 +5,9 @@ export interface PopperContextProps { popperStyle: CSSProperties; id: string; highlightedItem: HTMLElement | null; + activeTrigger: HTMLElement | null; + highlightedIndex?: number; + setHighlightedIndex(value?: number): void; highlight(element: HTMLElement | null): void; openPopper(event: React.MouseEvent): void; closePopper(): void; @@ -14,7 +17,7 @@ export interface PopperProps { children: React.ReactNode; } -export interface PopperTriggerProps { +export interface PopperTriggerProps extends ComponentProps<'button'> { children: React.ReactNode; } diff --git a/components/ui/primitives/selectors.ts b/components/ui/primitives/selectors.ts index 33b2dcd..f1793eb 100644 --- a/components/ui/primitives/selectors.ts +++ b/components/ui/primitives/selectors.ts @@ -1 +1,2 @@ export const POPPER_CONTENT_SELECTOR = '[data-popper-content]'; +export const POPPER_ITEM_SELECTOR = '[data-popper-item]';