Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gold-otters-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@radix-ui/react-dropdown-menu': minor
---

move focus to the last item when opening with ArrowUp
35 changes: 33 additions & 2 deletions packages/react/dropdown-menu/src/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type DropdownMenuContextValue = {
onOpenChange(open: boolean): void;
onOpenToggle(): void;
modal: boolean;
wasOpenedWithArrowUp: boolean;
setWasOpenedWithArrowUp(value: boolean): void;
};

const [DropdownMenuProvider, useDropdownMenuContext] =
Expand Down Expand Up @@ -65,6 +67,14 @@ const DropdownMenu: React.FC<DropdownMenuProps> = (props: ScopedProps<DropdownMe
onChange: onOpenChange,
caller: DROPDOWN_MENU_NAME,
});
const [wasOpenedWithArrowUp, setWasOpenedWithArrowUp] = React.useState(false);

// Reset arrow up state when menu closes
React.useEffect(() => {
if (!open) {
setWasOpenedWithArrowUp(false);
}
}, [open]);

return (
<DropdownMenuProvider
Expand All @@ -76,6 +86,8 @@ const DropdownMenu: React.FC<DropdownMenuProps> = (props: ScopedProps<DropdownMe
onOpenChange={setOpen}
onOpenToggle={React.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen])}
modal={modal}
wasOpenedWithArrowUp={wasOpenedWithArrowUp}
setWasOpenedWithArrowUp={setWasOpenedWithArrowUp}
>
<MenuPrimitive.Root {...menuScope} open={open} onOpenChange={setOpen} dir={dir} modal={modal}>
{children}
Expand Down Expand Up @@ -130,7 +142,11 @@ const DropdownMenuTrigger = React.forwardRef<DropdownMenuTriggerElement, Dropdow
if (event.key === 'ArrowDown') context.onOpenChange(true);
// prevent keydown from scrolling window / first focused item to execute
// that keydown (inadvertently closing the menu)
if (['Enter', ' ', 'ArrowDown'].includes(event.key)) event.preventDefault();
if (event.key === 'ArrowUp') {
context.setWasOpenedWithArrowUp(true);
context.onOpenChange(true);
}
if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key)) event.preventDefault();
})}
/>
</MenuPrimitive.Anchor>
Expand Down Expand Up @@ -167,7 +183,7 @@ const CONTENT_NAME = 'DropdownMenuContent';

type DropdownMenuContentElement = React.ComponentRef<typeof MenuPrimitive.Content>;
type MenuContentProps = React.ComponentPropsWithoutRef<typeof MenuPrimitive.Content>;
interface DropdownMenuContentProps extends Omit<MenuContentProps, 'onEntryFocus'> {}
interface DropdownMenuContentProps extends MenuContentProps {}

const DropdownMenuContent = React.forwardRef<DropdownMenuContentElement, DropdownMenuContentProps>(
(props: ScopedProps<DropdownMenuContentProps>, forwardedRef) => {
Expand All @@ -183,6 +199,21 @@ const DropdownMenuContent = React.forwardRef<DropdownMenuContentElement, Dropdow
{...menuScope}
{...contentProps}
ref={forwardedRef}
onEntryFocus={(event: Event) => {
// If opened with ArrowUp, focus last item instead of first
if (context.wasOpenedWithArrowUp) {
event.preventDefault();
// Focus the last item
const target = event.target as HTMLElement;
const items = target.querySelectorAll('[role="menuitem"]:not([data-disabled])');
const lastItem = items[items.length - 1] as HTMLElement;
if (lastItem) {
setTimeout(() => lastItem.focus(), 0);
}
}
// Call the original onEntryFocus if provided
contentProps.onEntryFocus?.(event);
}}
onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus();
hasInteractedOutsideRef.current = false;
Expand Down