Skip to content
Open
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
12 changes: 12 additions & 0 deletions .changeset/olive-donkeys-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"flowbite-react": patch
---

Support multiple dropdowns inside a single `<MegaMenu>`

### Changes

- [x] pair each `<MegaMenuDropdownToggle>` with its own `<MegaMenuDropdown>` by declaration order, instead of always resolving the first dropdown of the navbar
- [x] close the other dropdowns of a mega menu when one is opened
- [x] export `MegaMenuContext`, `useMegaMenuContext` and `MegaMenuContextValue`
- [x] stop calling hooks conditionally in `<MegaMenuDropdown>`
62 changes: 62 additions & 0 deletions apps/storybook/src/MegaMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Button,
MegaMenu,
MegaMenuDropdown,
MegaMenuDropdownToggle,
NavbarBrand,
NavbarCollapse,
NavbarLink,
Expand Down Expand Up @@ -106,3 +107,64 @@ const Template: StoryFn<MegaMenuProps> = (args) => (

export const Default = Template.bind({});
Default.args = {};

const MultipleDropdownsTemplate: StoryFn<MegaMenuProps> = (args) => (
<MegaMenu {...args}>
<NavbarBrand href="/">
<img alt="" src="/favicon.svg" className="mr-3 h-6 sm:h-9" />
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Flowbite</span>
</NavbarBrand>
<NavbarToggle />
<NavbarCollapse>
<NavbarLink href="#">Home</NavbarLink>
<NavbarLink as="span">
<MegaMenuDropdownToggle>Company</MegaMenuDropdownToggle>
</NavbarLink>
<NavbarLink as="span">
<MegaMenuDropdownToggle>Marketing</MegaMenuDropdownToggle>
</NavbarLink>
<NavbarLink href="#">Contact</NavbarLink>
</NavbarCollapse>
<MegaMenuDropdown className="hidden w-full">
<ul className="mx-auto mt-6 grid max-w-screen-xl border-y border-gray-200 px-2 py-5 shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-white sm:grid-cols-2 md:grid-cols-3">
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">About Us</div>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Careers</div>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Brand Center</div>
</a>
</li>
</ul>
</MegaMenuDropdown>
<MegaMenuDropdown className="hidden w-full">
<ul className="mx-auto mt-6 grid max-w-screen-xl border-y border-gray-200 px-2 py-5 shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-white sm:grid-cols-2 md:grid-cols-3">
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Blog</div>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Newsletter</div>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Events</div>
</a>
</li>
</ul>
</MegaMenuDropdown>
</MegaMenu>
);

export const MultipleDropdowns = MultipleDropdownsTemplate.bind({});
MultipleDropdowns.args = {};
8 changes: 8 additions & 0 deletions apps/web/content/docs/components/mega-menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ This example of a mega menu dropdown can be used to also show an icon near the t

<Example name="megaMenu.icons" />

## Multiple dropdowns

Use this example to show more than one mega menu dropdown inside the same navbar. Pair each `<MegaMenuDropdownToggle>` with a `<MegaMenuDropdown>` by declaring them in the same order: the first toggle controls the first dropdown, the second toggle the second one, and so on. The toggles and the dropdowns do not need to be siblings, so the toggles can stay inside `<NavbarCollapse>` while the dropdowns span the full width of the navbar.

Opening a dropdown closes any other dropdown of the same mega menu. Add a `hidden` class to a `<MegaMenuDropdown>` to have it start closed, and a `w-full` class to have it span its own row instead of sitting next to the navbar links.

<Example name="megaMenu.multipleDropdowns" />

## Full width dropdown

Use this example to show a mega menu dropdown that spans the entire width of the document page.
Expand Down
1 change: 1 addition & 0 deletions apps/web/examples/megaMenu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { fullWidth } from "./megaMenu.fullWidth";
export { fullWidthCTA } from "./megaMenu.fullWidthCTA";
export { fullWidthImage } from "./megaMenu.fullWidthImage";
export { icons } from "./megaMenu.icons";
export { multipleDropdowns } from "./megaMenu.multipleDropdowns";
export { root } from "./megaMenu.root";
179 changes: 179 additions & 0 deletions apps/web/examples/megaMenu/megaMenu.multipleDropdowns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
MegaMenu,
MegaMenuDropdown,
MegaMenuDropdownToggle,
NavbarBrand,
NavbarCollapse,
NavbarLink,
NavbarToggle,
} from "flowbite-react";
import { HiChevronDown } from "react-icons/hi";
import type { CodeData } from "~/components/code-demo";

