1 | import * as React from "react";
|
2 | import {
|
3 | useSealedState,
|
4 | SealedInitialState,
|
5 | } from "reakit-utils/useSealedState";
|
6 | import { useIsomorphicEffect } from "reakit-utils/useIsomorphicEffect";
|
7 | import { warning } from "reakit-warning";
|
8 | import {
|
9 | unstable_IdState,
|
10 | unstable_IdActions,
|
11 | unstable_IdInitialState,
|
12 | unstable_useIdState,
|
13 | } from "../Id/IdState";
|
14 |
|
15 | export type DisclosureState = unstable_IdState & {
|
16 | |
17 |
|
18 |
|
19 | visible: boolean;
|
20 | |
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | animated: boolean | number;
|
27 | |
28 |
|
29 |
|
30 | animating: boolean;
|
31 | };
|
32 |
|
33 | export type DisclosureActions = unstable_IdActions & {
|
34 | |
35 |
|
36 |
|
37 | show: () => void;
|
38 | |
39 |
|
40 |
|
41 | hide: () => void;
|
42 | |
43 |
|
44 |
|
45 | toggle: () => void;
|
46 | |
47 |
|
48 |
|
49 | setVisible: React.Dispatch<React.SetStateAction<DisclosureState["visible"]>>;
|
50 | |
51 |
|
52 |
|
53 | setAnimated: React.Dispatch<
|
54 | React.SetStateAction<DisclosureState["animated"]>
|
55 | >;
|
56 | |
57 |
|
58 |
|
59 | stopAnimation: () => void;
|
60 | };
|
61 |
|
62 | export type DisclosureInitialState = unstable_IdInitialState &
|
63 | Partial<Pick<DisclosureState, "visible" | "animated">>;
|
64 |
|
65 | export type DisclosureStateReturn = DisclosureState & DisclosureActions;
|
66 |
|
67 | function 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 |
|
75 | export 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 |
|
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 | }
|