-
Notifications
You must be signed in to change notification settings - Fork 40
/
Copy pathAnimateHeight.tsx
75 lines (67 loc) · 2.39 KB
/
AnimateHeight.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
import cl from 'clsx/lite';
import { useCallback, useRef, useState } from 'react';
import type * as React from 'react';
import { useMediaQuery, usePrevious } from '../../utilities';
export type AnimateHeightProps = {
open: boolean;
} & React.HTMLAttributes<HTMLDivElement>;
type InternalState = 'open' | 'closed' | 'openingOrClosing';
const transitionDurationInMilliseconds = 250;
/**
* AnimateHeight is a component that animates its height when the `open` prop changes.
*/
export const AnimateHeight = ({
children,
className,
open = false,
style,
...rest
}: AnimateHeightProps) => {
/* We don't know the initial height we want to start with.
It depends on if it should start open or not, therefore we set height to `undefined`,
so we don't get any layoutshift on first render */
const [height, setHeight] = useState<number | undefined>(undefined);
const prevOpen = usePrevious(open);
const openOrClosed: InternalState = open ? 'open' : 'closed';
const [state, setState] = useState<InternalState>(openOrClosed);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const shouldAnimate = !useMediaQuery('(prefers-reduced-motion)');
const contentRef = useCallback(
(node: HTMLDivElement) => {
if (node) {
const resizeObserver = new ResizeObserver(() => {
setHeight(open ? node.getBoundingClientRect().height : 0);
});
resizeObserver.observe(node);
}
if (prevOpen !== undefined && prevOpen !== open) {
// Opening or closing
setState(shouldAnimate ? 'openingOrClosing' : openOrClosed);
timeoutRef.current && clearTimeout(timeoutRef.current); // Reset timeout if already active (i.e. if the user closes the component before it finishes opening)
timeoutRef.current = setTimeout(() => {
setState(openOrClosed);
}, transitionDurationInMilliseconds);
}
},
[open, openOrClosed, prevOpen, shouldAnimate],
);
const transition =
state === 'openingOrClosing'
? `height ${transitionDurationInMilliseconds}ms ease-in-out`
: undefined;
return (
<div
{...rest}
className={cl(
'ds-animate-height',
`ds-animate-height--${state}`,
className,
)}
style={{ height, transition, ...style }}
>
<div ref={contentRef} className='ds-animate-height__content'>
{children}
</div>
</div>
);
};