Reputation: 353
I've just started using Headless UI. I'm trying to use the Disclosure component from Headless UI to render my job experiences. Basically, I need "n" number of Disclosures which will be dynamically rendered and whenever one Disclosure is opened the others should close.
I am able to render the Disclosures dynamically, and they all have their individual states. (opening/closing a disclosure doesn't affect the other Disclosure). All I want to do is to have only one disclosure open at a time. Opening another Disclosure should close all the remaining Disclosures. I have gone through their docs but couldn't find a way to manage multiple Disclosure states together.
Here is my code:
import React, { useContext } from "react";
import { GlobalContext } from "../data/GlobalContext";
import { Tab, Disclosure } from "@headlessui/react";
import ReactMarkdown from "react-markdown";
const Experience = () => {
const { data } = useContext(GlobalContext);
const expData = data.pageContent.find(
(content) => content.__component === "page-content.experience-page-content"
);
return (
<div className="container h-screen">
<div className="flex h-full flex-col items-center justify-center">
<h3 className="">{expData.pageTitle}</h3>
<div className="flex min-h-[600px] flex-col">
{expData.jobs.map((job, i) => (
<Disclosure key={job.companyName} defaultOpen={i === 0}>
<Disclosure.Button
key={job.companyName + "_tab"}
className="px-4 py-3 dark:text-dark-primary"
>
{job.companyName}
</Disclosure.Button>
<Disclosure.Panel key={job.companyName + "_panel"}>
<p className="">
<span className="">{job.designation}</span>
<span className="">{" @ "}</span>
<span className="">{job.companyName}</span>
</p>
<p className="">{job.range}</p>
<ReactMarkdown className="">
{job.workDescription}
</ReactMarkdown>
</Disclosure.Panel>
</Disclosure>
))}
</div>
</div>
</div>
);
};
export default Experience;
It would be really helpful if someone could help me with this. Thanks.
Upvotes: 10
Views: 10811
Reputation: 1
If anyone is still looking for a quick hack for vuejs
"@click=closeOpenTabs"
to the DisclosureButton
const closeOpenTabs = () => document
.querySelectorAll('button[data-headlessui-state="open"]')
.forEach((b) => b.click());
Upvotes: 0
Reputation: 321
If you have multiple disclosures in different components here a tricky solution without using ref or useState
https://github.com/tailwindlabs/headlessui/discussions/475#discussioncomment-7638849
Upvotes: 0
Reputation: 1
I solved it by using this simple approach.
const handleDisclosureButtonClick = (event) => {
const disclosureButtons =
document.getElementsByClassName("disclosure-button");
for (let i = 0; i < disclosureButtons.length; i++) {
const disclosureButton = disclosureButtons.item(i);
if (
disclosureButton !== event.target &&
disclosureButton.getAttribute("data-headlessui-state") === "open"
) {
disclosureButton.click();
}
}
};
disclosure-button
for query and event handler to the disclosure button<Disclosure.Button className="disclosure-button" onClick={handleDisclosureButtonClick}>
button
</Disclosure.Button>
Upvotes: 0
Reputation: 228
I had the same issue and solved by using the Disclosure internal render props and some external state. The code below makes each disclosure to toggle itself and close others when opended (if any).
export default function MyAccordion({ data }) {
const [activeDisclosurePanel, setActiveDisclosurePanel] = useState(null);
function togglePanels(newPanel) {
if (activeDisclosurePanel) {
if (activeDisclosurePanel.key !== newPanel.key && activeDisclosurePanel.open) {
activeDisclosurePanel.close();
}
}
setActiveDisclosurePanel({
...newPanel,
open: !newPanel.open
});
}
return (
<ul>
{
data.map((item, index) => (
<Disclosure as="li" key={ index }>
{
(panel) => {
const { open, close } = panel
return (<>
<Disclosure.Button onClick={ () => {
if (!open) {
// On the first click, the panel is opened but the "open" prop's value is still false. Therefore the falsey verification
// This will make so the panel close itself when we click it while open
close();
}
// Now we call the function to close the other opened panels (if any)
togglePanels({ ...panel, key: index });
}}>
</Disclosure.Button>
<Disclosure.Panel>
{ item }
</Disclosure.Panel>
</>)
}
}
</Disclosure>
))
}
</ul>
);
}
Upvotes: 10
Reputation: 1168
Ok, I stole from various sources and managed to hack it. I haven't tested it for accessibility but it has some interesting things because it deviates a little bit (rather usefully if you ask me) from the React
mental model.
tldr
is that you will need to trigger clicks on the other elements imperatively via ref.current?.click()
Here we can't use hooks since you can't call hooks inside loops or conditionals, we use React.createRef<HTMLButtonElement>()
instead
const refs = React.useMemo(() => {
return (
items.map(() => {
return React.createRef<HTMLButtonElement>();
}) ?? []
);
}, [items]);
ref
to the Disclosure.Button
component{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button ref={refs[idx]}>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
this one is gonna be specially useful for the next step
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button
ref={refs[idx]}
data-id={item.id}
data-open={open}
>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
handleClosingOthers
function (an onClick handler)Basically here we get all the buttons that aren't the one that the user is clicking, verifying if they are open and if they are, clicking programmatically on them to close them.
function handleClosingOthers(id: string) {
const otherRefs = refs.filter((ref) => {
return ref.current?.getAttribute("data-id") !== id;
});
otherRefs.forEach((ref) => {
const isOpen = ref.current?.getAttribute("data-open") === "true";
if (isOpen) {
ref.current?.click();
}
});
}
onClick
handler{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button
ref={refs[idx]}
data-id={item.id}
data-open={open}
onClick={() => handleClosingOthers(item.id)}
>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
Upvotes: 2
Reputation: 536
I've used this approach:
function Akkordion({ items }) {
const buttonRefs = useRef([]);
const openedRef = useRef(null);
const clickRecent = (index) => {
const clickedButton = buttonRefs.current[index];
if (clickedButton === openedRef.current) {
openedRef.current = null;
return;
}
if (Boolean(openedRef.current?.getAttribute("data-value"))) {
openedRef.current?.click();
}
openedRef.current = clickedButton;
};
return (
<div>
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({ open }) => (
<div>
<Disclosure.Button as="div">
<button
data-value={open}
ref={(ref) => {
buttonRefs.current[idx] = ref;
}}
onClick={() => clickRecent(idx)}
>
{item.label}
</button>
</Disclosure.Button>
<Disclosure.Panel
>
{item.content}
</Disclosure.Panel>
</div>
)}
</Disclosure>
))}
</div>
);
}
Upvotes: 0
Reputation: 31
<template>
<div class="mx-auto w-full max-w-md space-y-3 rounded-2xl bg-white p-2">
<Disclosure v-for="(i, idx) in 3" :key="i" v-slot="{ open, close }">
<DisclosureButton
:ref="el => (disclosure[idx] = close)"
class="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"
@click="hideOther(idx)"
>
<span> What is your refund policy? {{ open }} </span>
</DisclosureButton>
<DisclosurePanel class="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.
</DisclosurePanel>
</Disclosure>
</div>
</template>
<script setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
const disclosure = ref([])
const hideOther = id => {
disclosure.value.filter((d, i) => i !== id).forEach(c => c())
}
</script>
here how I did it in Vue.
Upvotes: 3