mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
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
This commit is contained in:
parent
1109baea6a
commit
7df2c71ecc
6
.changeset/heavy-kangaroos-stare.md
Normal file
6
.changeset/heavy-kangaroos-stare.md
Normal file
@ -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)
|
||||
@ -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(
|
||||
<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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user