-
-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathCollapsiblePrimitiveContent.tsx
160 lines (139 loc) · 5.44 KB
/
CollapsiblePrimitiveContent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import React, { useState, useRef, useEffect } from 'react';
import Primitive from '~/core/primitives/Primitive';
import { useCollapsiblePrimitiveContext } from '../contexts/CollapsiblePrimitiveContext';
export type CollapsiblePrimitiveContentProps = {
/**
* Content to be rendered inside the collapsible content
*/
children?: React.ReactNode;
/**
* CSS class name for custom styling
*/
className?: string;
/**
* For Polymorphic component support
*/
asChild?: boolean;
/**
* Additional props to be spread on the content element
*/
[key: string]: any;
};
const CollapsiblePrimitiveContent = React.forwardRef<HTMLDivElement, CollapsiblePrimitiveContentProps>(
({
children,
className,
asChild = false,
...props
}, forwardedRef) => {
const {
open,
contentId,
transitionDuration,
transitionTimingFunction
} = useCollapsiblePrimitiveContext();
const ref = useRef<HTMLDivElement>(null);
const combinedRef = (forwardedRef || ref) as React.RefObject<HTMLDivElement>;
const [height, setHeight] = useState<number | undefined>(open ? undefined : 0);
const [shouldRender, setShouldRender] = useState(open);
const animationTimeoutRef = useRef<NodeJS.Timeout>();
const rafRef = useRef<number>();
// When opening, we need to immediately render
useEffect(() => {
if (open) {
setShouldRender(true);
}
}, [open]);
useEffect(() => {
// Clear any existing timeout and animation frames to avoid conflicts
if (animationTimeoutRef.current) {
clearTimeout(animationTimeoutRef.current);
}
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
if (!ref.current) return;
// Handle the case when transitionDuration is 0 - no animation
if (transitionDuration === 0) {
setHeight(open ? undefined : 0);
// For instant changes, also update visibility immediately
if (!open) {
setTimeout(() => setShouldRender(false), 0);
}
return;
}
if (open) {
// Opening animation
// First set height to 0 to ensure proper animation start state
setHeight(0);
// Use RAF to ensure the DOM has updated with the new height
rafRef.current = requestAnimationFrame(() => {
// Force a reflow
const _ = ref.current?.offsetHeight;
// Now measure and start animation in the next frame
rafRef.current = requestAnimationFrame(() => {
if (ref.current) {
const contentHeight = ref.current.scrollHeight;
setHeight(contentHeight);
}
});
});
// After animation completes, set height to undefined for responsive flexibility
animationTimeoutRef.current = setTimeout(() => {
setHeight(undefined);
}, transitionDuration);
} else {
// Closing animation
// First set to current height to ensure smooth start
const contentHeight = ref.current.scrollHeight;
setHeight(contentHeight);
// Use RAF to ensure browser processes the height setting
rafRef.current = requestAnimationFrame(() => {
// Force a reflow
const _ = ref.current?.offsetHeight;
// Then animate to 0 in the next frame
rafRef.current = requestAnimationFrame(() => {
setHeight(0);
});
});
// After animation completes, we can hide the element completely
animationTimeoutRef.current = setTimeout(() => {
setShouldRender(false);
}, transitionDuration);
}
return () => {
if (animationTimeoutRef.current) {
clearTimeout(animationTimeoutRef.current);
}
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [open, transitionDuration]);
// Don't render anything if closed and animation is complete
if (!shouldRender && !open) {
return null;
}
return (
<Primitive.div
id={contentId}
ref={combinedRef}
aria-hidden={!open}
data-state={open ? 'open' : 'closed'}
className={className}
style={{
height: height !== undefined ? `${height}px` : undefined,
overflow: 'hidden',
...(transitionDuration > 0
? { transition: `height ${transitionDuration}ms ${transitionTimingFunction}` }
: {})
}}
{...props}
>
{children}
</Primitive.div>
);
}
);
CollapsiblePrimitiveContent.displayName = 'CollapsiblePrimitiveContent';
export default CollapsiblePrimitiveContent;