const code = `
import {
MegaMenu,
MegaMenuDropdown,
MegaMenuDropdownToggle,
NavbarBrand,
NavbarCollapse,
NavbarLink,
NavbarToggle,
} from "flowbite-react";
import { HiChevronDown } from "react-icons/hi";

function Component() {
return (
<MegaMenu>
<NavbarBrand href="/">
<img alt="" src="/favicon.svg" className="mr-3 h-6 sm:h-9" />
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Flowbite</span>
</NavbarBrand>
<NavbarToggle />
<NavbarCollapse>
<NavbarLink href="/">Home</NavbarLink>
<NavbarLink as="span">
<MegaMenuDropdownToggle>
Company
<HiChevronDown className="ml-2" />
</MegaMenuDropdownToggle>
</NavbarLink>
<NavbarLink as="span">
<MegaMenuDropdownToggle>
Marketing
<HiChevronDown className="ml-2" />
</MegaMenuDropdownToggle>
</NavbarLink>
<NavbarLink href="/">Contact</NavbarLink>
</NavbarCollapse>
<MegaMenuDropdown className="hidden w-full">
<ul className="mx-auto mt-6 grid max-w-screen-xl border-y border-gray-200 px-2 py-5 shadow-sm sm:grid-cols-2 md:grid-cols-3 dark:border-gray-600 dark:bg-gray-800 dark:text-white">
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">About Us</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Learn more about our mission and team.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Careers</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Browse the roles we are hiring for.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Brand Center</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Download our logos and brand assets.</span>
</a>
</li>
</ul>
</MegaMenuDropdown>
<MegaMenuDropdown className="hidden w-full">
<ul className="mx-auto mt-6 grid max-w-screen-xl border-y border-gray-200 px-2 py-5 shadow-sm sm:grid-cols-2 md:grid-cols-3 dark:border-gray-600 dark:bg-gray-800 dark:text-white">
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Blog</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Read the latest product announcements.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Newsletter</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Get a monthly digest in your inbox.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Events</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Meet the team at an upcoming meetup.</span>
</a>
</li>
</ul>
</MegaMenuDropdown>
</MegaMenu>
);
}
`;

function Component() {
return (
<MegaMenu>
<NavbarBrand href="/">
<img alt="" src="/favicon.svg" className="mr-3 h-6 sm:h-9" />
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Flowbite</span>
</NavbarBrand>
<NavbarToggle />
<NavbarCollapse>
<NavbarLink href="/">Home</NavbarLink>
<NavbarLink as="span">
<MegaMenuDropdownToggle>
Company
<HiChevronDown className="ml-2" />
</MegaMenuDropdownToggle>
</NavbarLink>
<NavbarLink as="span">
<MegaMenuDropdownToggle>
Marketing
<HiChevronDown className="ml-2" />
</MegaMenuDropdownToggle>
</NavbarLink>
<NavbarLink href="/">Contact</NavbarLink>
</NavbarCollapse>
<MegaMenuDropdown className="hidden w-full">
<ul className="mx-auto mt-6 grid max-w-screen-xl border-y border-gray-200 px-2 py-5 shadow-sm sm:grid-cols-2 md:grid-cols-3 dark:border-gray-600 dark:bg-gray-800 dark:text-white">
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">About Us</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Learn more about our mission and team.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Careers</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Browse the roles we are hiring for.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Brand Center</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Download our logos and brand assets.</span>
</a>
</li>
</ul>
</MegaMenuDropdown>
<MegaMenuDropdown className="hidden w-full">
<ul className="mx-auto mt-6 grid max-w-screen-xl border-y border-gray-200 px-2 py-5 shadow-sm sm:grid-cols-2 md:grid-cols-3 dark:border-gray-600 dark:bg-gray-800 dark:text-white">
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Blog</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Read the latest product announcements.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Newsletter</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Get a monthly digest in your inbox.</span>
</a>
</li>
<li>
<a href="#" className="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div className="font-semibold">Events</div>
<span className="text-sm text-gray-500 dark:text-gray-400">Meet the team at an upcoming meetup.</span>
</a>
</li>
</ul>
</MegaMenuDropdown>
</MegaMenu>
);
}

