animation ein/ausklappen eines tenants hinzugefügt
This commit is contained in:
parent
eb0a58aaa7
commit
fa7988b4b6
4 changed files with 126 additions and 15 deletions
|
|
@ -31,9 +31,15 @@
|
||||||
|
|
||||||
.chevronIcon {
|
.chevronIcon {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chevronIconExpanded {
|
.chevronIconExpanded {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.chevronIcon {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -592,10 +592,37 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandedDetailCell {
|
.expandedDetailCell {
|
||||||
padding: 12px 16px 16px;
|
padding: 0 16px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
border-bottom: 2px solid var(--color-border, #e2e8f0);
|
border-bottom: 2px solid transparent;
|
||||||
box-sizing: border-box;
|
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 {
|
.expandedDetailInner {
|
||||||
|
|
@ -604,6 +631,24 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: visible;
|
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 */
|
/* Items that live inside a group — subtle tint + left connector */
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ import {
|
||||||
isNumberType,
|
isNumberType,
|
||||||
} from '../../../utils/attributeTypeMapper';
|
} from '../../../utils/attributeTypeMapper';
|
||||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||||
|
import { ExpandableDetailRow } from './ExpandableDetailRow';
|
||||||
import { FaFilter } from 'react-icons/fa';
|
import { FaFilter } from 'react-icons/fa';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { PeriodPicker } from '../../PeriodPicker';
|
import { PeriodPicker } from '../../PeriodPicker';
|
||||||
|
|
@ -2760,18 +2761,10 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
{isExpanded && renderExpandedRow && (
|
{renderExpandedRow && (
|
||||||
<tr className={styles.expandedDetailRow}>
|
<ExpandableDetailRow open={isExpanded} colSpan={detailColSpan}>
|
||||||
<td
|
|
||||||
colSpan={detailColSpan}
|
|
||||||
className={styles.expandedDetailCell}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className={styles.expandedDetailInner}>
|
|
||||||
{renderExpandedRow(row)}
|
{renderExpandedRow(row)}
|
||||||
</div>
|
</ExpandableDetailRow>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue