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: menu item classNames not work #4156

Merged
merged 7 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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/brave-trains-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/menu": patch
jrgarciadev marked this conversation as resolved.
Show resolved Hide resolved
---

Fix menu item classNames not work (#4119)
2 changes: 1 addition & 1 deletion apps/docs/content/docs/components/dropdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ you to customize each item individually.
| isReadOnly | `boolean` | Whether the dropdown item press events should be ignored. | `false` |
| hideSelectedIcon | `boolean` | Whether to hide the check icon when the item is selected. | `false` |
| closeOnSelect | `boolean` | Whether the dropdown menu should be closed when the item is selected. | `true` |
| classNames | `Record<"base"| "wrapper"| "title"| "description"| "shortcut" | "selectedIcon", string>` | Allows to set custom class names for the dropdown item slots. | - |
| classNames | `Record<"base"| "wrapper"| "title"| "description"| "shortcut" | "selectedIcon", string>` | Allows to set custom class names for the dropdown item slots, which will override the menu `itemClasses`. | - |

### DropdownItem Events

Expand Down
41 changes: 41 additions & 0 deletions packages/components/menu/__tests__/menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,47 @@ describe("Menu", () => {
expect(onClick).toHaveBeenCalledTimes(1);
});

it("should menuItem classNames work", () => {
const wrapper = render(
<Menu>
<MenuItem classNames={{title: "test"}}>New file</MenuItem>
</Menu>,
);
const menuItem = wrapper.getByText("New file");

expect(menuItem.classList.contains("test")).toBeTruthy();
});

it("should menuItem classNames override menu itemClasses", () => {
const wrapper = render(
<Menu itemClasses={{title: "test"}}>
<MenuItem classNames={{title: "test2"}}>New file</MenuItem>
</Menu>,
);
const menuItem = wrapper.getByText("New file");

expect(menuItem.classList.contains("test2")).toBeTruthy();
});
it("should merge menu item classNames with itemClasses", () => {
const wrapper = render(
<Menu itemClasses={{title: "test"}}>
<MenuItem classNames={{title: "test2"}}>New file</MenuItem>
<MenuItem>Delete file</MenuItem>
</Menu>,
);

const menuItemWithBoth = wrapper.getByText("New file");
const menuItemWithDefault = wrapper.getByText("Delete file");

// Check first MenuItem has both classes
expect(menuItemWithBoth.classList.contains("test2")).toBeTruthy();
expect(menuItemWithBoth.classList.contains("test")).toBeTruthy();

// Check second MenuItem only has the default class
expect(menuItemWithDefault.classList.contains("test")).toBeTruthy();
expect(menuItemWithDefault.classList.contains("test2")).toBeFalsy();
});

it("should truncate the text if the child is not a string", () => {
const wrapper = render(
<Menu>
Expand Down
7 changes: 5 additions & 2 deletions packages/components/menu/src/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {forwardRef} from "@nextui-org/system";
import {ForwardedRef, ReactElement, Ref} from "react";
import {mergeClasses} from "@nextui-org/theme";

import {UseMenuProps, useMenu} from "./use-menu";
import MenuSection from "./menu-section";
Expand Down Expand Up @@ -48,10 +49,12 @@ function Menu<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLUListElem
...item.props,
};

const mergedItemClasses = mergeClasses(itemClasses, itemProps?.classNames);

if (item.type === "section") {
return <MenuSection key={item.key} {...itemProps} itemClasses={itemClasses} />;
return <MenuSection key={item.key} {...itemProps} itemClasses={mergedItemClasses} />;
}
let menuItem = <MenuItem key={item.key} {...itemProps} classNames={itemClasses} />;
let menuItem = <MenuItem key={item.key} {...itemProps} classNames={mergedItemClasses} />;

if (item.wrapper) {
menuItem = item.wrapper(menuItem);
Expand Down
1 change: 1 addition & 0 deletions packages/core/theme/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export {
export type {SlotsToClasses} from "./types";
export {colorVariants} from "./variants";
export {COMMON_UNITS, twMergeConfig} from "./tw-merge-config";
export {mergeClasses} from "./merge-classes";
export {cn} from "./cn";
26 changes: 26 additions & 0 deletions packages/core/theme/src/utils/merge-classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {SlotsToClasses} from "./types";

import {clsx} from "@nextui-org/shared-utils";

/**
* Merges two sets of class names for each slot in a component.
* @param itemClasses - Base classes for each slot
* @param itemPropsClasses - Additional classes from props for each slot
* @returns A merged object containing the combined classes for each slot
*/
export const mergeClasses = <T extends SlotsToClasses<string>, P extends SlotsToClasses<string>>(
itemClasses?: T,
itemPropsClasses?: P,
): T => {
if (!itemClasses && !itemPropsClasses) return {} as T;

const keys = new Set([...Object.keys(itemClasses || {}), ...Object.keys(itemPropsClasses || {})]);

return Array.from(keys).reduce(
(acc, key) => ({
...acc,
[key]: clsx(itemClasses?.[key], itemPropsClasses?.[key]),
}),
{} as T,
);
};