UNPKG

@loke/ui

Version:
205 lines (159 loc) 6.58 kB
--- name: collapsible type: core domain: navigation requires: [loke-ui] description: > Single collapsible section with Collapsible/CollapsibleTrigger/CollapsibleContent. CSS variable animation via --loke-collapsible-content-height and --loke-collapsible-content-width (measured via ResizeObserver-style layout effect). forceMount for exit animations. Controlled via open/onOpenChange. For multiple coordinated sections, use Accordion (which wraps Collapsible internally). --- # Collapsible ## Setup A collapsible section with a trigger button and hidden content. ```tsx import { Collapsible, CollapsibleTrigger, CollapsibleContent, } from "@loke/ui/collapsible"; function FilterPanel() { return ( <Collapsible defaultOpen> <CollapsibleTrigger>Advanced filters</CollapsibleTrigger> <CollapsibleContent> <div> <label> <input type="checkbox" /> Include archived </label> <label> <input type="checkbox" /> Show drafts </label> </div> </CollapsibleContent> </Collapsible> ); } ``` `CollapsibleTrigger` renders a `<button>` with `aria-expanded` and `aria-controls` wired automatically. ## Core Patterns ### CSS variable animation `CollapsibleContent` measures the real content dimensions before each open/close transition and exposes them as CSS custom properties. Use `--loke-collapsible-content-height` for smooth height animations — never use a fixed `max-height`. ```css .collapsible-content { overflow: hidden; } .collapsible-content[data-state="open"] { animation: collapsible-open 200ms ease-out; } .collapsible-content[data-state="closed"] { animation: collapsible-close 200ms ease-in; } @keyframes collapsible-open { from { height: 0; } to { height: var(--loke-collapsible-content-height); } } @keyframes collapsible-close { from { height: var(--loke-collapsible-content-height); } to { height: 0; } } ``` `data-state` is `"open"` or `"closed"` on both `Collapsible` and `CollapsibleContent`. ```tsx <Collapsible> <CollapsibleTrigger className="collapsible-trigger"> Show more </CollapsibleTrigger> <CollapsibleContent className="collapsible-content"> Additional content here. </CollapsibleContent> </Collapsible> ``` Source: `src/components/collapsible/collapsible.tsx` — `CollapsibleContentImpl` measures `getBoundingClientRect()` and sets CSS variables in a layout effect. ### Exit animations with `forceMount` By default `CollapsibleContent` unmounts when closed, cutting off any exit animation. Use `forceMount` to keep the content in the DOM through the close animation. The component tracks `isPresent` internally via `Presence`. ```tsx <Collapsible> <CollapsibleTrigger>Toggle</CollapsibleTrigger> <CollapsibleContent forceMount className="collapsible-content"> This content stays mounted for exit animations. </CollapsibleContent> </Collapsible> ``` Source: `src/components/collapsible/collapsible.tsx` — `Presence` with `forceMount || context.open`. ### Controlled state ```tsx const [open, setOpen] = useState(false); <Collapsible open={open} onOpenChange={setOpen}> <div style={{ display: "flex", justifyContent: "space-between" }}> <span>Repositories</span> <CollapsibleTrigger> {open ? "Hide" : "Show"} more </CollapsibleTrigger> </div> <CollapsibleContent> <ul> <li>repo-one</li> <li>repo-two</li> </ul> </CollapsibleContent> </Collapsible> ``` ### Disabled state ```tsx <Collapsible disabled> <CollapsibleTrigger>Locked section</CollapsibleTrigger> <CollapsibleContent>Cannot be toggled.</CollapsibleContent> </Collapsible> ``` `data-disabled=""` is set on both `Collapsible` and `CollapsibleContent` when disabled. ## Common Mistakes ### Using Collapsible for multiple expandable sections — use Accordion instead `Collapsible` manages a single open/closed state. If you need multiple expandable sections — especially with single-selection (one open at a time) — use `Accordion`. Accordion wraps Collapsible internally and adds coordinated state, keyboard navigation (Home/End/Arrow), and grouped ARIA semantics. ```tsx // Wrong — manual coordination of multiple Collapsibles <Collapsible open={openA} onOpenChange={setOpenA}>...</Collapsible> <Collapsible open={openB} onOpenChange={setOpenB}>...</Collapsible> // Correct — use Accordion for coordinated sections import { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from "@loke/ui/accordion"; <Accordion type="single" collapsible> <AccordionItem value="a"> <AccordionHeader><AccordionTrigger>Section A</AccordionTrigger></AccordionHeader> <AccordionContent>Content A</AccordionContent> </AccordionItem> <AccordionItem value="b"> <AccordionHeader><AccordionTrigger>Section B</AccordionTrigger></AccordionHeader> <AccordionContent>Content B</AccordionContent> </AccordionItem> </Accordion> ``` Source: `src/components/accordion/accordion.tsx` — Accordion wraps Collapsible with coordinated state. ### Hardcoded `max-height` for animation — janky transitions The component measures actual content height at runtime. A fixed `max-height` value won't match the content and produces uneven easing. ```css /* Wrong */ .collapsible-content[data-state="open"] { max-height: 300px; transition: max-height 200ms; } /* Correct */ .collapsible-content[data-state="open"] { height: var(--loke-collapsible-content-height); } ``` Source: `src/components/collapsible/collapsible.tsx` — `--loke-collapsible-content-height` set from `getBoundingClientRect().height`. ### Expecting open animation on initial render with `defaultOpen={true}` `CollapsibleContent` suppresses mount animation via `isMountAnimationPreventedRef`. When the component first renders with `defaultOpen={true}`, content appears instantly without animating in — this is intentional to avoid animation on page load. ```tsx // Content appears instantly on first render — this is expected <Collapsible defaultOpen> <CollapsibleTrigger>Section</CollapsibleTrigger> <CollapsibleContent>Initially visible, no entry animation.</CollapsibleContent> </Collapsible> ``` Source: `src/components/collapsible/collapsible.tsx` — `isMountAnimationPreventedRef` cleared after first `requestAnimationFrame`. ## Cross-references - **Accordion** (`@loke/ui/accordion`) — wraps Collapsible; use for multiple coordinated sections - **Choosing the Right Component** — Collapsible vs Accordion decision guidance