Replies: 26 comments 16 replies
-
@RobinMalfait Is there anything we can do about this? I also have several Disclosures and all other needs to close when one opens |
Beta Was this translation helpful? Give feedback.
-
in vue it can be done with some tricky way |
Beta Was this translation helpful? Give feedback.
-
Hello, guys |
Beta Was this translation helpful? Give feedback.
-
This would be a nice feature |
Beta Was this translation helpful? Give feedback.
-
Its is actually (partially) possible. Its a very ugly solution but
|
Beta Was this translation helpful? Give feedback.
-
Is this going to be done at some point? |
Beta Was this translation helpful? Give feedback.
-
I'm waiting on this lol |
Beta Was this translation helpful? Give feedback.
-
Definitely would love to see this. As it stands it's just a bad user experience that forces unnecessary scrolling and clutter. |
Beta Was this translation helpful? Give feedback.
-
I've come up with a pretty simple workaround. Maybe there are drawbacks I'm not aware of so far, but it works good for me. My solution comes quite close to that what @relatkrusodotdk was suggesting. Additionaly I would advise onMouseUp/onTouchEnd instead of onMouseDown/onTouchStart for less jittering
edit: forgot to pass close as args in closeCurrent |
Beta Was this translation helpful? Give feedback.
-
I tried the solution(s) proposed by @kevinfluegel but am not able to get it to work reliably with keyboard navigation. Sometimes I simply wish that more could be exposed from the library. If we would have access to the Disclosure Context from the outside this library would be much simpler to use. I am finding myself constantly battling the components as they are today since I like to have programmatic control over them. Maybe I'll just end up copying the code. |
Beta Was this translation helpful? Give feedback.
-
The fact that Disclosure component doesn't expose its |
Beta Was this translation helpful? Give feedback.
-
Hey Guys, I also had the same issue and this works perfect. Check this out. |
Beta Was this translation helpful? Give feedback.
-
Dunno if this issue is still relevant but i managed to solve it with a straight forward solution. I used the onClickOutside hook logic. Basically, wrap the content in the function as you do to have access to the close(). Then instead of adding the content directly there in the function make it a component, call it PanelContent This component can receive the close() as a prop Inside the content component add the onClick outside hook and make it so that when you click outside the content - ex: on the disclosure button or any other element, the close() is called... and there you go. useEffect(() => {
}, []); |
Beta Was this translation helpful? Give feedback.
-
This works for two Disclosures:
|
Beta Was this translation helpful? Give feedback.
-
Hi, the way I used for vue3
Hope this will help :) |
Beta Was this translation helpful? Give feedback.
-
So let's say I have a ton of disclosures that each handle their own state with open. Why can't I just assign an open={useOpen} prop to the Disclosures and handle it inside an object, preferably through context? Then you can do cool things like collapse or uncollapse all the disclosures with the click of a button, using again the context's setter. The fact it's a render prop and doesn't allow the developer to make decisions on the UI functionality to me is a huge drawback of this component. |
Beta Was this translation helpful? Give feedback.
-
This is what if we follow @mroussel-rhinos's solution in React-
|
Beta Was this translation helpful? Give feedback.
-
Hello the Disclosure.panel exposes a render props "close" that we can use to close other tabs depending on the current opened index with a simplified and semantic implementation hope that helps. const [disclosureState, setDisclosureState] = React.useState(0); function handleDisclosureChange(i) { `{productionMatches.map((elem, i) => (
|
Beta Was this translation helpful? Give feedback.
-
@RobinMalfait Are there any plans to implement a new feature/component with the ability to open one disclosure at a time? I think it's a pretty common requirement that other libraries have solved. The workarounds shared here in the last two years are quite intricate. Thank you. |
Beta Was this translation helpful? Give feedback.
-
This is how i was able to do so.
/**
* This is done to prevent Next.js from throwing an error when using
* document.querySelector() in the useEffect hook.
*/
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
/**
* Close all other accordions when one is clicked.
*/
function closeOtherAccordions() {
// Find all buttons with an open state.
const buttons = document.querySelectorAll(
'button[data-headlessui-state="open"]'
)
// Re-run click on each open button to close them.
buttons.forEach((button) => {
button.click()
})
}
<Disclosure.Button
onMouseDown={() => {mounted && closeOtherAccordions()}}
> |
Beta Was this translation helpful? Give feedback.
-
It's a shame that we have to hack around such basic functionality 😞 Here's an alternative implementation for @Moelafi 's great idea to use on-Click-Outside logic. This version uses real export function YourSinglePanelDisclosureComponent() {
const disclosureRef = useRef(null)
const closeFnRef = useRef<() => void>()
function onClickOutside() {
if (closeFnRef.current) {
closeFnRef.current()
}
}
useOnClickOutside(disclosureRef, onClickOutside, "mouseup") // "mousedown" -> "mouseup" to avoid (race condition?) bug
return (
<Disclosure
as="div"
...
ref={disclosureRef}
>
{({open, close}) => {
closeFnRef.current = close
return ...
}}
</Disclosure>
)
}) With reusable hook:hooks function useExclusiveDisclosure<T extends HTMLElement>() {
const disclosureRef = useRef<T>(null)
const closeFnRef = useRef<() => void>()
function onClickOutside() {
if (closeFnRef.current) {
closeFnRef.current()
}
}
useOnClickOutside(disclosureRef, onClickOutside, "mouseup")
return {disclosureRef, closeFnRef}
} components export function YourSinglePanelDisclosureComponent() {
const {disclosureRef, closeFnRef} = useExclusiveDisclosure()
return (
<Disclosure
as="div"
...
ref={disclosureRef}
>
{({open, close}) => {
closeFnRef.current = close
return ...
}}
</Disclosure>
)
}) |
Beta Was this translation helpful? Give feedback.
-
There is a |
Beta Was this translation helpful? Give feedback.
-
If you have multiple disclosures in different components here a tricky solution without using ref or useState `
|
Beta Was this translation helpful? Give feedback.
-
I figured I should use the close() function in every other Disclosure instance when I click on a particular instance. I would save the current active one in a useState and check with each instance if the ref is equal to the active state and call the close() function if it's not.
It might be a little messy, but I'm using the provided close() function to close the Disclosures which is better than writing a custom close function imo. |
Beta Was this translation helpful? Give feedback.
-
Hello, I have a lot of problem with a precise usecase, but with my friend, we found a solution.
It's really useful with headlessUI accordion |
Beta Was this translation helpful? Give feedback.
-
Finally resolved as a charm with a lot of help of some guys here, in special, @kevinfluegel: import { useInView } from 'framer-motion';
import {
FC,
MouseEvent,
PropsWithChildren,
ReactNode,
useCallback,
useEffect,
useRef,
useState
} from 'react';
import { delay } from 'utils/delay';
import Accordion from './Accordion';
import Entering from './Entering';
type TAcordion = {
title?: ReactNode;
children?: ReactNode;
defaultOpen?: boolean;
};
interface IAcordions {
acordions?: TAcordion[];
}
const Accordions: FC<PropsWithChildren<IAcordions>> = ({ acordions }) => {
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const refDiv = useRef<HTMLDivElement | null>(null);
const openedRef = useRef<HTMLButtonElement | null>(null);
const inView = useInView(refDiv, {
once: true,
amount: 'all'
});
const [tryToOpen, setTryToOpen] = useState(true);
const handleClick = (index: number) => async (e: MouseEvent<HTMLButtonElement>) => {
if (!!buttonRefs.current?.length) {
const clickedButton = buttonRefs.current[index];
if (clickedButton === openedRef.current) return (openedRef.current = null);
if (openedRef.current && !!openedRef.current?.getAttribute('data-value')) {
openedRef.current?.click();
e.preventDefault();
e.stopPropagation();
await delay(450);
clickedButton?.click();
}
openedRef.current = clickedButton;
}
};
const getRef = (index: number) => (ref: HTMLButtonElement | null) =>
(buttonRefs.current[index] = ref);
const handler = () => {
if (typeof window !== 'undefined') {
const elements = window?.document?.querySelectorAll(
'div[aria-expanded="false"]'
) as NodeListOf<HTMLDivElement>;
if (!!elements.length) elements[0]?.click();
}
};
const handlerTimer = useCallback(async () => {
await delay(200);
handler();
await delay(200);
handler();
if (buttonRefs.current) openedRef.current = buttonRefs.current[0];
setTryToOpen(false);
}, []);
useEffect(() => {
if (tryToOpen && inView) handlerTimer();
}, [handlerTimer, inView, tryToOpen]);
return (
<div ref={refDiv}>
{acordions?.map(({ children, title }, index) => (
<Entering key={'acordion' + index} initial={false}>
<Accordion title={title} onClick={handleClick(index)} ref={getRef(index)}>
{children}
</Accordion>
</Entering>
))}
</div>
);
};
export default Accordions; import { Disclosure, Transition } from '@headlessui/react';
import Arrow from 'components/Arrow';
import {
forwardRef,
ForwardRefRenderFunction,
MouseEvent,
PropsWithChildren,
ReactNode
} from 'react';
import { cn } from 'utils/cn';
interface IAccordion {
title?: ReactNode;
defaultOpen?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}
const Accordion: ForwardRefRenderFunction<HTMLButtonElement, PropsWithChildren<IAccordion>> = (
{ children, title, onClick, defaultOpen },
ref
) => {
return (
<div className=" my-4 w-full">
<div className="w-full">
<Disclosure defaultOpen={defaultOpen}>
{({ open }) => (
<div
className={cn(
'rounded-6xl transition-colors duration-200 ease-in-out',
'border-1 border-tolq-gray-100 bg-white',
open && ' shadow-custom-light-gray outline-none'
)}
>
<Disclosure.Button as="div">
<button
onClick={onClick}
className={cn(
'transition-colors duration-200 ease-in-out',
'group flex w-full items-center justify-between',
'text-left text-sm font-medium',
'focus-visible:ring-offset-primary-blue-500/75',
'focus:outline-none focus-visible:ring',
'px-4 py-2',
'text-tertiary-100 hover:bg-tolq-gray-200',
'rounded-6xl bg-white',
open && 'rounded-b-none '
)}
data-value={open}
ref={ref}
>
{typeof title === 'string' ? (
<h2>{title}</h2>
) : (
<>{title}</>
)}
<Arrow open={!open} className="size-7 !text-tolq-black-200" />
</button>
</Disclosure.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0 h-0"
enterTo="transform scale-100 opacity-100 h-full"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100 h-full"
leaveTo="transform scale-95 opacity-0 h-0"
>
<Disclosure.Panel className="px-6 py-4 text-sm">{children}</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</div>
);
};
export default forwardRef(Accordion); import { AnimatePresence, motion, useInView, Variants } from 'framer-motion';
import { FC, HTMLAttributes, PropsWithChildren, useMemo, useRef } from 'react';
import { cn } from 'utils/cn';
export interface IEntering extends PropsWithChildren<HTMLAttributes<HTMLDivElement>> {
offset?: number;
position?: 'left' | 'right' | 'top' | 'bottom';
stiffness?: number;
duration?: number;
damping?: number;
mass?: number;
amount?: 'some' | 'all' | number;
delay?: number;
initial?: boolean;
}
const Entering: FC<IEntering> = ({
offset = 50,
position: initPosition = 'bottom',
duration = 0.2,
stiffness = 100,
damping = 12,
mass = 1,
delay = 0.0,
amount = 0.7,
children,
initial = true,
className = '',
style = {}
}) => {
const ref = useRef(null);
const inView = useInView(ref, {
once: true,
amount
});
const isActive = initial || inView;
const position = useMemo(() => {
if (initPosition === 'left') {
return { x: -1 * offset * 2 };
}
if (initPosition === 'right') {
return { x: offset };
}
if (initPosition === 'top') {
return { y: -1 * offset };
}
if (initPosition === 'bottom') {
return { y: offset };
}
}, [offset, initPosition]);
const variants: Variants = {
inactive: {
opacity: 1,
y: 0,
x: 0
},
out: {
opacity: 0
},
in: {
...position,
opacity: 0
}
};
return (
<AnimatePresence mode="wait" initial={initial}>
<div ref={ref} className={cn(className, isActive ? 'hidden' : 'opacity-0')} style={style}>
{children}
</div>
{isActive && (
<motion.div
variants={variants}
initial="in"
animate="inactive"
exit="out"
className={className}
style={style}
transition={{
type: 'spring',
damping,
stiffness,
restDelta: 0.01,
mass,
duration,
delay
}}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
export default Entering; |
Beta Was this translation helpful? Give feedback.
-
What package within Headless UI are you using?
@headlessui/react
What version of that package are you using?
v1.1.1
What browser are you using?
Chrome
Reproduction repository
Describe your issue
At the moment it is not possible to manually close the disclosure. I have a case where where another disclosure open, it will close the other. This is not possible currently. Thanks a lot, kudos
Beta Was this translation helpful? Give feedback.
All reactions