import type { HTMLAttributes, ReactElement, ReactNode } from "react"; import { Children, cloneElement, createContext, isValidElement, useContext, useState } from "react"; import { cx } from "../../utils/cx.utils"; import { Icon } from "../icons/icon"; import "./todo.component.css"; /** Identifier reported for a {@link Todo.Item}: its `id` prop, or its index among siblings when `id` is omitted. */ export type TodoItemIdentifier = string | number; interface TodoContextValue { onItemChange: (identifier: TodoItemIdentifier, checked: boolean) => void; } const TodoContext = createContext(null); /** Props accepted by {@link Todo.Item}. */ export interface TodoItemProps extends Omit, "id" | "onClick"> { /** Unique identifier for this item, reported to {@link TodoProps.onChange}. Defaults to the item's index among siblings when omitted. */ id?: string; /** Whether the item is checked when it first renders. Defaults to `false`. */ initialState?: boolean; /** Text describing the task. */ children: ReactNode; } interface TodoItemInternalProps extends TodoItemProps { index?: number; } /** * Single todo entry rendered on its own line with a round checkbox in front. * Clicking it toggles between the empty and checked state, reporting the * new state to the parent {@link Todo} via context. * * @example * ```tsx * Water the plants * ``` */ function TodoItem({ className, id, index = 0, initialState = false, children, ...rest }: TodoItemInternalProps) { const [checked, setChecked] = useState(initialState); const context = useContext(TodoContext); return ( ); } /** Props accepted by {@link Todo}. */ export interface TodoProps extends Omit, "title" | "onChange"> { /** Optional title rendered above the list of items. */ title?: ReactNode; /** Called whenever an item is toggled, with its identifier ({@link TodoItemProps.id} or index) and new checked state. */ onChange?: (identifier: TodoItemIdentifier, checked: boolean) => void; } /** * A list of checkable tasks. Composes {@link Todo.Item} for each entry; each * item renders on its own line with a round checkbox that fills in with a * check icon once marked done. Items report their state changes up to * {@link TodoProps.onChange} via context. * * @example * ```tsx * console.log(id, checked)}> * Water the plants * Feed the cat * * ``` */ export function Todo({ className, title, children, onChange, ...rest }: TodoProps) { let index = 0; const items = Children.map(children, (child) => { if (!isValidElement(child) || child.type !== TodoItem) { return child; } return cloneElement(child as ReactElement, { index: index++ }); }); return ( onChange?.(identifier, checked) }} >
{title &&
{title}
} {items}
); } Todo.Item = TodoItem;