diff --git a/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css b/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css index 9437cb0..26f5372 100644 --- a/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css +++ b/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css @@ -31,9 +31,15 @@ .chevronIcon { font-size: 11px; - transition: transform 0.15s ease; + transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1); } .chevronIconExpanded { transform: rotate(90deg); } + +@media (prefers-reduced-motion: reduce) { + .chevronIcon { + transition: none; + } +} diff --git a/src/components/FormGenerator/FormGeneratorTable/ExpandableDetailRow.tsx b/src/components/FormGenerator/FormGeneratorTable/ExpandableDetailRow.tsx new file mode 100644 index 0000000..c5c956b --- /dev/null +++ b/src/components/FormGenerator/FormGeneratorTable/ExpandableDetailRow.tsx @@ -0,0 +1,67 @@ +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) => { + if (e.target !== e.currentTarget) return; + if (e.propertyName !== 'grid-template-rows') return; + if (!openRef.current) { + setMounted(false); + } + }, []); + + if (!mounted) return null; + + return ( + + e.stopPropagation()} + > +
+
+
{children}
+
+
+ + + ); +} + +export default ExpandableDetailRow; diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css index 2cc58a6..0fbafd7 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css @@ -592,10 +592,37 @@ } .expandedDetailCell { - padding: 12px 16px 16px; + padding: 0 16px; vertical-align: top; - border-bottom: 2px solid var(--color-border, #e2e8f0); + border-bottom: 2px solid transparent; box-sizing: border-box; + transition: + padding 0.32s cubic-bezier(0.4, 0, 0.2, 1), + border-color 0.32s cubic-bezier(0.4, 0, 0.2, 1); +} + +.expandedDetailCellOpen { + padding: 12px 16px 16px; + border-bottom-color: var(--color-border, #e2e8f0); +} + +.expandedCollapsible { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transition: + grid-template-rows 0.36s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.28s cubic-bezier(0.4, 0, 0.2, 1); +} + +.expandedCollapsibleOpen { + grid-template-rows: 1fr; + opacity: 1; +} + +.expandedCollapsibleInner { + overflow: hidden; + min-height: 0; } .expandedDetailInner { @@ -604,6 +631,24 @@ min-width: 0; overflow-x: auto; overflow-y: visible; + transform: translateY(-6px); + transition: transform 0.36s cubic-bezier(0.4, 0, 0.2, 1); +} + +.expandedCollapsibleOpen .expandedDetailInner { + transform: translateY(0); +} + +@media (prefers-reduced-motion: reduce) { + .expandedDetailCell, + .expandedCollapsible, + .expandedDetailInner { + transition: none; + } + + .expandedCollapsibleOpen .expandedDetailInner { + transform: none; + } } /* Items that live inside a group — subtle tint + left connector */ diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx index 2815c83..a9943e5 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx @@ -79,6 +79,7 @@ import { isNumberType, } from '../../../utils/attributeTypeMapper'; import type { AttributeType } from '../../../utils/attributeTypeMapper'; +import { ExpandableDetailRow } from './ExpandableDetailRow'; import { FaFilter } from 'react-icons/fa'; import api from '../../../api'; import { PeriodPicker } from '../../PeriodPicker'; @@ -2760,18 +2761,10 @@ export function FormGeneratorTable>({ ); })} - {isExpanded && renderExpandedRow && ( - - e.stopPropagation()} - > -
- {renderExpandedRow(row)} -
- - + {renderExpandedRow && ( + + {renderExpandedRow(row)} + )} );