mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [35a27c3d] UX/UI: design task-detail overhaul (#404) * [39ea1900] docs(ux_ui): add content-readability spec for markdown, collapsible sections, timestamps (#388) Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> * [71f9aec6] docs(ux_ui): add task navigation/structure design spec (#400) Adds docs/ux_ui/design/task-navigation-structure.md covering the breadcrumb trail, prev/next sibling navigation, and a distinct visual treatment for the read-only constraints section, grounded in the real task-detail components and existing amber/Lock read-only tokens. Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech> --------- Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech> * [9baa1c34] Frontend: implement task-detail overhaul (#408) * [13b6c723] Task detail: inline timestamps + breadcrumb + prev/next navigation (#390) * [13b6c723] feat(panel): add inline absolute timestamps, task breadcrumb, and prev/next list nav to task detail Adds a shared formatAbsoluteTimestamp helper used inline (with tooltip) next to relative time on progress updates and checkpoints in tab-progress.tsx, progress-timeline.tsx, and checkpoint-card.tsx. Adds TaskBreadcrumb (renders only when task.parent_task_id is set) and TaskListNav, which reads a new taskListNav context in the scroll-restoration zustand store — populated by the Tasks list page from TaskTable's live filtered/sorted order — to move to the adjacent task. When no list context exists for the session or the current task isn't part of the captured order, both nav buttons render disabled with an explanatory tooltip (the documented fallback). * [13b6c723] docs(guide): task detail navigation, timestamps, breadcrumb, and prev/next behavior --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [40acdd31] Task detail: collapsible markdown sections + distinct Constraints styling (#407) * [40acdd31] feat(panel): collapsible task-detail sections + distinct Constraints styling Wrap the Description, per-field Notes, and Plan cards in a new CollapsibleSection (Radix Collapsible + tw-animate-css fade/slide, so collapse/expand only animates opacity/transform) so a long task no longer forces continuous scrolling. Restyle the read-only Constraints card with an amber accent border, background tint, and ShieldAlert icon so it reads as distinct from authored content. Existing edit/preview toggles are force-open while active and otherwise unchanged. Adds a global prefers-reduced-motion override in globals.css. * [40acdd31] docs(panel): CollapsibleSection component API and usage guide Documents the new CollapsibleSection wrapper component used for independent collapse/expand of task-detail sections (Description, Constraints, Notes, Plan). Covers component API, controlled vs. uncontrolled state patterns, animation behavior (fade+slide, transform/opacity only), prefers-reduced-motion handling, and usage examples across task-description.tsx / tab-notes.tsx / tab-plan.tsx. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [73f8311f] fix(task-table): remove exhaustive-deps suppression on visible-order effect (#409) Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> * [eb417ef1] Fix: apply auto-collapse thresholds to Progress and Acceptance Criteria surfaces (#429) * [4e855d24] Apply content-readability-spec collapse thresholds to Progress and Acceptance Criteria surfaces (#416) * [4e855d24] feat(task-detail): auto-collapse long progress/checkpoint/AC content per readability spec * [4e855d24] refactor(task-detail): remove inline JSX section-marker comments per no-inline-comments convention * [4e855d24] docs(task-detail): document content-readability-spec collapse thresholds for CollapsibleSection --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [3c90ef34] Wire content-readability thresholds into CollapsibleSection, tab-progress, acceptance-criteria (#430) * [3c90ef34] test(task-detail): add AC4 combined readability test — 30+ progress entries + long acceptance-criteria list * [3c90ef34] docs: enhance content-readability thresholds documentation and code comments - Enhance panel/src/lib/content-readability.ts with usage examples and clarified intent - Enhance CollapsibleSection with auto-collapse logic explanation and precedence rules - Enhance TabProgress's RECENT_OPEN_COUNT logic with dual-threshold explanation - Add comprehensive architecture guide: panel/docs/CONTENT_READABILITY_THRESHOLDS.md covering thresholds, components, testing, and implementation notes The readability feature prevents long-history tasks (30+ updates, 20+ criteria) from rendering fully expanded, keeping pages navigable. Tests confirm 32 progress updates default to 2 open, and long criteria lists collapse while short ones stay expanded. --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [fc04d84a] Round-3 revision: fix 4 named gaps on task-detail overhaul, one dev leaf per fix (#455) * [cac9b603] fix(panel): fall back to task.created_at for missing written_at stamp in tab-notes.tsx (#446) Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> * [31dd4f99] Remove ArrowLeft back button from task-header.tsx (#441) * [31dd4f99] Remove ArrowLeft back button and Link wrapper from task-header.tsx, drop now-unused imports * [31dd4f99] docs(task-navigation): mark spec as implemented, clarify ArrowLeft button removal Update task-navigation-structure.md to reflect v0.21.0+ implementation: - Status changed from "proposed" to "implemented" - Clarified that ArrowLeft back button was removed from task-header.tsx - Noted that breadcrumb and prev/next navigation now provide all navigation - Constraints section styling with amber tint and ShieldAlert icon is complete - Referenced related guide documentation for task-detail-navigation features --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [75fd7444] Wire content prop into EditableNoteCard's CollapsibleSection (#449) * [75fd7444] feat(panel): wire content prop into EditableNoteCard's CollapsibleSection Pass the note field's current value into CollapsibleSection's content prop and derive EditableNoteCard's initial sectionOpen state from exceedsReadabilityThreshold, so long notes default collapsed with an expand affordance while short notes render fully expanded. * [75fd7444] docs(panel): document EditableNoteCard's content-driven collapse pattern in collapsible-section.md Updated docs/frontend/components/collapsible-section.md to reflect how EditableNoteCard in tab-notes.tsx uses both controlled mode (force-open while editing) and content-driven initialization (seed sectionOpen from content length). Added a new "Combined: controlled + content-driven initialization" example showing this pattern for future developers extending editable-content sections. Pattern: long notes default collapsed with expand affordance, short notes default expanded, edit forms always visible during editing. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [18ada610] docs(ux-ui): reconcile prev/next nav design spec with shipped list-order behavior (#453) Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> * [3dfc43a1] round-3 fixes: reconcile nav spec, Alt+Arrow shortcuts, CHANGELOG The breadcrumb section of task-navigation-structure.md now describes the shipped single-ancestor design (and drops the stale DropdownMenu claims); Alt+ArrowLeft/Right on TaskListNav mirror the visible prev/next buttons, suppressed while an editable element has focus, with tests; the user-facing CHANGELOG entry lands under Unreleased. Also reflows the round-1 content-readability-spec so the prose gate is green branch-wide. * [3dfc43a1] blank line between Unreleased and 0.22.0 sections --------- Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Renn F <rennf93@users.noreply.github.com>
300 lines
15 KiB
Markdown
300 lines
15 KiB
Markdown
# CollapsibleSection component
|
|
|
|
A reusable Card wrapper that enables independent collapse/expand of section content, allowing users to navigate long pages (like task-detail) without forcing continuous scrolling. Collapse/expand animations use only opacity and transform (no height/width), respecting prefers-reduced-motion globally.
|
|
|
|
## Purpose
|
|
|
|
When task-detail pages carry long descriptions, many notes, and detailed plans, users must scroll through all expanded content to reach later sections. `CollapsibleSection` wraps each logical section (Description, Constraints, Notes fields, Plan subsections) in a collapsible card so users can fold away irrelevant content and jump to what they need. The component supports both **controlled** (e.g., force-open while editing) and **uncontrolled** (stateless) modes.
|
|
|
|
## Files
|
|
|
|
| File | Role |
|
|
|------|------|
|
|
| `panel/src/components/tasks/task-detail/collapsible-section.tsx` | Component definition, `CollapsibleSectionProps` interface, state management. |
|
|
| `panel/src/components/tasks/task-detail/task-description.tsx` | Description and Constraints sections wrapped. Constraints styled with amber accent border/background + ShieldAlert icon for visual distinction. |
|
|
| `panel/src/components/tasks/task-detail/tab-notes.tsx` | Each editable note field (Description, Notes, Plan) wrapped. Edit/preview toggle is force-open while editing. |
|
|
| `panel/src/components/tasks/task-detail/tab-plan.tsx` | Approach, Sub-Tasks, Technical Considerations, Risks, and Open Questions sections wrapped. |
|
|
| `panel/src/app/globals.css` | Global `prefers-reduced-motion: reduce` override that disables all animations/transitions for users with reduced-motion enabled. |
|
|
|
|
## API
|
|
|
|
### `CollapsibleSectionProps`
|
|
|
|
```typescript
|
|
interface CollapsibleSectionProps {
|
|
/** Card title content (icon + text + badges as needed) */
|
|
title: ReactNode;
|
|
|
|
/** Right-aligned header controls (edit/preview toggles, buttons) — always visible */
|
|
actions?: ReactNode;
|
|
|
|
/** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */
|
|
open?: boolean;
|
|
|
|
/**
|
|
* Whether the (uncontrolled) section starts expanded. Takes precedence
|
|
* over `content`-derived collapsing. Omit to let `content` decide, or to
|
|
* default open when neither is given (so nothing visible today disappears).
|
|
*/
|
|
defaultOpen?: boolean;
|
|
|
|
/**
|
|
* Plain-text representation of the section's body, used to derive
|
|
* `defaultOpen` per the content-readability spec (~10 lines / ~640 chars)
|
|
* when `defaultOpen` is not explicitly set. Ignored otherwise.
|
|
*/
|
|
content?: string;
|
|
|
|
/** Callback when the user toggles the section open/closed. */
|
|
onOpenChange?: (open: boolean) => void;
|
|
|
|
/** Tailwind class string applied to the outer Card element. */
|
|
className?: string;
|
|
|
|
/** Tailwind class string applied to the CardHeader (title + actions row). */
|
|
headerClassName?: string;
|
|
|
|
/** Content rendered inside CardContent when the section is open. */
|
|
children: ReactNode;
|
|
}
|
|
```
|
|
|
|
### Component behavior
|
|
|
|
- **Uncontrolled mode** (omit `open` prop): component manages its own open state. `defaultOpen` determines initial state; if `defaultOpen` is omitted, the component uses `content`-derived collapsing (if `content` is provided), or defaults to `true` if neither is set. `onOpenChange` is called when the user clicks the toggle; internal state updates automatically.
|
|
- **Controlled mode** (`open` prop set): `onOpenChange` is called on toggle, but internal state is not updated; parent must update the `open` prop. Useful to force a section open while a user is editing (e.g., `open={isEditing || sectionOpen}`).
|
|
- **Content-driven defaultOpen** (new): when `content` is provided without an explicit `defaultOpen`, the component checks if the content exceeds the readability thresholds (~10 lines / ~640 characters, per `content-readability.ts`). If it does, the section defaults collapsed; otherwise, it defaults open. This keeps long lists/sections from forcing continuous scrolling. An explicit `defaultOpen` prop always takes precedence over this logic, maintaining backward compatibility with existing callers.
|
|
- **Title and actions**: title is always visible in the header; actions (right side) are also always visible, never collapsed away. This allows edit/preview toggles, save/cancel buttons, etc. to remain accessible.
|
|
- **ChevronDown icon**: rotates -90° when closed, 0° when open. Uses `transition-transform duration-200` so the rotation animates smoothly.
|
|
|
|
### Animation
|
|
|
|
Collapse/expand uses fade + slide from Tailwind CSS's `tw-animate-css` utilities:
|
|
|
|
```tsx
|
|
"duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
"data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
|
|
```
|
|
|
|
- **Duration**: 200ms
|
|
- **Animation type**: fade (opacity) + slide (translateY), both controlled via transform/opacity CSS properties only — no height/width animation, so layout does not reflow mid-animation.
|
|
- **Accessibility**: `prefers-reduced-motion: reduce` is handled globally in `panel/src/app/globals.css`, which sets `animation-duration` and `transition-duration` to 0.01ms for all elements when the user has enabled reduced motion in their OS settings. The section content still opens/closes; it just doesn't animate.
|
|
|
|
## How to use
|
|
|
|
Wrap any section content that should be collapsible:
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { CollapsibleSection } from "./collapsible-section";
|
|
import { FileText, Edit3 } from "lucide-react";
|
|
|
|
export function MySection() {
|
|
const [sectionOpen, setSectionOpen] = useState(true);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const sectionText = "Section content here."; // Plain-text representation
|
|
|
|
return (
|
|
<CollapsibleSection
|
|
title={
|
|
<>
|
|
<FileText className="h-5 w-5" />
|
|
Section Title
|
|
</>
|
|
}
|
|
actions={
|
|
<Button size="sm" variant="ghost" onClick={() => setIsEditing(true)}>
|
|
<Edit3 className="h-4 w-4 mr-1" />
|
|
Edit
|
|
</Button>
|
|
}
|
|
content={sectionText} // Optional: drive defaultOpen based on content length
|
|
open={isEditing || sectionOpen}
|
|
onOpenChange={setSectionOpen}
|
|
>
|
|
<p>{sectionText}</p>
|
|
</CollapsibleSection>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Using content-driven defaultOpen
|
|
|
|
To automatically collapse long sections without explicit `defaultOpen`:
|
|
|
|
```tsx
|
|
const listText = items.map(item => item.title).join("\n");
|
|
|
|
<CollapsibleSection
|
|
title="Long List"
|
|
content={listText} // Checked against ~10 lines / ~640 chars thresholds
|
|
>
|
|
<ul>
|
|
{items.map(item => (
|
|
<li key={item.id}>{item.title}</li>
|
|
))}
|
|
</ul>
|
|
</CollapsibleSection>
|
|
```
|
|
|
|
If `listText` exceeds the readability thresholds, the section defaults collapsed; otherwise, it defaults open. No explicit `defaultOpen` prop needed.
|
|
|
|
### Controlled vs. uncontrolled
|
|
|
|
**Uncontrolled (simple case):**
|
|
|
|
```tsx
|
|
<CollapsibleSection title="Notes" defaultOpen={true}>
|
|
<p>Your notes content.</p>
|
|
</CollapsibleSection>
|
|
```
|
|
|
|
The component manages open state internally. `onOpenChange` is optional; if provided, it's called for logging/debugging, but state still updates automatically.
|
|
|
|
**Controlled (e.g., force-open while editing):**
|
|
|
|
```tsx
|
|
const [sectionOpen, setSectionOpen] = useState(true);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
<CollapsibleSection
|
|
title="Notes"
|
|
open={isEditing || sectionOpen}
|
|
onOpenChange={setSectionOpen}
|
|
>
|
|
{isEditing ? <textarea /> : <p>Rendered content.</p>}
|
|
</CollapsibleSection>
|
|
```
|
|
|
|
When `isEditing` is `true`, the section is forced open even if the user clicked to close it. This prevents an edit form from being hidden mid-interaction.
|
|
|
|
### Combined: controlled mode + content-driven initialization
|
|
|
|
For components that edit long content (like task notes), seed the initial collapsed/expanded state from content length, but use controlled mode to force-open during edit:
|
|
|
|
```tsx
|
|
const currentValue = task.dev_notes;
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [sectionOpen, setSectionOpen] = useState(() =>
|
|
!exceedsReadabilityThreshold(currentValue ?? ""),
|
|
);
|
|
|
|
<CollapsibleSection
|
|
title="Developer Notes"
|
|
content={currentValue ?? undefined} // Drives content-readability check
|
|
open={isEditing || sectionOpen} // Controlled: force-open while editing
|
|
onOpenChange={setSectionOpen}
|
|
>
|
|
{isEditing ? <textarea value={currentValue} /> : <p>{currentValue}</p>}
|
|
</CollapsibleSection>
|
|
```
|
|
|
|
This pattern (used in `tab-notes.tsx`'s `EditableNoteCard`) ensures:
|
|
- Long notes default collapsed with an expand affordance
|
|
- Short notes default expanded (fully visible)
|
|
- Edit forms are always visible when editing, even if the user had collapsed the section
|
|
- User's collapse/expand choice persists across edit cycles (via `sectionOpen` state)
|
|
|
|
## Used in
|
|
|
|
The component is now applied across task-detail pages:
|
|
|
|
| Page / Component | Sections wrapped | Notes |
|
|
|---|---|---|
|
|
| `task-description.tsx` | Description, Constraints | Constraints section styled with amber border/background + ShieldAlert icon for visual distinction from authored content. |
|
|
| `tab-notes.tsx` | Each editable note field (dev_notes, qa_notes, doc_notes, etc.) | `EditableNoteCard` seeds initial `sectionOpen` from content length via `exceedsReadabilityThreshold`, passes `content={currentValue}` to CollapsibleSection, and forces open while editing via controlled `open={isEditing \|\| sectionOpen}`. Long notes default collapsed with expand affordance; short notes default expanded. |
|
|
| `tab-plan.tsx` | Approach, Sub-Tasks, Technical Considerations, Risks, Open Questions | Each sub-section independently collapsible. |
|
|
| `acceptance-criteria.tsx` | Full acceptance criteria list | Wrapped in CollapsibleSection with `content={criteriaText}`, so a long AC list defaults collapsed per content-readability spec. Forced open while adding/editing via controlled `open` prop. |
|
|
| `tab-progress.tsx` | Individual progress updates and checkpoints (via internal Radix Collapsible wrapper) | Each entry wrapped in a collapsible section; only the 2 most recent entries default open (gated by content length as well). Older entries default collapsed even if short, keeping task detail navigable without endless scrolling. |
|
|
|
|
## Design decisions
|
|
|
|
- **Content-driven defaultOpen (new)**: instead of always defaulting open (which forced tasks with long histories to be fully expanded), the component now checks content length against readability thresholds (~10 lines / ~640 characters) when `defaultOpen` is not explicitly set. Long content defaults collapsed, keeping task-detail pages navigable. The explicit `defaultOpen` prop always takes precedence, so existing callers (task-description, tab-notes, tab-plan) that pass `open={...}` are unaffected — the content-length check only applies to uncontrolled sections. This is the "content-readability spec" driving progress and AC collapse in tab-progress and acceptance-criteria.
|
|
|
|
- **Fade + slide animation only**: opacity and transform are GPU-accelerated and don't trigger layout reflow. Height/width animations are avoided because they force the browser to recalculate layout mid-animation, causing jank on slower devices and making the motion distracting.
|
|
|
|
- **Controlled + uncontrolled modes**: uncontrolled is the default for simple read-only sections (no extra state management needed), while controlled mode (via `open` prop) lets parent components force a section open during edit (the common case for tab-notes EditableNoteCard).
|
|
|
|
- **Always-visible actions**: the actions slot (buttons, toggles) is never collapsed, so users can always edit, delete, or perform actions on a section without expanding it first.
|
|
|
|
- **ChevronDown icon rotates, not replaces**: using a rotating icon is more intuitive and uses less real estate than swapping between two different icons (Chevron-Down vs. Chevron-Up).
|
|
|
|
- **Global prefers-reduced-motion override**: instead of checking `prefers-reduced-motion` in JavaScript (which is error-prone and scattered across components), a single global CSS rule ensures that **all** animations and transitions respect the user's OS setting. No component logic needed.
|
|
|
|
- **`CardTitle` inside the trigger**: the title is inside a styled `<button>` (the CollapsibleTrigger) so it's keyboard-accessible and screenreader-labeled. The button is full-width (flex-1) and text-left, so users can click anywhere on the title to toggle.
|
|
|
|
## Testing
|
|
|
|
The test suite covers:
|
|
|
|
- Rendering a section with title, children, and optional actions.
|
|
- Toggling the section open/closed on click.
|
|
- Checking `aria-expanded` attribute on the trigger button.
|
|
- Verifying that content is hidden when closed (not just visually; `CollapsibleContent` removes it from the DOM).
|
|
- Controlled vs. uncontrolled state management.
|
|
- Animation classes applied to `CollapsibleContent` based on open state.
|
|
- ChevronDown icon rotation (CSS class toggling).
|
|
- Always-visible actions slot (buttons not collapsed away).
|
|
|
|
See `panel/src/components/tasks/task-detail/__tests__/task-description.test.tsx` for integration tests covering:
|
|
- Independent collapse of Description and Constraints sections.
|
|
- Controlled open state while editing.
|
|
- Edit/preview toggle working alongside collapse behavior.
|
|
|
|
## Related work
|
|
|
|
- **Task detail: collapsible markdown sections + distinct Constraints styling** — this commit. Introduces `CollapsibleSection` and applies it to task-description.tsx, tab-notes.tsx, tab-plan.tsx. Constraints section gets distinct amber styling.
|
|
|
|
## Migration / rollout
|
|
|
|
For developers adding a new collapsible section to task-detail or other pages:
|
|
|
|
1. Import `CollapsibleSection` from `./collapsible-section` (adjust path as needed).
|
|
2. Wrap the section content, provide a `title` (can include icons, badges).
|
|
3. Optionally provide `actions` (buttons, toggles that should stay visible).
|
|
4. **For sections with potentially long content** (lists, histories): pass a plain-text `content` prop so the section automatically collapses if the content is long. This defers to the content-readability spec thresholds (~10 lines / ~640 chars).
|
|
5. For editable content, use the controlled pattern: `open={isEditing || sectionOpen}` to force-open while editing. The controlled `open` prop takes precedence over content-driven collapse.
|
|
6. No extra state management is needed for read-only sections; the component handles it internally.
|
|
|
|
Example (uncontrolled with content-driven collapse):
|
|
|
|
```tsx
|
|
const listText = items.map(item => item.name).join("\n");
|
|
|
|
<CollapsibleSection
|
|
title="My List"
|
|
content={listText} // Drives defaultOpen based on length
|
|
>
|
|
<ul>
|
|
{items.map(item => (
|
|
<li key={item.id}>{item.name}</li>
|
|
))}
|
|
</ul>
|
|
</CollapsibleSection>
|
|
```
|
|
|
|
Example (controlled, forcing open during edit):
|
|
|
|
```tsx
|
|
const [sectionOpen, setSectionOpen] = useState(true);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
<CollapsibleSection
|
|
title="My Section"
|
|
open={isEditing || sectionOpen} // Controlled: explicit `open` wins over content
|
|
onOpenChange={setSectionOpen}
|
|
>
|
|
{isEditing ? <textarea /> : <p>Content here.</p>}
|
|
</CollapsibleSection>
|
|
```
|
|
|
|
## Accessibility
|
|
|
|
- **`aria-expanded` attribute** on the trigger button communicates the open/closed state to screenreaders.
|
|
- **Keyboard support**: the trigger is a `<button>`, so it's focusable with Tab and activatable with Enter/Space.
|
|
- **`aria-hidden="true"` on the ChevronDown icon**: the icon is decorative; screenreaders skip it.
|
|
- **`prefers-reduced-motion` support**: animations are disabled globally for users with motion sensitivity, but the section still opens/closes (the content is not hidden, just instant).
|