Satel
Satel

Reputation: 187

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?

Upvotes: 5

Views: 6162

Answers (3)

jacobedawson
jacobedawson

Reputation: 3212

There's a way to do this with React (assuming you're using @headlessui/react) via useState:

  const [disclosureState, setDisclosureState] = useState(0);

  function handleDisclosureChange(state: number) {
    if (state === disclosureState) {
      setDisclosureState(0); // close all of them
    } else {
      setDisclosureState(state); // open the clicked disclosure
    }
  }

And in each Disclosure component, just pass an onClick callback to the Disclosure.Button:

<Disclosure.Button onClick={() => handleDisclosureChange(N)} />

Where N is the index of the clicked Disclosure (using 1 as the first Disclosure, since 0 handles all disclosures closed).

Finally, conditionally render the Disclosure.Panel based on the disclosureState:

{
  disclosureState === N && (<Disclosure.Panel />)
}

Where N is the index of the clicked Disclosure. Using this method you can open just 1 disclosure at a time, and clicking an open disclosure will close all of them.

Upvotes: 5

FlorentinoG9
FlorentinoG9

Reputation: 1

it is possible just you need to add some extra props selectors to the Disclosure.Button Component, in this case, I am adding aria-label='panel' like so...

import { Disclosure } from '@headlessui/react'

function MyDisclosure() {
  return (
    <Disclosure>
      <Disclosure.Button aria-label="panel" className="py-2">
        Is team pricing available?
      </Disclosure.Button>
      <Disclosure.Panel className="text-gray-500">
        Yes! You can purchase a license that you can share with your entire
        team.
      </Disclosure.Panel>
    </Disclosure>
  )
}

next you need to select the following with "querySelectorAll" like...

    <button
     type='button'
     onClick={() => {
        const panels = [...document.querySelectorAll('[aria-expanded=true][aria-label=panel]')]
        panels.map((panel) => panel.click())
     }}
    >
</button>

with this, you just need to change 'aria-expanded' to either 'true' or 'false' to expand or collapse

Upvotes: 0

Som Shekhar Mukherjee
Som Shekhar Mukherjee

Reputation: 8188

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

Upvotes: 4

Related Questions