109 lines
2.8 KiB
TypeScript
109 lines
2.8 KiB
TypeScript
/**
|
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
* See the LICENSE file for details.
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback, createContext, useContext } from "react";
|
|
import { Collapsible as BaseCollapsible } from "@base-ui-components/react/collapsible";
|
|
import clsx from "clsx";
|
|
|
|
// Types
|
|
type CollapsibleContextType = {
|
|
isOpen: boolean;
|
|
onToggle: () => void;
|
|
};
|
|
|
|
type RootProps = {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
isOpen?: boolean;
|
|
onToggle?: () => void;
|
|
defaultOpen?: boolean;
|
|
};
|
|
|
|
type TriggerProps = {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
buttonRef?: React.RefObject<HTMLButtonElement>;
|
|
};
|
|
|
|
type ContentProps = {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
};
|
|
|
|
// Context
|
|
const CollapsibleContext = createContext<CollapsibleContextType | undefined>(undefined);
|
|
|
|
// Hook
|
|
const useCollapsible = () => {
|
|
const context = useContext(CollapsibleContext);
|
|
if (!context) {
|
|
throw new Error("Collapsible compound components cannot be rendered outside the Collapsible component");
|
|
}
|
|
return context;
|
|
};
|
|
|
|
// Components
|
|
function Root({ children, className, isOpen: controlledIsOpen, onToggle, defaultOpen }: RootProps) {
|
|
const [localIsOpen, setLocalIsOpen] = useState<boolean>(controlledIsOpen || defaultOpen || false);
|
|
|
|
useEffect(() => {
|
|
if (controlledIsOpen !== undefined) {
|
|
setLocalIsOpen(controlledIsOpen);
|
|
}
|
|
}, [controlledIsOpen]);
|
|
|
|
const handleToggle = useCallback(() => {
|
|
if (controlledIsOpen !== undefined) {
|
|
onToggle?.();
|
|
} else {
|
|
setLocalIsOpen((prev) => !prev);
|
|
}
|
|
}, [controlledIsOpen, onToggle]);
|
|
|
|
return (
|
|
<CollapsibleContext.Provider value={{ isOpen: localIsOpen, onToggle: handleToggle }}>
|
|
<BaseCollapsible.Root
|
|
className={clsx(className)}
|
|
defaultOpen={defaultOpen}
|
|
open={localIsOpen}
|
|
onOpenChange={handleToggle}
|
|
>
|
|
{children}
|
|
</BaseCollapsible.Root>
|
|
</CollapsibleContext.Provider>
|
|
);
|
|
}
|
|
|
|
function Trigger({ children, className, buttonRef }: TriggerProps) {
|
|
const { isOpen } = useCollapsible();
|
|
|
|
return (
|
|
<BaseCollapsible.Trigger data-panel-open={isOpen} ref={buttonRef} className={className}>
|
|
{children}
|
|
</BaseCollapsible.Trigger>
|
|
);
|
|
}
|
|
|
|
function Content({ children, className }: ContentProps) {
|
|
return (
|
|
<BaseCollapsible.Panel
|
|
className={clsx(
|
|
"flex h-[var(--collapsible-panel-height)] flex-col overflow-hidden text-13 transition-all ease-out data-[ending-style]:h-0 data-[starting-style]:h-0",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</BaseCollapsible.Panel>
|
|
);
|
|
}
|
|
|
|
// Compound Component
|
|
export const Collapsible = {
|
|
CollapsibleRoot: Root,
|
|
CollapsibleTrigger: Trigger,
|
|
CollapsibleContent: Content,
|
|
};
|