Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/brave-trains-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nextui-org/menu": patch
Comment thread
jrgarciadev marked this conversation as resolved.
"@nextui-org/theme": patch
---

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,
);
};