Search code examples
javascriptreactjsheadless-ui

How to collapse Siblings with Headless UI


To make the accordion component with Headless UI, I have used Disclosure component. But I have a problem to control the collapse/expand state for it's siblings.

So, I want to close other siblings when I open one, but Disclosure component is only supporting internal render props, open and close. So, I can't control it outside of the component and can't close others when I open one.

import { Disclosure } from '@headlessui/react'
import { ChevronUpIcon } from '@heroicons/react/solid'

export default function Example() {
  return (
    <div className="w-full px-4 pt-16">
      <div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2">
        <Disclosure>
          {({ open }) => (
            <>
              <Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
                <span>What is your refund policy?</span>
                <ChevronUpIcon
                  className={`${
                    open ? 'rotate-180 transform' : ''
                  } h-5 w-5 text-purple-500`}
                />
              </Disclosure.Button>
              <Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
                If you're unhappy with your purchase for any reason, email us
                within 90 days and we'll refund you in full, no questions asked.
              </Disclosure.Panel>
            </>
          )}
        </Disclosure>
        <Disclosure as="div" className="mt-2">
          {({ open }) => (
            <>
              <Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
                <span>Do you offer technical support?</span>
                <ChevronUpIcon
                  className={`${
                    open ? 'rotate-180 transform' : ''
                  } h-5 w-5 text-purple-500`}
                />
              </Disclosure.Button>
              <Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
                No.
              </Disclosure.Panel>
            </>
          )}
        </Disclosure>
      </div>
    </div>
  )
}

How do we control the close/open state outside of the component?


Solution

  • I don't think so it's possible using HeadlessUI, although you can create your own Disclosure like component.

    • Lift the state up to the parent component by creating a disclosures state that stores all the information about the disclosures.
    • Loop over the disclosures using map and render them.
    • Render a button that toggles the isClose property of the disclosures and also handles the aria attributes.
    • On button click, toggle the isOpen value of the clicked disclosure and close all the other disclosures.

    Checkout the snippet below:

    import React, { useState } from "react";
    import { ChevronUpIcon } from "@heroicons/react/solid";
    
    export default function Example() {
      const [disclosures, setDisclosures] = useState([
        {
          id: "disclosure-panel-1",
          isOpen: false,
          buttonText: "What is your refund policy?",
          panelText:
            "If you're unhappy with your purchase for any reason, email us within 90 days and we'll refund you in full, no questions asked."
        },
        {
          id: "disclosure-panel-2",
          isOpen: false,
          buttonText: "Do you offer technical support?",
          panelText: "No."
        }
      ]);
    
      const handleClick = (id) => {
        setDisclosures(
          disclosures.map((d) =>
            d.id === id ? { ...d, isOpen: !d.isOpen } : { ...d, isOpen: false }
          )
        );
      };
    
      return (
        <div className="w-full px-4 pt-16">
          <div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2 space-y-2">
            {disclosures.map(({ id, isOpen, buttonText, panelText }) => (
              <React.Fragment key={id}>
                <button
                  className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
                  onClick={() => handleClick(id)}
                  aria-expanded={isOpen}
                  {...(isOpen && { "aria-controls": id })}
                >
                  {buttonText}
                  <ChevronUpIcon
                    className={`${
                      isOpen ? "rotate-180 transform" : ""
                    } h-5 w-5 text-purple-500`}
                  />
                </button>
                {isOpen && (
                  <div className="px-4 pt-4 pb-2 text-sm text-gray-500">
                    {panelText}
                  </div>
                )}
              </React.Fragment>
            ))}
          </div>
        </div>
      );
    }