Skip to content

Commit bead799

Browse files
committed
Isolate CSS animation logic
1 parent 0fd6683 commit bead799

File tree

6 files changed

+349
-10
lines changed

6 files changed

+349
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { Collapsible } from '@base-ui-components/react/collapsible';
4+
import styles from './animation.module.css';
5+
6+
export default function CollapsibleCssAnimation() {
7+
return (
8+
<div className={styles.wrapper}>
9+
<Collapsible.Root defaultOpen className={styles.Root}>
10+
<Collapsible.Trigger className={styles.Trigger}>Trigger</Collapsible.Trigger>
11+
<Collapsible.Panel className={styles.Panel} keepMounted>
12+
<div className={styles.Content}>
13+
<p>
14+
He rubbed his eyes, and came close to the picture, and examined it
15+
again. There were no signs of any change when he looked into the actual
16+
painting, and yet there was no doubt that the whole expression had
17+
altered. It was not a mere fancy of his own. The thing was horribly
18+
apparent.
19+
</p>
20+
</div>
21+
</Collapsible.Panel>
22+
</Collapsible.Root>
23+
24+
<Collapsible.Root defaultOpen={false} className={styles.Root}>
25+
<Collapsible.Trigger className={styles.Trigger}>Trigger</Collapsible.Trigger>
26+
<Collapsible.Panel className={styles.Panel} keepMounted>
27+
<div className={styles.Content}>
28+
<p>
29+
He rubbed his eyes, and came close to the picture, and examined it
30+
again. There were no signs of any change when he looked into the actual
31+
painting, and yet there was no doubt that the whole expression had
32+
altered. It was not a mere fancy of his own. The thing was horribly
33+
apparent.
34+
</p>
35+
</div>
36+
</Collapsible.Panel>
37+
</Collapsible.Root>
38+
</div>
39+
);
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
@keyframes slide-down {
2+
from {
3+
height: 0;
4+
}
5+
6+
to {
7+
height: var(--collapsible-panel-height);
8+
}
9+
}
10+
11+
@keyframes slide-up {
12+
from {
13+
height: var(--collapsible-panel-height);
14+
}
15+
16+
to {
17+
height: 0;
18+
}
19+
}
20+
21+
.Panel {
22+
overflow: hidden;
23+
box-sizing: border-box;
24+
width: 100%;
25+
animation-fill-mode: forwards !important;
26+
27+
&[data-open] {
28+
animation: slide-down var(--duration) ease-out;
29+
}
30+
31+
&[data-closed] {
32+
animation: slide-up var(--duration) ease-in;
33+
}
34+
}
35+
36+
/* the styles below are irrelevant to the features */
37+
.grid {
38+
display: grid;
39+
grid-template-columns: 1fr 1fr;
40+
grid-gap: 5rem;
41+
}
42+
43+
.wrapper {
44+
font-family: system-ui, sans-serif;
45+
line-height: 1.4;
46+
display: flex;
47+
flex-flow: column nowrap;
48+
align-items: stretch;
49+
gap: 1rem;
50+
align-self: flex-start;
51+
}
52+
53+
.Root {
54+
--width: 320px;
55+
--duration: 800ms;
56+
57+
width: var(--width);
58+
59+
& + .Root {
60+
margin-top: 2rem;
61+
}
62+
}
63+
64+
.Trigger {
65+
display: flex;
66+
width: 100%;
67+
align-items: center;
68+
padding: 0.25rem 0.5rem;
69+
border-radius: 0.25rem;
70+
background-color: var(--color-gray-200);
71+
color: var(--color-gray-900);
72+
73+
& svg {
74+
transform: rotate(-90deg);
75+
transition: transform var(--duration) ease-in;
76+
}
77+
78+
&[data-panel-open] svg {
79+
transform: rotate(0);
80+
transition: transform var(--duration) ease-out;
81+
}
82+
}
83+
84+
.Content {
85+
display: flex;
86+
flex-direction: column;
87+
gap: 0.5rem;
88+
margin-top: 0.25rem;
89+
padding: 0.5rem;
90+
border-radius: 0.25rem;
91+
background-color: var(--color-gray-200);
92+
cursor: text;
93+
94+
& p {
95+
overflow-wrap: break-word;
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
'use client';
2+
import * as React from 'react';
3+
import {
4+
useEnhancedEffect,
5+
// useTransitionStatus,
6+
} from '@base-ui-components/react/utils';
7+
import classes from './animation.module.css';
8+
9+
import { useAnimationsFinished } from '../../../../../../packages/react/src/utils/useAnimationsFinished';
10+
import { useEventCallback } from '../../../../../../packages/react/src/utils/useEventCallback';
11+
// import { useForkRef } from '../../../../../../packages/react/src/utils/useForkRef';
12+
import { useOnMount } from '../../../../../../packages/react/src/utils/useOnMount';
13+
14+
function Collapsible(props: {
15+
defaultOpen?: boolean;
16+
keepMounted?: boolean;
17+
id?: string;
18+
}) {
19+
const { keepMounted = true, defaultOpen = false, id } = props;
20+
21+
const [open, setOpen] = React.useState(defaultOpen);
22+
23+
const [visible, setVisible] = React.useState(open);
24+
25+
const [mounted, setMounted] = React.useState(open);
26+
27+
const [height, setHeight] = React.useState<number | undefined>(0);
28+
29+
const latestAnimationNameRef = React.useRef<string>(null);
30+
const shouldCancelInitialOpenAnimationRef = React.useRef(open);
31+
32+
const isHidden = !visible;
33+
34+
const panelRef: React.RefObject<HTMLElement | null> = React.useRef(null);
35+
36+
const runOnceAnimationsFinish = useAnimationsFinished(panelRef, false);
37+
38+
const handleTrigger = useEventCallback(() => {
39+
const nextOpen = !open;
40+
41+
const panel = panelRef.current;
42+
43+
if (panel) {
44+
panel.style.removeProperty('animation-name');
45+
}
46+
47+
if (!keepMounted) {
48+
if (!visible && nextOpen) {
49+
setVisible(true);
50+
}
51+
if (!mounted && nextOpen) {
52+
setMounted(true);
53+
}
54+
}
55+
setOpen(nextOpen);
56+
});
57+
58+
useEnhancedEffect(() => {
59+
const panel = panelRef.current;
60+
if (!panel) {
61+
return;
62+
}
63+
64+
latestAnimationNameRef.current =
65+
panel.style.animationName || latestAnimationNameRef.current;
66+
67+
panel.style.animationName = 'none';
68+
69+
setHeight(panel.scrollHeight);
70+
71+
if (!shouldCancelInitialOpenAnimationRef.current) {
72+
panel.style.removeProperty('animation-name');
73+
}
74+
75+
if (open) {
76+
setMounted(true);
77+
}
78+
79+
runOnceAnimationsFinish(() => {
80+
setVisible(open);
81+
});
82+
}, [open, visible, runOnceAnimationsFinish]);
83+
84+
useOnMount(() => {
85+
const frame = requestAnimationFrame(() => {
86+
shouldCancelInitialOpenAnimationRef.current = false;
87+
});
88+
return () => cancelAnimationFrame(frame);
89+
});
90+
91+
return (
92+
<div
93+
className={classes.Root}
94+
style={{
95+
// @ts-ignore
96+
'--collapsible-panel-height': height ? `${height}px` : undefined,
97+
}}
98+
>
99+
<button
100+
type="button"
101+
className={classes.Trigger}
102+
data-panel-open={open || undefined}
103+
onClick={handleTrigger}
104+
>
105+
<ExpandMoreIcon className={classes.Icon} />
106+
Trigger {id}
107+
</button>
108+
109+
{(keepMounted || (!keepMounted && mounted)) && (
110+
<div
111+
// @ts-ignore
112+
ref={panelRef}
113+
className={classes.Panel}
114+
{...{ [open ? 'data-open' : 'data-closed']: '' }}
115+
// {...styleHooks}
116+
hidden={isHidden}
117+
id={id}
118+
>
119+
<div className={classes.Content}>
120+
<p>
121+
He rubbed his eyes, and came close to the picture, and examined it
122+
again. There were no signs of any change when he looked into the actual
123+
painting, and yet there was no doubt that the whole expression had
124+
altered. It was not a mere fancy of his own. The thing was horribly
125+
apparent.
126+
</p>
127+
</div>
128+
</div>
129+
)}
130+
</div>
131+
);
132+
}
133+
134+
export default function App() {
135+
return (
136+
<div className={classes.grid}>
137+
<div className={classes.wrapper}>
138+
<pre>keepMounted: true</pre>
139+
<Collapsible keepMounted defaultOpen id="1" />
140+
141+
<Collapsible keepMounted defaultOpen={false} id="2" />
142+
143+
<small>———</small>
144+
</div>
145+
<div className={classes.wrapper}>
146+
<pre>keepMounted: false</pre>
147+
<Collapsible keepMounted={false} defaultOpen id="3" />
148+
149+
<Collapsible keepMounted={false} defaultOpen={false} id="4" />
150+
<small>———</small>
151+
</div>
152+
</div>
153+
);
154+
}
155+
156+
function ExpandMoreIcon(props: React.SVGProps<SVGSVGElement>) {
157+
return (
158+
<svg
159+
xmlns="http://www.w3.org/2000/svg"
160+
{...props}
161+
width="24"
162+
height="24"
163+
viewBox="0 0 24 24"
164+
fill="none"
165+
>
166+
<path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" fill="currentColor" />
167+
</svg>
168+
);
169+
}

0 commit comments

Comments
 (0)