1 | import * as React from "react";
|
2 | import { createPopper, Instance, State } from "@popperjs/core";
|
3 | import {
|
4 | SealedInitialState,
|
5 | useSealedState,
|
6 | } from "reakit-utils/useSealedState";
|
7 | import { useIsomorphicEffect } from "reakit-utils/useIsomorphicEffect";
|
8 | import { shallowEqual } from "reakit-utils/shallowEqual";
|
9 | import { isUA } from "reakit-utils/dom";
|
10 | import {
|
11 | DialogState,
|
12 | DialogActions,
|
13 | DialogInitialState,
|
14 | useDialogState,
|
15 | DialogStateReturn,
|
16 | } from "../Dialog/DialogState";
|
17 |
|
18 | const isSafari = isUA("Mac") && !isUA("Chrome") && isUA("Safari");
|
19 |
|
20 | type Placement =
|
21 | | "auto-start"
|
22 | | "auto"
|
23 | | "auto-end"
|
24 | | "top-start"
|
25 | | "top"
|
26 | | "top-end"
|
27 | | "right-start"
|
28 | | "right"
|
29 | | "right-end"
|
30 | | "bottom-end"
|
31 | | "bottom"
|
32 | | "bottom-start"
|
33 | | "left-end"
|
34 | | "left"
|
35 | | "left-start";
|
36 |
|
37 | export type PopoverState = DialogState & {
|
38 | |
39 |
|
40 |
|
41 | unstable_referenceRef: React.RefObject<HTMLElement | null>;
|
42 | |
43 |
|
44 |
|
45 |
|
46 | unstable_popoverRef: React.RefObject<HTMLElement | null>;
|
47 | |
48 |
|
49 |
|
50 |
|
51 | unstable_arrowRef: React.RefObject<HTMLElement | null>;
|
52 | |
53 |
|
54 |
|
55 |
|
56 | unstable_popoverStyles: React.CSSProperties;
|
57 | |
58 |
|
59 |
|
60 |
|
61 | unstable_arrowStyles: React.CSSProperties;
|
62 | |
63 |
|
64 |
|
65 |
|
66 | unstable_originalPlacement: Placement;
|
67 | |
68 |
|
69 |
|
70 | unstable_update: () => boolean;
|
71 | |
72 |
|
73 |
|
74 | placement: Placement;
|
75 | };
|
76 |
|
77 | export type PopoverActions = DialogActions & {
|
78 | |
79 |
|
80 |
|
81 | place: React.Dispatch<React.SetStateAction<Placement>>;
|
82 | };
|
83 |
|
84 | export type PopoverInitialState = DialogInitialState &
|
85 | Partial<Pick<PopoverState, "placement">> & {
|
86 | |
87 |
|
88 |
|
89 | unstable_fixed?: boolean;
|
90 | |
91 |
|
92 |
|
93 |
|
94 | unstable_flip?: boolean;
|
95 | |
96 |
|
97 |
|
98 | unstable_offset?: [number | string, number | string];
|
99 | |
100 |
|
101 |
|
102 | gutter?: number;
|
103 | |
104 |
|
105 |
|
106 | unstable_preventOverflow?: boolean;
|
107 | };
|
108 |
|
109 | export type PopoverStateReturn = DialogStateReturn &
|
110 | PopoverState &
|
111 | PopoverActions;
|
112 |
|
113 | function applyStyles(styles?: Partial<CSSStyleDeclaration>) {
|
114 | return (prevStyles: React.CSSProperties) => {
|
115 | if (styles && !shallowEqual(prevStyles, styles)) {
|
116 | return styles as React.CSSProperties;
|
117 | }
|
118 | return prevStyles;
|
119 | };
|
120 | }
|
121 |
|
122 | export function usePopoverState(
|
123 | initialState: SealedInitialState<PopoverInitialState> = {}
|
124 | ): PopoverStateReturn {
|
125 | const {
|
126 | gutter = 12,
|
127 | placement: sealedPlacement = "bottom",
|
128 | unstable_flip: flip = true,
|
129 | unstable_offset: sealedOffset,
|
130 | unstable_preventOverflow: preventOverflow = true,
|
131 | unstable_fixed: fixed = false,
|
132 | modal = false,
|
133 | ...sealed
|
134 | } = useSealedState(initialState);
|
135 |
|
136 | const popper = React.useRef<Instance | null>(null);
|
137 | const referenceRef = React.useRef<HTMLElement>(null);
|
138 | const popoverRef = React.useRef<HTMLElement>(null);
|
139 | const arrowRef = React.useRef<HTMLElement>(null);
|
140 |
|
141 | const [originalPlacement, place] = React.useState(sealedPlacement);
|
142 | const [placement, setPlacement] = React.useState(sealedPlacement);
|
143 | const [offset] = React.useState(sealedOffset || [0, gutter]);
|
144 | const [popoverStyles, setPopoverStyles] = React.useState<React.CSSProperties>(
|
145 | {
|
146 | position: "fixed",
|
147 | left: "100%",
|
148 | top: "100%",
|
149 | }
|
150 | );
|
151 | const [arrowStyles, setArrowStyles] = React.useState<React.CSSProperties>({});
|
152 |
|
153 | const dialog = useDialogState({ modal, ...sealed });
|
154 |
|
155 | const update = React.useCallback(() => {
|
156 | if (popper.current) {
|
157 | popper.current.forceUpdate();
|
158 | return true;
|
159 | }
|
160 | return false;
|
161 | }, []);
|
162 |
|
163 | const updateState = React.useCallback((state: Partial<State>) => {
|
164 | if (state.placement) {
|
165 | setPlacement(state.placement);
|
166 | }
|
167 | if (state.styles) {
|
168 | setPopoverStyles(applyStyles(state.styles.popper));
|
169 | if (arrowRef.current) {
|
170 | setArrowStyles(applyStyles(state.styles.arrow));
|
171 | }
|
172 | }
|
173 | }, []);
|
174 |
|
175 | useIsomorphicEffect(() => {
|
176 | if (referenceRef.current && popoverRef.current) {
|
177 | popper.current = createPopper(referenceRef.current, popoverRef.current, {
|
178 |
|
179 | placement: originalPlacement,
|
180 | strategy: fixed ? "fixed" : "absolute",
|
181 |
|
182 |
|
183 |
|
184 | onFirstUpdate: isSafari ? updateState : undefined,
|
185 | modifiers: [
|
186 | {
|
187 |
|
188 | name: "eventListeners",
|
189 | enabled: dialog.visible,
|
190 | },
|
191 | {
|
192 |
|
193 | name: "applyStyles",
|
194 | enabled: false,
|
195 | },
|
196 | {
|
197 |
|
198 | name: "flip",
|
199 | enabled: flip,
|
200 | options: { padding: 8 },
|
201 | },
|
202 | {
|
203 |
|
204 | name: "offset",
|
205 | options: { offset },
|
206 | },
|
207 | {
|
208 |
|
209 | name: "preventOverflow",
|
210 | enabled: preventOverflow,
|
211 | options: {
|
212 | tetherOffset: () => arrowRef.current?.clientWidth || 0,
|
213 | },
|
214 | },
|
215 | {
|
216 |
|
217 | name: "arrow",
|
218 | enabled: !!arrowRef.current,
|
219 | options: { element: arrowRef.current },
|
220 | },
|
221 | {
|
222 |
|
223 | name: "updateState",
|
224 | phase: "write",
|
225 | requires: ["computeStyles"],
|
226 | enabled: dialog.visible && process.env.NODE_ENV !== "test",
|
227 | fn: ({ state }) => updateState(state),
|
228 | },
|
229 | ],
|
230 | });
|
231 | }
|
232 | return () => {
|
233 | if (popper.current) {
|
234 | popper.current.destroy();
|
235 | popper.current = null;
|
236 | }
|
237 | };
|
238 | }, [originalPlacement, fixed, dialog.visible, flip, offset, preventOverflow]);
|
239 |
|
240 |
|
241 |
|
242 | React.useEffect(() => {
|
243 | if (!dialog.visible) return undefined;
|
244 | const id = window.requestAnimationFrame(() => {
|
245 | popper.current?.forceUpdate();
|
246 | });
|
247 | return () => {
|
248 | window.cancelAnimationFrame(id);
|
249 | };
|
250 | }, [dialog.visible]);
|
251 |
|
252 | return {
|
253 | ...dialog,
|
254 | unstable_referenceRef: referenceRef,
|
255 | unstable_popoverRef: popoverRef,
|
256 | unstable_arrowRef: arrowRef,
|
257 | unstable_popoverStyles: popoverStyles,
|
258 | unstable_arrowStyles: arrowStyles,
|
259 | unstable_update: update,
|
260 | unstable_originalPlacement: originalPlacement,
|
261 | placement,
|
262 | place,
|
263 | };
|
264 | }
|