UNPKG

3.73 kBPlain TextView Raw
1import * as React from "react";
2import {
3 useSealedState,
4 SealedInitialState,
5} from "reakit-utils/useSealedState";
6import { useIsomorphicEffect } from "reakit-utils/useIsomorphicEffect";
7import { warning } from "reakit-warning";
8import {
9 unstable_IdState,
10 unstable_IdActions,
11 unstable_IdInitialState,
12 unstable_useIdState,
13} from "../Id/IdState";
14
15export type DisclosureState = unstable_IdState & {
16 /**
17 * Whether it's visible or not.
18 */
19 visible: boolean;
20 /**
21 * If `true`, `animating` will be set to `true` when `visible` is updated.
22 * It'll wait for `stopAnimation` to be called or a CSS transition ends.
23 * If `animated` is set to a `number`, `stopAnimation` will be called only
24 * after the same number of milliseconds have passed.
25 */
26 animated: boolean | number;
27 /**
28 * Whether it's animating or not.
29 */
30 animating: boolean;
31};
32
33export type DisclosureActions = unstable_IdActions & {
34 /**
35 * Changes the `visible` state to `true`
36 */
37 show: () => void;
38 /**
39 * Changes the `visible` state to `false`
40 */
41 hide: () => void;
42 /**
43 * Toggles the `visible` state
44 */
45 toggle: () => void;
46 /**
47 * Sets `visible`.
48 */
49 setVisible: React.Dispatch<React.SetStateAction<DisclosureState["visible"]>>;
50 /**
51 * Sets `animated`.
52 */
53 setAnimated: React.Dispatch<
54 React.SetStateAction<DisclosureState["animated"]>
55 >;
56 /**
57 * Stops animation. It's called automatically if there's a CSS transition.
58 */
59 stopAnimation: () => void;
60};
61
62export type DisclosureInitialState = unstable_IdInitialState &
63 Partial<Pick<DisclosureState, "visible" | "animated">>;
64
65export type DisclosureStateReturn = DisclosureState & DisclosureActions;
66
67function useLastValue<T>(value: T) {
68 const lastValue = React.useRef<T | null>(null);
69 useIsomorphicEffect(() => {
70 lastValue.current = value;
71 }, [value]);
72 return lastValue;
73}
74
75export function useDisclosureState(
76 initialState: SealedInitialState<DisclosureInitialState> = {}
77): DisclosureStateReturn {
78 const {
79 visible: initialVisible = false,
80 animated: initialAnimated = false,
81 ...sealed
82 } = useSealedState(initialState);
83
84 const id = unstable_useIdState(sealed);
85
86 const [visible, setVisible] = React.useState(initialVisible);
87 const [animated, setAnimated] = React.useState(initialAnimated);
88 const [animating, setAnimating] = React.useState(false);
89 const lastVisible = useLastValue(visible);
90
91 const visibleHasChanged =
92 lastVisible.current != null && lastVisible.current !== visible;
93
94 if (animated && !animating && visibleHasChanged) {
95 // Sets animating to true when when visible is updated
96 setAnimating(true);
97 }
98
99 React.useEffect(() => {
100 if (typeof animated === "number" && animating) {
101 const timeout = setTimeout(() => setAnimating(false), animated);
102 return () => {
103 clearTimeout(timeout);
104 };
105 }
106 if (animated && animating && process.env.NODE_ENV === "development") {
107 const timeout = setTimeout(() => {
108 warning(
109 animating,
110 "It's been 8 seconds but stopAnimation has not been called. Does the disclousure element have a CSS transition?"
111 );
112 }, 8000);
113 return () => {
114 clearTimeout(timeout);
115 };
116 }
117 return () => {};
118 }, [animated, animating]);
119
120 const show = React.useCallback(() => setVisible(true), []);
121 const hide = React.useCallback(() => setVisible(false), []);
122 const toggle = React.useCallback(() => setVisible((v) => !v), []);
123 const stopAnimation = React.useCallback(() => setAnimating(false), []);
124
125 return {
126 ...id,
127 visible,
128 animated,
129 animating,
130 show,
131 hide,
132 toggle,
133 setVisible,
134 setAnimated,
135 stopAnimation,
136 };
137}