animation ein/ausklappen eines tenants hinzugefügt

This commit is contained in:
Ida 2026-06-04 14:16:17 +02:00
parent eb0a58aaa7
commit fa7988b4b6
4 changed files with 126 additions and 15 deletions

View file

@ -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;
}
}

View file

@ -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<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;

View file

@ -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 */

View file

@ -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<T extends Record<string, any>>({
);
})}
</tr>
{isExpanded && renderExpandedRow && (
<tr className={styles.expandedDetailRow}>
<td
colSpan={detailColSpan}
className={styles.expandedDetailCell}
onClick={(e) => e.stopPropagation()}
>
<div className={styles.expandedDetailInner}>
{renderExpandedRow(row)}
</div>
</td>
</tr>
{renderExpandedRow && (
<ExpandableDetailRow open={isExpanded} colSpan={detailColSpan}>
{renderExpandedRow(row)}
</ExpandableDetailRow>
)}
</React.Fragment>
);