diff --git a/packages/propel/package.json b/packages/propel/package.json index 15933f2af5a..c9e1cf1c192 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -20,6 +20,7 @@ "./avatar": "./dist/avatar/index.js", "./card": "./dist/card/index.js", "./charts/*": "./dist/charts/*/index.js", + "./collapsible": "./dist/collapsible/index.js", "./combobox": "./dist/combobox/index.js", "./command": "./dist/command/index.js", "./dialog": "./dist/dialog/index.js", diff --git a/packages/propel/src/collapsible/collapsible.tsx b/packages/propel/src/collapsible/collapsible.tsx new file mode 100644 index 00000000000..ec40775156f --- /dev/null +++ b/packages/propel/src/collapsible/collapsible.tsx @@ -0,0 +1,100 @@ +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; +}; + +type ContentProps = { + children: React.ReactNode; + className?: string; +}; + +// Context +const CollapsibleContext = createContext(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 +const Root: React.FC = ({ children, className, isOpen: controlledIsOpen, onToggle, defaultOpen }) => { + const [localIsOpen, setLocalIsOpen] = useState(controlledIsOpen || defaultOpen || false); + + useEffect(() => { + if (controlledIsOpen !== undefined) { + setLocalIsOpen(controlledIsOpen); + } + }, [controlledIsOpen]); + + const handleToggle = useCallback(() => { + if (controlledIsOpen !== undefined) { + onToggle?.(); + } else { + setLocalIsOpen((prev) => !prev); + } + }, [controlledIsOpen, onToggle]); + + return ( + + + {children} + + + ); +}; + +const Trigger: React.FC = ({ children, className, buttonRef }) => { + const { isOpen } = useCollapsible(); + + return ( + + {children} + + ); +}; + +const Content: React.FC = ({ children, className }) => ( + + {children} + +); + +// Compound Component +export const Collapsible = { + CollapsibleRoot: Root, + CollapsibleTrigger: Trigger, + CollapsibleContent: Content, +}; diff --git a/packages/propel/src/collapsible/index.ts b/packages/propel/src/collapsible/index.ts new file mode 100644 index 00000000000..dbd92623720 --- /dev/null +++ b/packages/propel/src/collapsible/index.ts @@ -0,0 +1 @@ +export * from "./collapsible"; diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index 8f23c301cb8..05ac15adeb2 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ "src/avatar/index.ts", "src/card/index.ts", "src/charts/*/index.ts", + "src/collapsible/index.ts", "src/combobox/index.ts", "src/command/index.ts", "src/dialog/index.ts",