useDisclosure.ts•2.66 kB
import { useCallback, useEffect, useId, useRef, useState } from "react";
export interface OpenChangeEvent {
open: boolean;
}
export interface UseDisclosureProps {
isOpen?: boolean;
defaultIsOpen?: boolean;
onClose?(): void;
onOpen?(): void;
onOpenChange?(e: OpenChangeEvent): void;
id?: string;
}
export type WithDisclosure<T> = UseDisclosureProps & T;
type HTMLProps = React.HTMLAttributes<HTMLElement>;
export function useCallbackRef<T extends (...args: any[]) => any>(
callback: T | undefined,
deps: React.DependencyList = [],
) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(((...args) => callbackRef.current?.(...args)) as T, deps);
}
export function useDisclosure(props: UseDisclosureProps = {}) {
const {
onClose: onCloseProp,
onOpen: onOpenProp,
isOpen: isOpenProp,
onOpenChange: onOpenChangeProp,
id: idProp,
} = props;
const handleOpen = useCallbackRef(onOpenProp);
const handleClose = useCallbackRef(onCloseProp);
const handleOpenChange = useCallbackRef(onOpenChangeProp);
const [isOpenState, setIsOpen] = useState(props.defaultIsOpen || false);
const isOpen = isOpenProp !== undefined ? isOpenProp : isOpenState;
const isControlled = isOpenProp !== undefined;
const uid = useId();
const id = idProp ?? `disclosure-${uid}`;
const onClose = useCallback(() => {
if (!isControlled) {
setIsOpen(false);
}
handleClose?.();
}, [isControlled, handleClose]);
const onOpen = useCallback(() => {
if (!isControlled) {
setIsOpen(true);
}
handleOpen?.();
}, [isControlled, handleOpen]);
const onToggle = useCallback(() => {
if (isOpen) {
onClose();
} else {
onOpen();
}
}, [isOpen, onOpen, onClose]);
const onOpenChange = useCallback(
(event: OpenChangeEvent) => {
if (event.open) {
onOpen();
} else {
onClose();
}
handleOpenChange?.(event);
},
[onOpen, onClose, handleOpenChange],
);
function getButtonProps(props: HTMLProps = {}): HTMLProps {
return {
...props,
"aria-expanded": isOpen,
"aria-controls": id,
onClick(event) {
props.onClick?.(event);
onToggle();
},
};
}
function getDisclosureProps(props: HTMLProps = {}): HTMLProps {
return {
...props,
hidden: !isOpen,
id,
};
}
return {
isOpen,
onOpen,
onClose,
onOpenChange,
onToggle,
isControlled,
getButtonProps,
getDisclosureProps,
};
}