From 1bd9a4c535ed84f886db4dcbb670db694eb54215 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 11 Dec 2025 17:19:18 +0530 Subject: [PATCH 1/3] feat: custom-accordion-expand-button --- src/lib/accordion/accordion-item.tsx | 77 +++++++++--- src/lib/accordion/custom.tsx | 64 ++++++++-- src/lib/accordion/index.tsx | 34 ++++-- src/stories/accordion.stories.tsx | 8 +- src/stories/custom-accordion.stories.tsx | 148 +++++++++++++++++++++++ 5 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 src/stories/custom-accordion.stories.tsx diff --git a/src/lib/accordion/accordion-item.tsx b/src/lib/accordion/accordion-item.tsx index 7df4461..1b30424 100644 --- a/src/lib/accordion/accordion-item.tsx +++ b/src/lib/accordion/accordion-item.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode, useMemo } from "react"; import { useElementSize } from "../../hooks/useElementSize"; import Plus from "../../assets/svgs/accordion/plus.svg"; import Minus from "../../assets/svgs/accordion/minus.svg"; @@ -6,51 +6,94 @@ import Minus from "../../assets/svgs/accordion/minus.svg"; import { Button } from "react-aria-components"; import { cn } from "../../utils"; -interface AccordionItemProps { +export interface AccordionItemProps { setExpanded: React.Dispatch>; index: number; + /** + * The title displayed in the accordion header. + * + * This is usually text, but can be any ReactNode. + */ title: ReactNode; + /** + * The body/content of the accordion section. + * This is only visible when the item is expanded. + */ body: ReactNode; - expanded?: boolean; + /** + * Custom render function for the expand/collapse button. + * + * This function receives: + * - `expanded`: boolean => whether the item is currently expanded + * - `toggle`: function => expands/collapses this item when called + * + * Example: + * ``` + * expandButton={({ expanded, toggle }) => ( + * + * )} + * ``` + * + * If not provided, a default + or − icon will be shown. + * + * This button does NOT need to manage its own state — calling `toggle()` + * properly expands/collapses the item. + */ + expandButton?: (options: { + expanded: boolean; + toggle: () => void; + }) => ReactNode; + expanded: boolean; } const AccordionItem: React.FC = ({ title, body, + expandButton, index, expanded, setExpanded, }) => { const [ref, { height }] = useElementSize(); + const ExpandButton = useMemo( + () => + expandButton ? ( + expandButton({ + expanded, + toggle: () => setExpanded(expanded ? -1 : index), + }) + ) : expanded ? ( + + ) : ( + + ), + [expanded, expandButton, index, setExpanded], + ); return (
{body} diff --git a/src/lib/accordion/custom.tsx b/src/lib/accordion/custom.tsx index 3a5d75c..88c8b47 100644 --- a/src/lib/accordion/custom.tsx +++ b/src/lib/accordion/custom.tsx @@ -1,30 +1,69 @@ -import React, { ReactNode, useState } from "react"; -import AccordionItem from "./accordion-item"; +import React, { useState } from "react"; +import AccordionItem, { AccordionItemProps } from "./accordion-item"; import { cn, isUndefined } from "../../utils"; -interface AccordionItem { - title: ReactNode; - body: ReactNode; -} +export interface CustomAccordionProps { + /** + * Array of accordion items. + * + * Each item can optionally define its own `expandButton`. + * If omitted, the parent-level `expandButton` (if provided) is used. + */ -interface AccordionProps { - items: AccordionItem[]; + items: Pick[]; className?: string; + + /** + * Index of the item to expand by default. + * + * - Set to a number (0-based index) to expand an item on mount + * - Leave undefined to start with all items collapsed + */ + defaultExpanded?: number; + /** + * A global expand/collapse button renderer applied to every item + * **unless that item provides its own expandButton**. + * + * Signature: + * ``` + * expandButton?: ({ expanded, toggle }) => ReactNode; + * ``` + * + * Example: + * ``` + * expandButton={({ expanded, toggle }) => ( + * * )} * ``` */ @@ -56,7 +56,7 @@ function CustomAccordion({ ...props }: Readonly) { const [expanded, setExpanded] = useState( - !isUndefined(defaultExpanded) ? defaultExpanded : -1, + isUndefined(defaultExpanded) ? -1 : defaultExpanded, ); return (
) { const [expanded, setExpanded] = useState( - !isUndefined(defaultExpanded) ? defaultExpanded : -1, + isUndefined(defaultExpanded) ? -1 : defaultExpanded, ); return (
{ + expandButton: ({ expanded, toggle }) => { return expanded ? ( -