Open a dropdown with a single click when another dropdown is already open NextUI

171 Views Asked by At

As the title suggest, I've got a NextJS component which renders NextUI Dropdowns using a object of data. The problem i'm facing right now is that when i have a dropdown already open and want to open anoter dropdown rendered by the component i must click it twice to get it open (one click for close the already open dropdown and one click for open the dropdown i want to).

The component is for a NextJS project using typescript, tailwind for styles and NextUI react library

enter image description here

This is the code of the component:

'use client'

import React, { useState } from 'react'
import {
  Navbar,
  NavbarContent,
  NavbarItem,
  Link,
  DropdownMenu,
  DropdownItem,
  Dropdown,
  DropdownTrigger,
  Button
} from '@nextui-org/react'

export function NavbarFooter() {
  const items = [
    {
      title: 'Dropdown 1',
      dropdown: [
        { title: 'Subitem 1', path: '/' },
        { title: 'Subitem 2', path: '/' }
      ]
    },
    {
      title: 'Dropdown 2',
      dropdown: [
        { title: 'Subitem 1', path: '/' },
        { title: 'Subitem 2', path: '/' }
      ]
    },
    { title: 'Item 3', path: '/' }
  ]

  const [activeDropdown, setActiveDropdown] = useState<null | number>(null)

  const handleDropdownClick = (index: number | null) => {
    setActiveDropdown((prev) => (prev === index ? null : index))
  }

  return (
    <Navbar
      className="top-[4rem] w-full bg-[#BC9A22] px-0 md:h-[2.8rem]"
      height="0.8rem"
      maxWidth="2xl"
    >
      <NavbarContent justify="end" className="">
        {items.map((item, index) =>
          item.dropdown ? (
            <NavbarItem key={`${item.title}-${index}`}>
              <Dropdown
                isOpen={activeDropdown === index}
                onOpenChange={() => handleDropdownClick(index)}
              >
                <DropdownTrigger>
                  <Button>
                    {item.title}
                    {activeDropdown === index ? ' ▲' : ' ▼'}
                  </Button>
                </DropdownTrigger>

                <DropdownMenu>
                  {item.dropdown.map((subItem, subIndex) => (
                    <DropdownItem key={subIndex}>
                      <Link href={subItem.path}>{subItem.title}</Link>
                    </DropdownItem>
                  ))}
                </DropdownMenu>
              </Dropdown>
            </NavbarItem>
          ) : (
            <NavbarItem key={`${item.title}-${index}`}>
              <Link href={item.path}>{item.title}</Link>
            </NavbarItem>
          )
        )}
      </NavbarContent>
    </Navbar>
  )
}
3

There are 3 best solutions below

0
Carlos Casado Vargas On BEST ANSWER

It's done. Using @Batman code's answer and adding shouldCloseOnInteractOutside={() => false} in the Dropdown component it behaves as expected.

Explanation why:

  • By adding shouldCloseOnInteractOutside={() => false} in the Dropdown fix the problem i was facing of having to click twice to open a dropdown when another one is already open. It fix the problem but add another one: It cancels the possibility to close the dropdown by clicking out of it. Gif of the solution of the first problem

  • To fix the new problem, the code that @Batman provided is very helpful (only the useEffect). With the function change is not posible to close the dropdown by clicking it again. the problem fixed

This is the modified and functional component:

'use client'

import React, { useEffect, useState } from 'react'
import {
  Navbar,
  NavbarContent,
  NavbarItem,
  Link,
  DropdownMenu,
  DropdownItem,
  Dropdown,
  DropdownTrigger,
  Button
} from '@nextui-org/react'

export function NavbarFooter() {
  const items = [
    {
      title: 'Dropdown 1',
      dropdown: [
        { title: 'Subitem 1', path: '/' },
        { title: 'Subitem 2', path: '/' }
      ]
    },
    {
      title: 'Dropdown 2',
      dropdown: [
        { title: 'Subitem 1', path: '/' },
        { title: 'Subitem 2', path: '/' }
      ]
    },
    { title: 'Item 3', path: '/' }
  ]

  const [activeDropdown, setActiveDropdown] = useState<null | number>(null)

  const handleDropdownClick = (index: number | null) => {
    setActiveDropdown((prev) => (prev === index ? null : index))
  }

  useEffect(() => {
    const closeDropdown = (event: MouseEvent) => {
      // Cast the event target to an HTMLElement instance
      const target = event.target as HTMLElement
      // Check if the clicked element is part of a dropdown. If not, close the open dropdown.
      const isDropdown =
        target.closest('[role="listbox"]') ||
        target.closest('[data-nextui-dropdown-trigger]')
      if (!isDropdown) {
        setActiveDropdown(null)
      }
    }

    // Attach the event listener to the window
    window.addEventListener('mousedown', closeDropdown)

    // Clean up the event listener when the component is unmounted
    return () => window.removeEventListener('mousedown', closeDropdown)
  }, [])

  return (
    <Navbar
      className="top-[4rem] w-full bg-[#BC9A22] px-0 md:h-[2.8rem]"
      height="0.8rem"
      maxWidth="2xl"
    >
      <NavbarContent justify="end" className="">
        {items.map((item, index) =>
          item.dropdown ? (
            <NavbarItem key={`${item.title}-${index}`}>
              <Dropdown
                isOpen={activeDropdown === index}
                onOpenChange={() => handleDropdownClick(index)}
                shouldCloseOnInteractOutside={() => false}
              >
                <DropdownTrigger>
                  <Button>
                    {item.title}
                    {activeDropdown === index ? ' ▲' : ' ▼'}
                  </Button>
                </DropdownTrigger>

                <DropdownMenu>
                  {item.dropdown.map((subItem, subIndex) => (
                    <DropdownItem key={subIndex}>
                      <Link href={subItem.path}>{subItem.title}</Link>
                    </DropdownItem>
                  ))}
                </DropdownMenu>
              </Dropdown>
            </NavbarItem>
          ) : (
            <NavbarItem key={`${item.title}-${index}`}>
              <Link href={item.path}>{item.title}</Link>
            </NavbarItem>
          )
        )}
      </NavbarContent>
    </Navbar>
  )
}

0
Techno55 On

Maybe try changing the click handler to:

const handleDropdownClick = (index: number | null) => {
    if(activeDropdown === index) {
        setActiveDropdown(null);
    }else {
        setActiveDropdown(index);
    }
}
1
Hashan Hemachandra On

You can modify the handleDropdownClick function to set the new active dropdown index directly, without toggling the current one.

const handleDropdownClick = (index: number) => {
  // Set the active dropdown to the current index
  setActiveDropdown(index);
};

To close the dropdown, you will need to add an effect that attaches a click event listner to the window, and when a click is detected outside of the dropdown content, the dropdown is closed.

useEffect(() => {
  const closeDropdown = (event: MouseEvent) => {
    // Cast the event target to an HTMLElement instance
    const target = event.target as HTMLElement;
    // Check if the clicked element is part of a dropdown. If not, close the open dropdown.
    const isDropdown = target.closest('[role="listbox"]') || target.closest('[data-nextui-dropdown-trigger]');
    if (!isDropdown) {
      setActiveDropdown(null);
    }
  };

  // Attach the event listener to the window
  window.addEventListener('mousedown', closeDropdown);

  // Clean up the event listener when the component is unmounted
  return () => window.removeEventListener('mousedown', closeDropdown);
}, []);