From 7df2c71ecc5f06d60807b6b3502d3a118080a0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D5=A1=D3=84=D5=A1?= Date: Mon, 20 May 2024 03:40:28 +0800 Subject: [PATCH] fix(dropdown): focus behaviour on press / enter keydown (#2970) * 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 --- .changeset/heavy-kangaroos-stare.md | 6 ++ .../dropdown/__tests__/dropdown.test.tsx | 81 ++++++++++++++++++- packages/components/dropdown/package.json | 1 + .../components/dropdown/src/use-dropdown.ts | 2 +- packages/components/menu/src/use-menu.ts | 6 +- pnpm-lock.yaml | 10 +-- 6 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 .changeset/heavy-kangaroos-stare.md diff --git a/.changeset/heavy-kangaroos-stare.md b/.changeset/heavy-kangaroos-stare.md new file mode 100644 index 0000000000..588f188bde --- /dev/null +++ b/.changeset/heavy-kangaroos-stare.md @@ -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) diff --git a/packages/components/dropdown/__tests__/dropdown.test.tsx b/packages/components/dropdown/__tests__/dropdown.test.tsx index 3080321f5b..2dffd2555f 100644 --- a/packages/components/dropdown/__tests__/dropdown.test.tsx +++ b/packages/components/dropdown/__tests__/dropdown.test.tsx @@ -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"; @@ -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( + + + + + + New file + Copy link + Edit file + + Delete file + + + , + ); + + 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( + + + + + + New file + Copy link + Edit file + + Delete file + + + , + ); + + 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(); + }); +}); diff --git a/packages/components/dropdown/package.json b/packages/components/dropdown/package.json index d239d69019..75346755cd 100644 --- a/packages/components/dropdown/package.json +++ b/packages/components/dropdown/package.json @@ -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", diff --git a/packages/components/dropdown/src/use-dropdown.ts b/packages/components/dropdown/src/use-dropdown.ts index 8a2aa62012..775449720f 100644 --- a/packages/components/dropdown/src/use-dropdown.ts +++ b/packages/components/dropdown/src/use-dropdown.ts @@ -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), diff --git a/packages/components/menu/src/use-menu.ts b/packages/components/menu/src/use-menu.ts index 36b4a7cf9a..82d8d7afeb 100644 --- a/packages/components/menu/src/use-menu.ts +++ b/packages/components/menu/src/use-menu.ts @@ -119,11 +119,11 @@ export function useMenu(props: UseMenuProps) { 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); @@ -144,9 +144,7 @@ export function useMenu(props: UseMenuProps) { return { "data-slot": "list", className: slots.list({class: classNames?.list}), - ...userMenuProps, ...menuProps, - ...props, }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cee156200..59796cc15f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1468,6 +1468,9 @@ importers: '@nextui-org/system': specifier: workspace:* version: link:../../core/system + '@nextui-org/test-utils': + specifier: workspace:* + version: link:../../utilities/test-utils '@nextui-org/theme': specifier: workspace:* version: link:../../core/theme @@ -5967,10 +5970,6 @@ packages: peerDependencies: '@effect-ts/otel-node': '*' peerDependenciesMeta: - '@effect-ts/core': - optional: true - '@effect-ts/otel': - optional: true '@effect-ts/otel-node': optional: true dependencies: @@ -22464,9 +22463,6 @@ packages: resolution: {integrity: sha512-W+gxAq7aQ9dJIg/XLKGcRT0cvnStFAQHPaI0pvD0U2l6IVLueUAm3nwN7lkY62zZNmlvNx6jNtE4wlbS+CyqSg==} engines: {node: '>= 12.0.0'} hasBin: true - peerDependenciesMeta: - '@parcel/core': - optional: true dependencies: '@parcel/config-default': 2.12.0(@parcel/core@2.12.0)(typescript@4.9.5) '@parcel/core': 2.12.0