diff --git a/package.json b/package.json index 342a1a0..d9b407f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/ui-components-library", - "version": "3.6.0", + "version": "3.7.0", "description": "UI components library which implements the Kleros design system.", "source": "./src/lib/index.ts", "main": "./dist/index.js", diff --git a/src/lib/accordion/accordion-item.tsx b/src/lib/accordion/accordion-item.tsx index 7df4461..a0b4915 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,89 @@ 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(() => { + if (expandButton) { + return expandButton({ + expanded, + toggle: () => setExpanded(expanded ? -1 : index), + }); + } + const IconComponent = expanded ? Minus : Plus; + return ( + + ); + }, [expanded, expandButton, index, setExpanded]); + return (
{body} diff --git a/src/lib/accordion/custom.tsx b/src/lib/accordion/custom.tsx index 3a5d75c..8b96453 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 }) => ( + * + * )} + * ``` + */ + expandButton?: AccordionItemProps["expandButton"]; } -const CustomAccordion: React.FC = ({ +/** + * @description This component manages a list of collapsible accordion items, + * where only one item can be expanded at a time. + * @param props - CustomAccordionProps + * @returns JSX.Element + */ +function CustomAccordion({ items, className, defaultExpanded, + expandButton, ...props -}) => { +}: Readonly) { const [expanded, setExpanded] = useState( - !isUndefined(defaultExpanded) ? defaultExpanded : -1, + isUndefined(defaultExpanded) ? -1 : defaultExpanded, ); return (
{items.map((item, index) => ( @@ -33,12 +72,13 @@ const CustomAccordion: React.FC = ({ index={index} title={item.title} body={item.body} + expandButton={item.expandButton ?? expandButton} setExpanded={setExpanded} expanded={expanded === index} /> ))}
); -}; +} export default CustomAccordion; diff --git a/src/lib/accordion/index.tsx b/src/lib/accordion/index.tsx index 2d3ea2a..3a7aafd 100644 --- a/src/lib/accordion/index.tsx +++ b/src/lib/accordion/index.tsx @@ -2,38 +2,56 @@ import React, { ReactNode, useState } from "react"; import AccordionItem from "./accordion-item"; import { cn, isUndefined } from "../../utils"; -interface AccordionItem { +export interface AccordionItemProps { title: string; body: ReactNode; Icon?: React.FC>; icon?: ReactNode; } -interface AccordionProps { - items: AccordionItem[]; +export interface AccordionProps { + /** + * Array of accordion items. + */ + items: AccordionItemProps[]; + /** + * 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; className?: string; } -const DefaultTitle: React.FC<{ item: AccordionItem }> = ({ item }) => ( +const DefaultTitle: React.FC<{ item: AccordionItemProps }> = ({ item }) => ( <> {item.icon ?? (item.Icon && )}

{item.title}

); -const Accordion: React.FC = ({ +/** + * @description This component manages a list of collapsible accordion items, + * where only one item can be expanded at a time. + * @param props - AccordionProps + * @returns JSX.Element + */ +function Accordion({ items, defaultExpanded, className, ...props -}) => { +}: Readonly) { const [expanded, setExpanded] = useState( - !isUndefined(defaultExpanded) ? defaultExpanded : -1, + isUndefined(defaultExpanded) ? -1 : defaultExpanded, ); return (
{items.map((item, index) => ( @@ -48,6 +66,6 @@ const Accordion: React.FC = ({ ))}
); -}; +} export default Accordion; diff --git a/src/stories/accordion.stories.tsx b/src/stories/accordion.stories.tsx index a1db66b..736d440 100644 --- a/src/stories/accordion.stories.tsx +++ b/src/stories/accordion.stories.tsx @@ -3,19 +3,19 @@ import type { Meta, StoryObj } from "@storybook/react"; import { IPreviewArgs } from "./utils"; -import Accordion from "../lib/accordion/index"; +import AccordionComponent from "../lib/accordion/index"; const meta = { - component: Accordion, + component: AccordionComponent, title: "Accordion", tags: ["autodocs"], -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj & IPreviewArgs; -export const DarkTheme: Story = { +export const Accordion: Story = { args: { className: "max-w-[80dvw]", diff --git a/src/stories/custom-accordion.stories.tsx b/src/stories/custom-accordion.stories.tsx new file mode 100644 index 0000000..578ea53 --- /dev/null +++ b/src/stories/custom-accordion.stories.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { IPreviewArgs } from "./utils"; + +import CustomAccordion from "../lib/accordion/custom"; +import Button from "../lib/button/index"; + +const meta = { + component: CustomAccordion, + title: "CustomAccordion", + tags: ["autodocs"], +} satisfies Meta; + +export default meta; + +type Story = StoryObj & IPreviewArgs; + +/** CustomAccordion provides the ability to render custom title, body and expandButton. */ +export const Accordion: Story = { + args: { + className: "max-w-[80dvw]", + + items: [ + { + title: ( +
+ How it works? +
+ ), + body: ( + + {"hello\nhello\n\n\n\n\nhello"} + + ), + expandButton: ({ expanded, toggle }) => { + return expanded ? ( +