Skip to content

Commit c1ea3e2

Browse files
committed
refactor: propagate arrow-up keyboard event to menu-primitive
1 parent e7a593a commit c1ea3e2

File tree

1 file changed

+33
-2
lines changed

1 file changed

+33
-2
lines changed

packages/react/dropdown-menu/src/dropdown-menu.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type DropdownMenuContextValue = {
3333
onOpenChange(open: boolean): void;
3434
onOpenToggle(): void;
3535
modal: boolean;
36+
wasOpenedWithArrowUp: boolean;
37+
setWasOpenedWithArrowUp(value: boolean): void;
3638
};
3739

3840
const [DropdownMenuProvider, useDropdownMenuContext] =
@@ -65,6 +67,14 @@ const DropdownMenu: React.FC<DropdownMenuProps> = (props: ScopedProps<DropdownMe
6567
onChange: onOpenChange,
6668
caller: DROPDOWN_MENU_NAME,
6769
});
70+
const [wasOpenedWithArrowUp, setWasOpenedWithArrowUp] = React.useState(false);
71+
72+
// Reset arrow up state when menu closes
73+
React.useEffect(() => {
74+
if (!open) {
75+
setWasOpenedWithArrowUp(false);
76+
}
77+
}, [open]);
6878

6979
return (
7080
<DropdownMenuProvider
@@ -76,6 +86,8 @@ const DropdownMenu: React.FC<DropdownMenuProps> = (props: ScopedProps<DropdownMe
7686
onOpenChange={setOpen}
7787
onOpenToggle={React.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen])}
7888
modal={modal}
89+
wasOpenedWithArrowUp={wasOpenedWithArrowUp}
90+
setWasOpenedWithArrowUp={setWasOpenedWithArrowUp}
7991
>
8092
<MenuPrimitive.Root {...menuScope} open={open} onOpenChange={setOpen} dir={dir} modal={modal}>
8193
{children}
@@ -130,7 +142,11 @@ const DropdownMenuTrigger = React.forwardRef<DropdownMenuTriggerElement, Dropdow
130142
if (event.key === 'ArrowDown') context.onOpenChange(true);
131143
// prevent keydown from scrolling window / first focused item to execute
132144
// that keydown (inadvertently closing the menu)
133-
if (['Enter', ' ', 'ArrowDown'].includes(event.key)) event.preventDefault();
145+
if (event.key === 'ArrowUp') {
146+
context.setWasOpenedWithArrowUp(true);
147+
context.onOpenChange(true);
148+
}
149+
if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key)) event.preventDefault();
134150
})}
135151
/>
136152
</MenuPrimitive.Anchor>
@@ -167,7 +183,7 @@ const CONTENT_NAME = 'DropdownMenuContent';
167183

168184
type DropdownMenuContentElement = React.ComponentRef<typeof MenuPrimitive.Content>;
169185
type MenuContentProps = React.ComponentPropsWithoutRef<typeof MenuPrimitive.Content>;
170-
interface DropdownMenuContentProps extends Omit<MenuContentProps, 'onEntryFocus'> {}
186+
interface DropdownMenuContentProps extends MenuContentProps {}
171187

172188
const DropdownMenuContent = React.forwardRef<DropdownMenuContentElement, DropdownMenuContentProps>(
173189
(props: ScopedProps<DropdownMenuContentProps>, forwardedRef) => {
@@ -183,6 +199,21 @@ const DropdownMenuContent = React.forwardRef<DropdownMenuContentElement, Dropdow
183199
{...menuScope}
184200
{...contentProps}
185201
ref={forwardedRef}
202+
onEntryFocus={(event: Event) => {
203+
// If opened with ArrowUp, focus last item instead of first
204+
if (context.wasOpenedWithArrowUp) {
205+
event.preventDefault();
206+
// Focus the last item
207+
const target = event.target as HTMLElement;
208+
const items = target.querySelectorAll('[role="menuitem"]:not([data-disabled])');
209+
const lastItem = items[items.length - 1] as HTMLElement;
210+
if (lastItem) {
211+
setTimeout(() => lastItem.focus(), 0);
212+
}
213+
}
214+
// Call the original onEntryFocus if provided
215+
contentProps.onEntryFocus?.(event);
216+
}}
186217
onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
187218
if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus();
188219
hasInteractedOutsideRef.current = false;

0 commit comments

Comments
 (0)