{"version":3,"file":"EditableCell.cjs","names":[],"sources":["../../../src/components/DataTable/EditableCell.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — an editable cell has to be announced\n * as well as rendered: columnLabel and rowNumber build the accessible name, labels\n * holds the button copy, and error/errorId wire the message to the input. The rest\n * is the edit lifecycle (editing, refocus, saving, onOpen, onCommit, onCancel) that\n * DataTable drives from outside.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport type { CellCommitMove, DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\ninterface CellEditorProps {\n    /** Text the editor opens with. */\n    initial: string;\n    /** `<input type>` for the editor. */\n    type: string;\n    /** Accessible name. */\n    label: string;\n    /** Id of the message element, when there is one. */\n    describedBy?: string;\n    /** Whether the current draft was rejected by validation. */\n    invalid: boolean;\n    /** Commit the draft. The parent decides whether the editor closes. */\n    onCommit: (raw: string, move: CellCommitMove) => void;\n    /** Discard the draft. */\n    onCancel: () => void;\n}\n\n/**\n * The `<input>` half of an editable cell.\n *\n * A separate component so it **mounts fresh** every time a cell opens: the draft\n * then starts from the current value through `useState`'s initialiser, with no\n * effect syncing props into state and no render where the draft is stale.\n *\n * `Tab` is intercepted rather than left to the browser. Default tab order would\n * walk into the next row's trigger button, which is one keystroke away from the\n * next *editor* — and in a table the reader is editing, moving cell to cell is the\n * expected behaviour. `Escape` discards; `Enter` commits and closes; blur commits,\n * because clicking away from a half-typed cell and losing the typing is a data-loss\n * bug users report as \"the table ate my edit\".\n *\n * `autoFocus` is safe here in a way it is not on page load: the editor only exists\n * because the user just clicked the cell or pressed `Tab` into it, so focus is\n * following the interaction rather than stealing it.\n *\n * The `settled` flag stops a keyboard commit from being repeated by the blur that\n * follows it. Typing clears the flag again, because after a commit the parent\n * rejected — the only case where the editor is still open and focused — the draft is\n * live once more, and dropping *that* edit on blur would be the same data loss the\n * blur-commits rule exists to prevent.\n */\nfunction CellEditor({\n    initial,\n    type,\n    label,\n    describedBy,\n    invalid,\n    onCommit,\n    onCancel,\n}: CellEditorProps) {\n    const [draft, setDraft] = useState<string>(initial);\n    const settled = useRef(false);\n\n    function finish(move: CellCommitMove): void {\n        settled.current = true;\n        onCommit(draft, move);\n    }\n\n    function handleKeyDown(event: KeyboardEvent<HTMLInputElement>): void {\n        if (event.key === \"Enter\") {\n            event.preventDefault();\n            finish(\"none\");\n            return;\n        }\n        if (event.key === \"Escape\") {\n            event.preventDefault();\n            settled.current = true;\n            onCancel();\n            return;\n        }\n        if (event.key === \"Tab\") {\n            event.preventDefault();\n            finish(event.shiftKey ? \"previous\" : \"next\");\n        }\n    }\n\n    return (\n        <input\n            autoFocus\n            className={cn(styles.cellEditor, invalid && styles.cellEditorInvalid)}\n            type={type}\n            value={draft}\n            aria-label={label}\n            aria-invalid={invalid || undefined}\n            aria-describedby={describedBy}\n            onChange={(event) => {\n                settled.current = false;\n                setDraft(event.target.value);\n            }}\n            onKeyDown={handleKeyDown}\n            onBlur={() => {\n                if (settled.current) return;\n                finish(\"none\");\n            }}\n        />\n    );\n}\n\nexport interface EditableCellProps {\n    /** Rendered value, used when the cell is closed. */\n    children: ReactNode;\n    /** Plain-text value, for the editor's initial draft and the trigger's name. */\n    text: string;\n    /** Header text of this column, for accessible names. */\n    columnLabel: string;\n    /** 1-based row number on the current page. */\n    rowNumber: number;\n    /** `<input type>` for the editor. */\n    inputType: string;\n    /** Whether this cell owns the open editor. */\n    editing: boolean;\n    /** Return focus to the trigger when the editor closes (Enter/Escape, not Tab). */\n    refocus: boolean;\n    /** A save is in flight for this cell. */\n    saving: boolean;\n    /** Validation or save error to surface, or null. */\n    error: string | null;\n    /** Stable id used for `aria-describedby` on the input and the trigger. */\n    errorId: string;\n    /** Copy for the affordances. */\n    labels: DataTableEditLabels;\n    /** Open the editor. */\n    onOpen: () => void;\n    /** Commit a draft. */\n    onCommit: (raw: string, move: CellCommitMove) => void;\n    /** Close without committing. */\n    onCancel: () => void;\n}\n\n/**\n * One cell of a {@link DataTable} column marked `editable`.\n *\n * Closed, it is a button carrying the value — a plain `<td>` with a click handler\n * would be invisible to a keyboard and unnamed to a screen reader, and a\n * `tabIndex`-only cell announces no role. Open, it is an `<input>`.\n *\n * The button's name comes from its **contents**: a visually hidden \"Editar {coluna}:\"\n * in front of whatever the column rendered. An `aria-label` built from the raw value\n * would read \"850000\" over a cell showing `R$ 8.500,00`, which fails WCAG 2.5.3 (Label\n * in Name) and leaves voice control unable to address the cell by what it says.\n *\n * The error message is a `role=\"alert\"` tied to the input (and to the trigger once\n * the editor closes) through `aria-describedby`, so a rejected save is announced and\n * then still reachable — an optimistic update that rolls back silently leaves the\n * user believing the edit stuck.\n */\nexport function EditableCell({\n    children,\n    text,\n    columnLabel,\n    rowNumber,\n    inputType,\n    editing,\n    refocus,\n    saving,\n    error,\n    errorId,\n    labels,\n    onOpen,\n    onCommit,\n    onCancel,\n}: EditableCellProps) {\n    const trigger = useRef<HTMLButtonElement | null>(null);\n    const wasEditing = useRef(false);\n\n    useEffect(() => {\n        if (!editing && wasEditing.current && refocus) trigger.current?.focus();\n        wasEditing.current = editing;\n    }, [editing, refocus]);\n\n    return (\n        <span className={styles.cellWrap}>\n            {editing ? (\n                <CellEditor\n                    initial={text}\n                    type={inputType}\n                    label={labels.editor(columnLabel, rowNumber)}\n                    describedBy={error ? errorId : undefined}\n                    invalid={error !== null}\n                    onCommit={onCommit}\n                    onCancel={onCancel}\n                />\n            ) : (\n                <button\n                    ref={trigger}\n                    type=\"button\"\n                    className={cn(styles.cellButton, error && styles.cellButtonInvalid)}\n                    aria-describedby={error ? errorId : undefined}\n                    aria-busy={saving || undefined}\n                    onClick={onOpen}\n                >\n                    <VisuallyHidden>{labels.editCell(columnLabel)}</VisuallyHidden> {children}\n                </button>\n            )}\n            {error && (\n                <span className={styles.cellError} id={errorId} role=\"alert\">\n                    {error}\n                </span>\n            )}\n        </span>\n    );\n}\n"],"mappings":"kLAuDA,SAAS,EAAW,CAChB,UACA,OACA,QACA,cACA,UACA,WACA,YACgB,CAChB,GAAM,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAiB,CAAO,EAC5C,GAAA,EAAU,EAAA,OAAA,CAAO,EAAK,EAE5B,SAAS,EAAO,EAA4B,CACxC,EAAQ,QAAU,GAClB,EAAS,EAAO,CAAI,CACxB,CAEA,SAAS,EAAc,EAA8C,CACjE,GAAI,EAAM,MAAQ,QAAS,CACvB,EAAM,eAAe,EACrB,EAAO,MAAM,EACb,MACJ,CACA,GAAI,EAAM,MAAQ,SAAU,CACxB,EAAM,eAAe,EACrB,EAAQ,QAAU,GAClB,EAAS,EACT,MACJ,CACI,EAAM,MAAQ,QACd,EAAM,eAAe,EACrB,EAAO,EAAM,SAAW,WAAa,MAAM,EAEnD,CAEA,OACI,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,UAAA,GACA,UAAW,EAAA,GAAG,EAAA,QAAO,WAAY,GAAW,EAAA,QAAO,iBAAiB,EAC9D,OACN,MAAO,EACP,aAAY,EACZ,eAAc,GAAW,IAAA,GACzB,mBAAkB,EAClB,SAAW,GAAU,CACjB,EAAQ,QAAU,GAClB,EAAS,EAAM,OAAO,KAAK,CAC/B,EACA,UAAW,EACX,WAAc,CACN,EAAQ,SACZ,EAAO,MAAM,CACjB,CACH,CAAA,CAET,CAkDA,SAAgB,EAAa,CACzB,WACA,OACA,cACA,YACA,YACA,UACA,UACA,SACA,QACA,UACA,SACA,SACA,WACA,YACkB,CAClB,IAAM,GAAA,EAAU,EAAA,OAAA,CAAiC,IAAI,EAC/C,GAAA,EAAa,EAAA,OAAA,CAAO,EAAK,EAO/B,OALA,EAAA,EAAA,UAAA,KAAgB,CACR,CAAC,GAAW,EAAW,SAAW,GAAS,EAAQ,SAAS,MAAM,EACtE,EAAW,QAAU,CACzB,EAAG,CAAC,EAAS,CAAO,CAAC,GAGjB,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAxB,SAAA,CACK,GACG,EAAA,EAAA,IAAA,CAAC,EAAD,CACI,QAAS,EACT,KAAM,EACN,MAAO,EAAO,OAAO,EAAa,CAAS,EAC3C,YAAa,EAAQ,EAAU,IAAA,GAC/B,QAAS,IAAU,KACT,WACA,UACb,CAAA,GAED,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,IAAK,EACL,KAAK,SACL,UAAW,EAAA,GAAG,EAAA,QAAO,WAAY,GAAS,EAAA,QAAO,iBAAiB,EAClE,mBAAkB,EAAQ,EAAU,IAAA,GACpC,YAAW,GAAU,IAAA,GACrB,QAAS,EANb,SAAA,EAQI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAO,SAAS,CAAW,CAAkB,CAAA,EAAC,IAAE,CAC7D,CAEX,CAAA,EAAA,IACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,UAAW,GAAI,EAAS,KAAK,QAChD,SAAA,CACC,CAAA,CAER,GAEd"}