export const multipleDropdowns: CodeData = {
type: "single",
code: {
fileName: "index",
language: "tsx",
code,
},
githubSlug: "megaMenu/megaMenu.multipleDropdowns.tsx",
component: <Component />,
};
80 changes: 80 additions & 0 deletions packages/ui/src/components/MegaMenu/MegaMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,71 @@ describe("Components / MegaMenu", () => {
expect(dropdown).not.toHaveClass("hidden");
});

it("should pair each <MegaMenuDropdownToggle> with its own <MegaMenuDropdown>", async () => {
render(<MultipleMegaMenuTest />);

const [companyToggle, marketingToggle] = screen.getAllByRole("button");
const [companyDropdown, marketingDropdown] = screen.getAllByRole("menu");

expect(companyToggle.getAttribute("aria-controls")).toEqual(companyDropdown.id);
expect(marketingToggle.getAttribute("aria-controls")).toEqual(marketingDropdown.id);

expect(companyDropdown.getAttribute("aria-labelledby")).toEqual(companyToggle.id);
expect(marketingDropdown.getAttribute("aria-labelledby")).toEqual(marketingToggle.id);
});

it("should hide/show only its own <MegaMenuDropdown> on click <MegaMenuDropdownToggle>", async () => {
const user = userEvent.setup();
render(<MultipleMegaMenuTest />);

const [companyToggle, marketingToggle] = screen.getAllByRole("button");
const [companyDropdown, marketingDropdown] = screen.getAllByRole("menu");

expect(companyDropdown).not.toHaveClass("hidden");
expect(marketingDropdown).not.toHaveClass("hidden");

await user.click(marketingToggle);

expect(marketingDropdown).toHaveClass("hidden");
expect(marketingToggle).toHaveAttribute("aria-expanded", "false");
expect(companyDropdown).not.toHaveClass("hidden");
expect(companyToggle).toHaveAttribute("aria-expanded", "true");

await user.click(marketingToggle);

expect(marketingDropdown).not.toHaveClass("hidden");
expect(marketingToggle).toHaveAttribute("aria-expanded", "true");
});

it("should close the other <MegaMenuDropdown> when one is opened", async () => {
const user = userEvent.setup();
render(<MultipleMegaMenuTest />);

const [companyToggle, marketingToggle] = screen.getAllByRole("button");
const [companyDropdown, marketingDropdown] = screen.getAllByRole("menu");

// both panels are declared without `hidden`, so they start open
await user.click(companyToggle);
await user.click(marketingToggle);

expect(companyDropdown).toHaveClass("hidden");
expect(marketingDropdown).toHaveClass("hidden");

await user.click(companyToggle);

expect(companyDropdown).not.toHaveClass("hidden");
expect(companyToggle).toHaveAttribute("aria-expanded", "true");
expect(marketingDropdown).toHaveClass("hidden");
expect(marketingToggle).toHaveAttribute("aria-expanded", "false");

await user.click(marketingToggle);

expect(marketingDropdown).not.toHaveClass("hidden");
expect(marketingToggle).toHaveAttribute("aria-expanded", "true");
expect(companyDropdown).toHaveClass("hidden");
expect(companyToggle).toHaveAttribute("aria-expanded", "false");
});

it("should hide/show <MegaMenuDropdown toggle={..}> on click toggle", async () => {
const user = userEvent.setup();
render(<MegaMenuDropdownOnlyTest />);
Expand Down Expand Up @@ -63,6 +128,21 @@ function MegaMenuTest() {
);
}

function MultipleMegaMenuTest() {
return (
<MegaMenu>
<MegaMenuDropdownToggle>Company</MegaMenuDropdownToggle>
<MegaMenuDropdown>
<p>Company content</p>
</MegaMenuDropdown>
<MegaMenuDropdownToggle>Marketing</MegaMenuDropdownToggle>
<MegaMenuDropdown>
<p>Marketing content</p>
</MegaMenuDropdown>
</MegaMenu>
);
}

function MegaMenuDropdownOnlyTest() {
return (
<MegaMenu>
Expand Down
Loading
Loading