Search code examples
reactjsnext.jsfrontendnextui

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


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

Solution

  • 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>
      )
    }