ui-nyla/src/components/FormGenerator/FormGeneratorTable/ExpandableDetailRow.tsx

67 lines
2 KiB
TypeScript

import React, { useState, useEffect, useRef, useCallback } from 'react';
import styles from './FormGeneratorTable.module.css';
export interface ExpandableDetailRowProps {
open: boolean;
colSpan: number;
children: React.ReactNode;
}
/**
* Table detail row with smooth expand/collapse (grid 0fr → 1fr).
* Stays mounted until the close animation finishes.
*/
export function ExpandableDetailRow({ open, colSpan, children }: ExpandableDetailRowProps) {
const [mounted, setMounted] = useState(open);
const [revealed, setRevealed] = useState(false);
const openRef = useRef(open);
openRef.current = open;
useEffect(() => {
if (open) {
setMounted(true);
const id = requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (openRef.current) setRevealed(true);
});
});
return () => cancelAnimationFrame(id);
}
setRevealed(false);
const fallback = window.setTimeout(() => {
if (!openRef.current) setMounted(false);
}, 420);
return () => window.clearTimeout(fallback);
}, [open]);
const handleTransitionEnd = useCallback((e: React.TransitionEvent<HTMLDivElement>) => {
if (e.target !== e.currentTarget) return;
if (e.propertyName !== 'grid-template-rows') return;
if (!openRef.current) {
setMounted(false);
}
}, []);
if (!mounted) return null;
return (
<tr className={styles.expandedDetailRow}>
<td
colSpan={colSpan}
className={`${styles.expandedDetailCell} ${revealed ? styles.expandedDetailCellOpen : ''}`}
onClick={(e) => e.stopPropagation()}
>
<div
className={`${styles.expandedCollapsible} ${revealed ? styles.expandedCollapsibleOpen : ''}`}
onTransitionEnd={handleTransitionEnd}
>
<div className={styles.expandedCollapsibleInner}>
<div className={styles.expandedDetailInner}>{children}</div>
</div>
</div>
</td>
</tr>
);
}
export default ExpandableDetailRow;