Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dropdown): _a2.find is not a function #3762

Merged
merged 9 commits into from
Sep 15, 2024
6 changes: 6 additions & 0 deletions .changeset/serious-panthers-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nextui-org/dropdown": patch
"@nextui-org/use-aria-menu": patch
---

fixed `_a2.find` is not a function (#3761)
49 changes: 48 additions & 1 deletion packages/components/dropdown/__tests__/dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ describe("Keyboard interactions", () => {
logSpy.mockRestore();
});

it("should respect closeOnSelect setting of DropdownItem", async () => {
it("should respect closeOnSelect setting of DropdownItem (static)", async () => {
const onOpenChange = jest.fn();
const wrapper = render(
<Dropdown onOpenChange={onOpenChange}>
Expand Down Expand Up @@ -831,4 +831,51 @@ describe("Keyboard interactions", () => {
expect(onOpenChange).toBeCalledTimes(2);
});
});

it("should respect closeOnSelect setting of DropdownItem (dynamic)", async () => {
const onOpenChange = jest.fn();
const items = [
{
key: "new",
label: "New file",
},
{
key: "copy",
label: "Copy link",
},
];
const wrapper = render(
<Dropdown onOpenChange={onOpenChange}>
<DropdownTrigger>
<Button data-testid="trigger-test">Trigger</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Actions" items={items}>
{(item) => (
<DropdownItem key={item.key} closeOnSelect={item.key !== "new"}>
{item.label}
</DropdownItem>
)}
</DropdownMenu>
</Dropdown>,
);

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

act(() => {
triggerButton.click();
});
expect(onOpenChange).toBeCalledTimes(1);

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

await act(async () => {
await userEvent.click(menuItems[0]);
expect(onOpenChange).toBeCalledTimes(1);
});

await act(async () => {
await userEvent.click(menuItems[1]);
expect(onOpenChange).toBeCalledTimes(2);
});
});
});
47 changes: 38 additions & 9 deletions packages/components/dropdown/src/use-dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils";
import {useMemo, useRef} from "react";
import {mergeProps} from "@react-aria/utils";
import {MenuProps} from "@nextui-org/menu";
import {CollectionElement} from "@react-types/shared";

interface Props extends HTMLNextUIProps<"div"> {
/**
Expand Down Expand Up @@ -42,6 +43,40 @@ interface Props extends HTMLNextUIProps<"div"> {

export type UseDropdownProps = Props & Omit<PopoverProps, "children" | "color" | "variant">;

const getMenuItem = <T extends object>(props: Partial<MenuProps<T>> | undefined, key: string) => {
if (props) {
const mergedChildren = Array.isArray(props.children)
? props.children
: [...(props?.items || [])];

if (mergedChildren && mergedChildren.length) {
const item = ((mergedChildren as CollectionElement<T>[]).find((item) => {
if (item.key === key) {
return item;
}
}) || {}) as {props: MenuProps};

return item;
}
}

return null;
};

const getCloseOnSelect = <T extends object>(
props: Partial<MenuProps<T>> | undefined,
key: string,
item?: any,
) => {
const mergedItem = item || getMenuItem(props, key);

if (mergedItem && mergedItem.props && "closeOnSelect" in mergedItem.props) {
return mergedItem.props.closeOnSelect;
}

return props?.closeOnSelect;
};

export function useDropdown(props: UseDropdownProps) {
const globalContext = useProviderContext();

Expand Down Expand Up @@ -152,16 +187,10 @@ export function useDropdown(props: UseDropdownProps) {
menuProps,
closeOnSelect,
...mergeProps(props, {
onAction: (key: any) => {
// @ts-ignore
const item = props?.children?.find((item) => item.key === key);

if (item?.props?.closeOnSelect === false) {
onMenuAction(false);
onAction: (key: any, item?: any) => {
const closeOnSelect = getCloseOnSelect(props, key, item);

return;
}
onMenuAction(props?.closeOnSelect);
onMenuAction(closeOnSelect);
},
onClose: state.close,
}),
Expand Down
6 changes: 3 additions & 3 deletions packages/hooks/use-aria-menu/src/use-menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export interface AriaMenuItemProps
* Handler that is called when the user activates the item.
* @deprecated - pass to the menu instead.
*/
onAction?: (key: Key) => void;
onAction?: (key: Key, item: any) => void;

/**
* The native button click event handler
Expand Down Expand Up @@ -167,11 +167,11 @@ export function useMenuItem<T>(

if (props.onAction) {
// @ts-ignore
props.onAction(key);
props.onAction(key, item);
// @ts-ignore
} else if (data.onAction) {
// @ts-ignore
data.onAction(key);
data.onAction(key, item);
}

if (e.target instanceof HTMLAnchorElement) {
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/use-aria-menu/src/use-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface AriaMenuOptions<T> extends Omit<AriaMenuProps<T>, "children">,

interface MenuData {
onClose?: () => void;
onAction?: (key: Key) => void;
onAction?: (key: Key, item: any) => void;
wingkwong marked this conversation as resolved.
Show resolved Hide resolved
}

export const menuData = new WeakMap<TreeState<unknown>, MenuData>();
Expand Down