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:

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