Skip to content

Commit

Permalink
fix(dropdown): focus behaviour on press / enter keydown (#2970)
Browse files Browse the repository at this point in the history
* fix(dropdown): set focus on the first item

* feat(dropdown): add keyboard interactions tests

* feat(changeset): add changeset

* fix(dropdown): use fireEvent.keyDown instead

* chore(deps): add @nextui-org/test-utils to dropdown

* refactor(dropdown): pass onKeyDown to menu trigger and don't hardcode autoFocus

* chore(dropdown): remove autoFocus

* fix(menu): pass userMenuProps to useTreeState and useAriaMenu and remove from getListProps

* chore(changeset): add menu package
  • Loading branch information
wingkwong committed May 19, 2024
1 parent 1109bae commit 7df2c71
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/heavy-kangaroos-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nextui-org/dropdown": patch
"@nextui-org/menu": patch
---

Focus on the first item when pressing Space / Enter key on dropdown menu open (#2863)
81 changes: 80 additions & 1 deletion packages/components/dropdown/__tests__/dropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from "react";
import {act, render} from "@testing-library/react";
import {act, render, fireEvent} from "@testing-library/react";
import {Button} from "@nextui-org/button";
import userEvent from "@testing-library/user-event";
import {keyCodes} from "@nextui-org/test-utils";
import {User} from "@nextui-org/user";
import {Image} from "@nextui-org/image";
import {Avatar} from "@nextui-org/avatar";
Expand Down Expand Up @@ -538,3 +539,81 @@ describe("Dropdown", () => {
spy.mockRestore();
});
});

describe("Keyboard interactions", () => {
it("should focus on the first item on keyDown (Enter)", async () => {
const wrapper = render(
<Dropdown>
<DropdownTrigger>
<Button data-testid="trigger-test">Trigger</Button>
</DropdownTrigger>
<DropdownMenu disallowEmptySelection aria-label="Actions" selectionMode="single">
<DropdownItem key="new">New file</DropdownItem>
<DropdownItem key="copy">Copy link</DropdownItem>
<DropdownItem key="edit">Edit file</DropdownItem>
<DropdownItem key="delete" color="danger">
Delete file
</DropdownItem>
</DropdownMenu>
</Dropdown>,
);

let triggerButton = wrapper.getByTestId("trigger-test");

act(() => {
triggerButton.focus();
});

expect(triggerButton).toHaveFocus();

fireEvent.keyDown(triggerButton, {key: "Enter", charCode: keyCodes.Enter});

let menu = wrapper.queryByRole("menu");

expect(menu).toBeTruthy();

let menuItems = wrapper.getAllByRole("menuitemradio");

expect(menuItems.length).toBe(4);

expect(menuItems[0]).toHaveFocus();
});

it("should focus on the first item on keyDown (Space)", async () => {
const wrapper = render(
<Dropdown>
<DropdownTrigger>
<Button data-testid="trigger-test">Trigger</Button>
</DropdownTrigger>
<DropdownMenu disallowEmptySelection aria-label="Actions" selectionMode="single">
<DropdownItem key="new">New file</DropdownItem>
<DropdownItem key="copy">Copy link</DropdownItem>
<DropdownItem key="edit">Edit file</DropdownItem>
<DropdownItem key="delete" color="danger">
Delete file
</DropdownItem>
</DropdownMenu>
</Dropdown>,
);

let triggerButton = wrapper.getByTestId("trigger-test");

act(() => {
triggerButton.focus();
});

expect(triggerButton).toHaveFocus();

fireEvent.keyDown(triggerButton, {key: " ", charCode: keyCodes.Space});

let menu = wrapper.queryByRole("menu");

expect(menu).toBeTruthy();

let menuItems = wrapper.getAllByRole("menuitemradio");

expect(menuItems.length).toBe(4);

expect(menuItems[0]).toHaveFocus();
});
});
1 change: 1 addition & 0 deletions packages/components/dropdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@nextui-org/user": "workspace:*",
"@nextui-org/image": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/test-utils": "workspace:*",
"framer-motion": "^11.0.22",
"clean-package": "2.2.0",
"react": "^18.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/components/dropdown/src/use-dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function useDropdown(props: UseDropdownProps) {
) => {
// These props are not needed for the menu trigger since it is handled by the popover trigger.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {onKeyDown, onPress, onPressStart, ...otherMenuTriggerProps} = menuTriggerProps;
const {onPress, onPressStart, ...otherMenuTriggerProps} = menuTriggerProps;

return {
...mergeProps(otherMenuTriggerProps, {isDisabled}, originalProps),
Expand Down
6 changes: 2 additions & 4 deletions packages/components/menu/src/use-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ export function useMenu<T extends object>(props: UseMenuProps<T>) {
const domRef = useDOMRef(ref);
const shouldFilterDOMProps = typeof Component === "string";

const innerState = useTreeState({...otherProps, children});
const innerState = useTreeState({...otherProps, ...userMenuProps, children});

const state = propState || innerState;

const {menuProps} = useAriaMenu(otherProps, state, domRef);
const {menuProps} = useAriaMenu({...otherProps, ...userMenuProps}, state, domRef);

const slots = useMemo(() => menu({className}), [className]);
const baseStyles = clsx(classNames?.base, className);
Expand All @@ -144,9 +144,7 @@ export function useMenu<T extends object>(props: UseMenuProps<T>) {
return {
"data-slot": "list",
className: slots.list({class: classNames?.list}),
...userMenuProps,
...menuProps,

...props,
};
};
Expand Down
10 changes: 3 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7df2c71

Please sign in to comment.