Skip to content

Commit 1734079

Browse files
committed
WIP
1 parent 447ad7d commit 1734079

File tree

3 files changed

+300
-9
lines changed

3 files changed

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

docs/src/app/(private)/experiments/collapsible/transition.tsx

+38-9
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import classes from './transition.module.css';
99
import { useAnimationsFinished } from '../../../../../../packages/react/src/utils/useAnimationsFinished';
1010
import { useEventCallback } from '../../../../../../packages/react/src/utils/useEventCallback';
1111
import { useForkRef } from '../../../../../../packages/react/src/utils/useForkRef';
12+
import { warn } from '../../../../../../packages/react/src/utils/warn';
1213

1314
const STARTING_HOOK = { 'data-starting-style': '' };
1415
const ENDING_HOOK = { 'data-ending-style': '' };
1516

16-
// const KEEP_MOUNTED = false;
17+
type AnimationType = 'css-transition' | 'css-animation' | 'none' | null;
1718

18-
function Collapsible(props: { defaultOpen?: boolean; keepMounted?: boolean }) {
19-
const { keepMounted = true, defaultOpen = false } = props;
19+
function Collapsible(props: {
20+
defaultOpen?: boolean;
21+
keepMounted?: boolean;
22+
id?: string;
23+
}) {
24+
const { keepMounted = true, defaultOpen = false, id } = props;
2025

2126
const [open, setOpen] = React.useState(defaultOpen);
2227

@@ -44,8 +49,8 @@ function Collapsible(props: { defaultOpen?: boolean; keepMounted?: boolean }) {
4449
return !open && !mounted;
4550
}, [keepMounted, open, mounted]);
4651

52+
const animationTypeRef = React.useRef<AnimationType>(null);
4753
const panelRef: React.RefObject<HTMLElement | null> = React.useRef(null);
48-
4954
/**
5055
* When `keepMounted` is `true` this runs once as soon as it exists in the DOM
5156
* regardless of initial open state.
@@ -58,6 +63,29 @@ function Collapsible(props: { defaultOpen?: boolean; keepMounted?: boolean }) {
5863
if (!element) {
5964
return;
6065
}
66+
if (animationTypeRef.current == null) {
67+
const panelStyles = getComputedStyle(element);
68+
if (
69+
panelStyles.animationName !== 'none' &&
70+
panelStyles.transitionDelay !== '0s'
71+
) {
72+
warn('CSS transitions and CSS animations both detected');
73+
} else if (
74+
panelStyles.animationName === 'none' &&
75+
panelStyles.transitionDuration !== '0s'
76+
) {
77+
animationTypeRef.current = 'css-transition';
78+
} else if (
79+
panelStyles.animationName !== 'none' &&
80+
panelStyles.transitionDuration === '0s'
81+
) {
82+
animationTypeRef.current = 'css-animation';
83+
} else {
84+
animationTypeRef.current = 'none';
85+
}
86+
}
87+
// console.log('animationType', animationTypeRef.current);
88+
6189
/**
6290
* Explicitly set `display` to ensure the panel is actually rendered before
6391
* measuring anything. `!important` is to needed to override a conflicting
@@ -221,7 +249,7 @@ function Collapsible(props: { defaultOpen?: boolean; keepMounted?: boolean }) {
221249
onClick={handleTrigger}
222250
>
223251
<ExpandMoreIcon className={classes.Icon} />
224-
Trigger {/* (keepMounted {String(keepMounted)}) */}
252+
Trigger {id}
225253
</button>
226254

227255
{(keepMounted || (!keepMounted && mounted)) && (
@@ -232,6 +260,7 @@ function Collapsible(props: { defaultOpen?: boolean; keepMounted?: boolean }) {
232260
{...{ [open ? 'data-open' : 'data-closed']: '' }}
233261
{...styleHooks}
234262
hidden={isHidden}
263+
id={id}
235264
>
236265
<div className={classes.Content}>
237266
<p>
@@ -253,17 +282,17 @@ export default function App() {
253282
<div className={classes.grid}>
254283
<div className={classes.wrapper}>
255284
<pre>keepMounted: true</pre>
256-
<Collapsible keepMounted defaultOpen />
285+
<Collapsible keepMounted defaultOpen id="1" />
257286

258-
<Collapsible keepMounted defaultOpen={false} />
287+
<Collapsible keepMounted defaultOpen={false} id="2" />
259288

260289
<small>———</small>
261290
</div>
262291
<div className={classes.wrapper}>
263292
<pre>keepMounted: false</pre>
264-
<Collapsible keepMounted={false} defaultOpen />
293+
<Collapsible keepMounted={false} defaultOpen id="3" />
265294

266-
<Collapsible keepMounted={false} defaultOpen={false} />
295+
<Collapsible keepMounted={false} defaultOpen={false} id="4" />
267296
<small>———</small>
268297
</div>
269298
</div>

0 commit comments

Comments
 (0)