Skip to content

Commit 4d89784

Browse files
committed
poc: create dialog stackable system compatible with router and promises
1 parent f3813f9 commit 4d89784

File tree

13 files changed

+1998
-19
lines changed

13 files changed

+1998
-19
lines changed

src/components/layout/RootLayout.tsx

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SidebarProvider } from "@/components/ui/sidebar";
66
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
77
import { BackendProvider } from "@/providers/BackendProvider";
88
import { ComponentSpecProvider } from "@/providers/ComponentSpecProvider";
9+
import { DialogProvider } from "@/providers/DialogProvider/DialogProvider";
910

1011
import AppFooter from "./AppFooter";
1112
import AppMenu from "./AppMenu";
@@ -15,25 +16,27 @@ const RootLayout = () => {
1516

1617
return (
1718
<BackendProvider>
18-
<SidebarProvider>
19-
<ComponentSpecProvider>
20-
<ToastContainer />
21-
22-
<div className="App flex flex-col min-h-screen w-full">
23-
<AppMenu />
24-
25-
<main className="flex-1 grid">
26-
<Outlet />
27-
</main>
28-
29-
<AppFooter />
30-
31-
{import.meta.env.VITE_ENABLE_ROUTER_DEVTOOLS === "true" && (
32-
<TanStackRouterDevtools />
33-
)}
34-
</div>
35-
</ComponentSpecProvider>
36-
</SidebarProvider>
19+
<DialogProvider>
20+
<SidebarProvider>
21+
<ComponentSpecProvider>
22+
<ToastContainer />
23+
24+
<div className="App flex flex-col min-h-screen w-full">
25+
<AppMenu />
26+
27+
<main className="flex-1 grid">
28+
<Outlet />
29+
</main>
30+
31+
<AppFooter />
32+
33+
{import.meta.env.VITE_ENABLE_ROUTER_DEVTOOLS === "true" && (
34+
<TanStackRouterDevtools />
35+
)}
36+
</div>
37+
</ComponentSpecProvider>
38+
</SidebarProvider>
39+
</DialogProvider>
3740
</BackendProvider>
3841
);
3942
};
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import {
2+
useCallback,
3+
useEffect,
4+
useLayoutEffect,
5+
useRef,
6+
useState,
7+
} from "react";
8+
9+
import { cn } from "@/lib/utils";
10+
11+
interface AnimatedHeightProps {
12+
children: React.ReactNode;
13+
className?: string;
14+
/** Duration of the height transition in milliseconds */
15+
duration?: number;
16+
/** Easing function for the transition */
17+
easing?: string;
18+
/** Key that changes when content changes - triggers re-measurement */
19+
contentKey?: string | number;
20+
}
21+
22+
/**
23+
* A component that smoothly animates its height based on content changes.
24+
* Uses ResizeObserver to detect content size changes and applies CSS transitions.
25+
* During shrink transitions, overflow is visible to prevent content clipping.
26+
*/
27+
export function AnimatedHeight({
28+
children,
29+
className,
30+
duration = 200,
31+
easing = "ease-out",
32+
contentKey,
33+
}: AnimatedHeightProps) {
34+
const contentRef = useRef<HTMLDivElement>(null);
35+
const [height, setHeight] = useState<number | null>(null);
36+
const [enableTransition, setEnableTransition] = useState(false);
37+
const [isShrinking, setIsShrinking] = useState(false);
38+
const prevHeightRef = useRef<number | null>(null);
39+
const shrinkTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
40+
const isFirstMeasurementRef = useRef(true);
41+
const isTransitioningRef = useRef(false);
42+
43+
const measureHeight = useCallback(
44+
(force = false) => {
45+
if (isTransitioningRef.current && !force) return;
46+
47+
const contentEl = contentRef.current;
48+
if (!contentEl) return;
49+
50+
const newHeight = contentEl.offsetHeight;
51+
const prevHeight = prevHeightRef.current;
52+
53+
if (newHeight !== prevHeight && newHeight > 0) {
54+
const shrinking = prevHeight !== null && newHeight < prevHeight;
55+
56+
if (shrinkTimeoutRef.current) {
57+
clearTimeout(shrinkTimeoutRef.current);
58+
shrinkTimeoutRef.current = null;
59+
}
60+
61+
if (shrinking) {
62+
setIsShrinking(true);
63+
shrinkTimeoutRef.current = setTimeout(() => {
64+
setIsShrinking(false);
65+
}, duration);
66+
}
67+
68+
prevHeightRef.current = newHeight;
69+
setHeight(newHeight);
70+
71+
if (isFirstMeasurementRef.current) {
72+
isFirstMeasurementRef.current = false;
73+
requestAnimationFrame(() => {
74+
setEnableTransition(true);
75+
});
76+
}
77+
}
78+
},
79+
[duration],
80+
);
81+
82+
// Handle contentKey changes synchronously before paint
83+
useLayoutEffect(() => {
84+
isTransitioningRef.current = true;
85+
86+
// Measure synchronously - useLayoutEffect runs after DOM update but before paint
87+
measureHeight(true);
88+
89+
isTransitioningRef.current = false;
90+
}, [contentKey, measureHeight]);
91+
92+
// ResizeObserver for dynamic content changes (not during key transitions)
93+
useEffect(() => {
94+
const contentEl = contentRef.current;
95+
if (!contentEl) return;
96+
97+
const resizeObserver = new ResizeObserver(() => {
98+
if (!isTransitioningRef.current) {
99+
measureHeight();
100+
}
101+
});
102+
103+
resizeObserver.observe(contentEl);
104+
105+
return () => {
106+
resizeObserver.disconnect();
107+
if (shrinkTimeoutRef.current) {
108+
clearTimeout(shrinkTimeoutRef.current);
109+
}
110+
};
111+
}, [measureHeight]);
112+
113+
return (
114+
<div
115+
className={cn(
116+
isShrinking ? "overflow-visible" : "overflow-hidden",
117+
className,
118+
)}
119+
style={{
120+
height: height !== null ? `${height}px` : "auto",
121+
transition: enableTransition
122+
? `height ${duration}ms ${easing}`
123+
: undefined,
124+
}}
125+
>
126+
<div ref={contentRef}>{children}</div>
127+
</div>
128+
);
129+
}

0 commit comments

Comments
 (0)