@perspective-ai/sdk-react 1.3.0 → 1.4.0-pr-25-20260306124553
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +224 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +225 -108
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/useAutoOpen.test.ts +110 -13
- package/src/hooks/useAutoOpen.ts +63 -4
- package/src/hooks/useFloatBubble.ts +8 -4
- package/src/hooks/usePopup.test.ts +224 -55
- package/src/hooks/usePopup.ts +93 -62
- package/src/hooks/useSlider.test.ts +274 -41
- package/src/hooks/useSlider.ts +93 -62
- package/src/hooks/useStableValue.test.ts +108 -0
- package/src/hooks/useStableValue.ts +34 -0
|
@@ -3,34 +3,90 @@ import { renderHook, act, cleanup } from "@testing-library/react";
|
|
|
3
3
|
import { usePopup } from "./usePopup";
|
|
4
4
|
|
|
5
5
|
const mockDestroy = vi.fn();
|
|
6
|
-
const mockUnmount = vi.fn();
|
|
7
6
|
const mockUpdate = vi.fn();
|
|
7
|
+
const mockShow = vi.fn();
|
|
8
|
+
const mockHide = vi.fn();
|
|
9
|
+
let invokeSdkClose: (() => void) | null = null;
|
|
8
10
|
|
|
9
11
|
vi.mock("@perspective-ai/sdk", () => ({
|
|
10
|
-
openPopup: vi.fn(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
openPopup: vi.fn(
|
|
13
|
+
(config: {
|
|
14
|
+
researchId?: string;
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
host?: string;
|
|
17
|
+
_startHidden?: boolean;
|
|
18
|
+
}) => {
|
|
19
|
+
const currentConfig = { ...config };
|
|
20
|
+
let open = !config._startHidden;
|
|
21
|
+
|
|
22
|
+
const show = vi.fn(() => {
|
|
23
|
+
if (open) return;
|
|
24
|
+
mockShow();
|
|
25
|
+
open = true;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const hide = vi.fn(() => {
|
|
29
|
+
if (!open) return;
|
|
30
|
+
mockHide();
|
|
31
|
+
open = false;
|
|
32
|
+
currentConfig.onClose?.();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const destroy = vi.fn(() => {
|
|
36
|
+
mockDestroy();
|
|
37
|
+
const wasOpen = open;
|
|
38
|
+
open = false;
|
|
39
|
+
if (wasOpen) {
|
|
40
|
+
currentConfig.onClose?.();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const update = vi.fn(
|
|
45
|
+
(options: Partial<{ onClose?: () => void; host?: string }>) => {
|
|
46
|
+
mockUpdate(options);
|
|
47
|
+
Object.assign(currentConfig, options);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
invokeSdkClose = () => {
|
|
52
|
+
if (!open) return;
|
|
53
|
+
open = false;
|
|
54
|
+
currentConfig.onClose?.();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
unmount: destroy,
|
|
59
|
+
update,
|
|
60
|
+
destroy,
|
|
61
|
+
show,
|
|
62
|
+
hide,
|
|
63
|
+
get isOpen() {
|
|
64
|
+
return open;
|
|
65
|
+
},
|
|
66
|
+
researchId: config.researchId ?? "test-research-id",
|
|
67
|
+
type: "popup",
|
|
68
|
+
iframe: null,
|
|
69
|
+
container: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
),
|
|
19
73
|
}));
|
|
20
74
|
|
|
21
75
|
import { openPopup } from "@perspective-ai/sdk";
|
|
76
|
+
|
|
22
77
|
const mockOpenPopup = vi.mocked(openPopup);
|
|
23
78
|
|
|
24
79
|
describe("usePopup", () => {
|
|
25
80
|
beforeEach(() => {
|
|
26
81
|
vi.clearAllMocks();
|
|
82
|
+
invokeSdkClose = null;
|
|
27
83
|
});
|
|
28
84
|
|
|
29
85
|
afterEach(() => {
|
|
30
86
|
cleanup();
|
|
31
87
|
});
|
|
32
88
|
|
|
33
|
-
it("returns open, close, toggle functions and
|
|
89
|
+
it("returns open, close, toggle functions and initial state", () => {
|
|
34
90
|
const { result } = renderHook(() =>
|
|
35
91
|
usePopup({ researchId: "test-research-id" })
|
|
36
92
|
);
|
|
@@ -39,47 +95,37 @@ describe("usePopup", () => {
|
|
|
39
95
|
expect(result.current.close).toBeInstanceOf(Function);
|
|
40
96
|
expect(result.current.toggle).toBeInstanceOf(Function);
|
|
41
97
|
expect(result.current.isOpen).toBe(false);
|
|
42
|
-
expect(result.current.handle).toBeNull();
|
|
98
|
+
expect(result.current.handle).not.toBeNull();
|
|
43
99
|
});
|
|
44
100
|
|
|
45
|
-
it("
|
|
101
|
+
it("preloads a hidden popup on mount", () => {
|
|
46
102
|
renderHook(() => usePopup({ researchId: "test-research-id" }));
|
|
47
103
|
|
|
48
|
-
expect(mockOpenPopup).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("calls openPopup when open() is called", () => {
|
|
52
|
-
const { result } = renderHook(() =>
|
|
53
|
-
usePopup({ researchId: "test-research-id" })
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
act(() => {
|
|
57
|
-
result.current.open();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
104
|
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
61
105
|
expect(mockOpenPopup).toHaveBeenCalledWith(
|
|
62
106
|
expect.objectContaining({
|
|
63
107
|
researchId: "test-research-id",
|
|
108
|
+
_startHidden: true,
|
|
64
109
|
})
|
|
65
110
|
);
|
|
66
111
|
});
|
|
67
112
|
|
|
68
|
-
it("
|
|
113
|
+
it("opens the preloaded popup without recreating it", () => {
|
|
69
114
|
const { result } = renderHook(() =>
|
|
70
115
|
usePopup({ researchId: "test-research-id" })
|
|
71
116
|
);
|
|
72
117
|
|
|
73
|
-
expect(result.current.isOpen).toBe(false);
|
|
74
|
-
|
|
75
118
|
act(() => {
|
|
76
119
|
result.current.open();
|
|
77
120
|
});
|
|
78
121
|
|
|
122
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(mockShow).toHaveBeenCalledTimes(1);
|
|
79
124
|
expect(result.current.isOpen).toBe(true);
|
|
125
|
+
expect(result.current.handle).not.toBeNull();
|
|
80
126
|
});
|
|
81
127
|
|
|
82
|
-
it("
|
|
128
|
+
it("hides the popup on close but keeps the handle for instant reopen", () => {
|
|
83
129
|
const { result } = renderHook(() =>
|
|
84
130
|
usePopup({ researchId: "test-research-id" })
|
|
85
131
|
);
|
|
@@ -88,22 +134,37 @@ describe("usePopup", () => {
|
|
|
88
134
|
result.current.open();
|
|
89
135
|
});
|
|
90
136
|
|
|
91
|
-
expect(result.current.isOpen).toBe(true);
|
|
92
|
-
|
|
93
137
|
act(() => {
|
|
94
138
|
result.current.close();
|
|
95
139
|
});
|
|
96
140
|
|
|
97
|
-
expect(
|
|
141
|
+
expect(mockHide).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(mockDestroy).not.toHaveBeenCalled();
|
|
98
143
|
expect(result.current.isOpen).toBe(false);
|
|
144
|
+
expect(result.current.handle).not.toBeNull();
|
|
99
145
|
});
|
|
100
146
|
|
|
101
|
-
it("
|
|
147
|
+
it("reuses the same popup on reopen after close", () => {
|
|
102
148
|
const { result } = renderHook(() =>
|
|
103
149
|
usePopup({ researchId: "test-research-id" })
|
|
104
150
|
);
|
|
105
151
|
|
|
106
|
-
|
|
152
|
+
act(() => {
|
|
153
|
+
result.current.open();
|
|
154
|
+
result.current.close();
|
|
155
|
+
result.current.open();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(mockShow).toHaveBeenCalledTimes(2);
|
|
160
|
+
expect(mockHide).toHaveBeenCalledTimes(1);
|
|
161
|
+
expect(result.current.isOpen).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("toggles open state using the same underlying popup", () => {
|
|
165
|
+
const { result } = renderHook(() =>
|
|
166
|
+
usePopup({ researchId: "test-research-id" })
|
|
167
|
+
);
|
|
107
168
|
|
|
108
169
|
act(() => {
|
|
109
170
|
result.current.toggle();
|
|
@@ -111,20 +172,22 @@ describe("usePopup", () => {
|
|
|
111
172
|
|
|
112
173
|
expect(result.current.isOpen).toBe(true);
|
|
113
174
|
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(mockShow).toHaveBeenCalledTimes(1);
|
|
114
176
|
|
|
115
177
|
act(() => {
|
|
116
178
|
result.current.toggle();
|
|
117
179
|
});
|
|
118
180
|
|
|
119
181
|
expect(result.current.isOpen).toBe(false);
|
|
120
|
-
expect(
|
|
182
|
+
expect(mockHide).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(mockDestroy).not.toHaveBeenCalled();
|
|
121
184
|
});
|
|
122
185
|
|
|
123
|
-
it("passes config to openPopup", () => {
|
|
186
|
+
it("passes config to openPopup during preload creation", () => {
|
|
124
187
|
const onReady = vi.fn();
|
|
125
188
|
const onSubmit = vi.fn();
|
|
126
189
|
|
|
127
|
-
|
|
190
|
+
renderHook(() =>
|
|
128
191
|
usePopup({
|
|
129
192
|
researchId: "test-research-id",
|
|
130
193
|
params: { source: "test" },
|
|
@@ -135,22 +198,19 @@ describe("usePopup", () => {
|
|
|
135
198
|
})
|
|
136
199
|
);
|
|
137
200
|
|
|
138
|
-
act(() => {
|
|
139
|
-
result.current.open();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
201
|
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
143
202
|
const config = mockOpenPopup.mock.calls[0]![0];
|
|
144
203
|
expect(config.researchId).toBe("test-research-id");
|
|
145
204
|
expect(config.params).toEqual({ source: "test" });
|
|
146
205
|
expect(config.theme).toBe("dark");
|
|
147
206
|
expect(config.host).toBe("https://custom.example.com");
|
|
207
|
+
expect(config._startHidden).toBe(true);
|
|
148
208
|
});
|
|
149
209
|
|
|
150
|
-
it("supports controlled mode with open prop", () => {
|
|
210
|
+
it("supports controlled mode with open prop using show and hide", () => {
|
|
151
211
|
const onOpenChange = vi.fn();
|
|
152
212
|
|
|
153
|
-
const {
|
|
213
|
+
const { rerender } = renderHook(
|
|
154
214
|
({ open }) =>
|
|
155
215
|
usePopup({
|
|
156
216
|
researchId: "test-research-id",
|
|
@@ -160,15 +220,15 @@ describe("usePopup", () => {
|
|
|
160
220
|
{ initialProps: { open: false } }
|
|
161
221
|
);
|
|
162
222
|
|
|
163
|
-
expect(
|
|
223
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
164
224
|
|
|
165
225
|
rerender({ open: true });
|
|
166
|
-
|
|
167
226
|
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(mockShow).toHaveBeenCalledTimes(1);
|
|
168
228
|
|
|
169
229
|
rerender({ open: false });
|
|
170
|
-
|
|
171
|
-
expect(mockDestroy).
|
|
230
|
+
expect(mockHide).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(mockDestroy).not.toHaveBeenCalled();
|
|
172
232
|
});
|
|
173
233
|
|
|
174
234
|
it("calls onOpenChange in controlled mode when open() is called", () => {
|
|
@@ -189,31 +249,140 @@ describe("usePopup", () => {
|
|
|
189
249
|
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
190
250
|
});
|
|
191
251
|
|
|
192
|
-
it("
|
|
193
|
-
const {
|
|
252
|
+
it("destroys the popup on component unmount", () => {
|
|
253
|
+
const { unmount } = renderHook(() =>
|
|
194
254
|
usePopup({ researchId: "test-research-id" })
|
|
195
255
|
);
|
|
196
256
|
|
|
257
|
+
unmount();
|
|
258
|
+
|
|
259
|
+
expect(mockUpdate).toHaveBeenCalledWith({ onClose: undefined });
|
|
260
|
+
expect(mockDestroy).toHaveBeenCalledTimes(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("keeps the handle available after mount", () => {
|
|
264
|
+
const { result } = renderHook(() =>
|
|
265
|
+
usePopup({ researchId: "test-research-id" })
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(result.current.handle).not.toBeNull();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("updates state when the SDK triggers onClose without destroying the popup", () => {
|
|
272
|
+
const onClose = vi.fn();
|
|
273
|
+
const { result } = renderHook(() =>
|
|
274
|
+
usePopup({
|
|
275
|
+
researchId: "test-research-id",
|
|
276
|
+
onClose,
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
|
|
197
280
|
act(() => {
|
|
198
281
|
result.current.open();
|
|
199
282
|
});
|
|
200
283
|
|
|
201
|
-
|
|
284
|
+
act(() => {
|
|
285
|
+
invokeSdkClose?.();
|
|
286
|
+
});
|
|
202
287
|
|
|
203
|
-
expect(mockDestroy).toHaveBeenCalled();
|
|
288
|
+
expect(mockDestroy).not.toHaveBeenCalled();
|
|
289
|
+
expect(result.current.isOpen).toBe(false);
|
|
290
|
+
expect(result.current.handle).not.toBeNull();
|
|
291
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
204
292
|
});
|
|
205
293
|
|
|
206
|
-
it("
|
|
207
|
-
const {
|
|
208
|
-
|
|
294
|
+
it("recreates the popup when researchId changes", () => {
|
|
295
|
+
const { rerender } = renderHook(
|
|
296
|
+
({ researchId }) =>
|
|
297
|
+
usePopup({
|
|
298
|
+
researchId,
|
|
299
|
+
}),
|
|
300
|
+
{ initialProps: { researchId: "research-1" } }
|
|
209
301
|
);
|
|
210
302
|
|
|
211
|
-
expect(
|
|
303
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
304
|
+
|
|
305
|
+
rerender({ researchId: "research-2" });
|
|
306
|
+
|
|
307
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(2);
|
|
308
|
+
expect(mockDestroy).toHaveBeenCalledTimes(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("recreates a hidden popup when iframe-defining config changes", () => {
|
|
312
|
+
const onClose = vi.fn();
|
|
313
|
+
|
|
314
|
+
const { result, rerender } = renderHook(
|
|
315
|
+
({ params }) =>
|
|
316
|
+
usePopup({
|
|
317
|
+
researchId: "test-research-id",
|
|
318
|
+
params,
|
|
319
|
+
onClose,
|
|
320
|
+
}),
|
|
321
|
+
{ initialProps: { params: { source: "first" } } }
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(result.current.isOpen).toBe(false);
|
|
325
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
326
|
+
|
|
327
|
+
rerender({ params: { source: "second" } });
|
|
328
|
+
|
|
329
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(2);
|
|
330
|
+
expect(mockDestroy).toHaveBeenCalledTimes(1);
|
|
331
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
332
|
+
expect(result.current.isOpen).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("keeps controlled popup open when recreated while open", () => {
|
|
336
|
+
const onOpenChange = vi.fn();
|
|
337
|
+
|
|
338
|
+
const { result, rerender } = renderHook(
|
|
339
|
+
({ researchId }) =>
|
|
340
|
+
usePopup({
|
|
341
|
+
researchId,
|
|
342
|
+
open: true,
|
|
343
|
+
onOpenChange,
|
|
344
|
+
}),
|
|
345
|
+
{ initialProps: { researchId: "research-1" } }
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
349
|
+
expect(result.current.isOpen).toBe(true);
|
|
350
|
+
|
|
351
|
+
rerender({ researchId: "research-2" });
|
|
352
|
+
|
|
353
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(2);
|
|
354
|
+
expect(mockDestroy).toHaveBeenCalledTimes(1);
|
|
355
|
+
expect(result.current.handle).not.toBeNull();
|
|
356
|
+
expect(result.current.isOpen).toBe(true);
|
|
357
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("keeps uncontrolled popup open when recreated while open", () => {
|
|
361
|
+
const onClose = vi.fn();
|
|
362
|
+
|
|
363
|
+
const { result, rerender } = renderHook(
|
|
364
|
+
({ params }) =>
|
|
365
|
+
usePopup({
|
|
366
|
+
researchId: "test-research-id",
|
|
367
|
+
params,
|
|
368
|
+
onClose,
|
|
369
|
+
}),
|
|
370
|
+
{ initialProps: { params: { source: "first" } } }
|
|
371
|
+
);
|
|
212
372
|
|
|
213
373
|
act(() => {
|
|
214
374
|
result.current.open();
|
|
215
375
|
});
|
|
216
376
|
|
|
377
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(1);
|
|
378
|
+
expect(result.current.isOpen).toBe(true);
|
|
379
|
+
|
|
380
|
+
rerender({ params: { source: "second" } });
|
|
381
|
+
|
|
382
|
+
expect(mockOpenPopup).toHaveBeenCalledTimes(2);
|
|
383
|
+
expect(mockDestroy).toHaveBeenCalledTimes(1);
|
|
217
384
|
expect(result.current.handle).not.toBeNull();
|
|
385
|
+
expect(result.current.isOpen).toBe(true);
|
|
386
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
218
387
|
});
|
|
219
388
|
});
|
package/src/hooks/usePopup.ts
CHANGED
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
type EmbedHandle,
|
|
6
6
|
} from "@perspective-ai/sdk";
|
|
7
7
|
import { useStableCallback } from "./useStableCallback";
|
|
8
|
+
import { useStableValue } from "./useStableValue";
|
|
9
|
+
|
|
10
|
+
type PopupHandle = ReturnType<typeof openPopup>;
|
|
8
11
|
|
|
9
12
|
/** Options for usePopup hook */
|
|
10
13
|
export interface UsePopupOptions extends Omit<EmbedConfig, "type"> {
|
|
@@ -24,7 +27,7 @@ export interface UsePopupReturn {
|
|
|
24
27
|
toggle: () => void;
|
|
25
28
|
/** Whether the popup is currently open */
|
|
26
29
|
isOpen: boolean;
|
|
27
|
-
/** The underlying SDK handle (null
|
|
30
|
+
/** The underlying SDK handle (created on mount; may be null during initial render) */
|
|
28
31
|
handle: EmbedHandle | null;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -65,10 +68,14 @@ export function usePopup(options: UsePopupOptions): UsePopupReturn {
|
|
|
65
68
|
|
|
66
69
|
const [handle, setHandle] = useState<EmbedHandle | null>(null);
|
|
67
70
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
68
|
-
const handleRef = useRef<
|
|
71
|
+
const handleRef = useRef<PopupHandle | null>(null);
|
|
69
72
|
|
|
70
73
|
const isControlled = controlledOpen !== undefined;
|
|
71
74
|
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
75
|
+
const initialHiddenRef = useRef(!isOpen);
|
|
76
|
+
|
|
77
|
+
const stableParams = useStableValue(params);
|
|
78
|
+
const stableBrand = useStableValue(brand);
|
|
72
79
|
|
|
73
80
|
const stableOnReady = useStableCallback(onReady);
|
|
74
81
|
const stableOnSubmit = useStableCallback(onSubmit);
|
|
@@ -86,72 +93,115 @@ export function usePopup(options: UsePopupOptions): UsePopupReturn {
|
|
|
86
93
|
[isControlled, onOpenChange]
|
|
87
94
|
);
|
|
88
95
|
|
|
96
|
+
const destroyPopup = useCallback(
|
|
97
|
+
(targetHandle: PopupHandle | null = handleRef.current) => {
|
|
98
|
+
if (!targetHandle) return;
|
|
99
|
+
|
|
100
|
+
targetHandle.update({ onClose: undefined });
|
|
101
|
+
targetHandle.destroy();
|
|
102
|
+
|
|
103
|
+
if (handleRef.current === targetHandle) {
|
|
104
|
+
handleRef.current = null;
|
|
105
|
+
setHandle(null);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[]
|
|
109
|
+
);
|
|
110
|
+
|
|
89
111
|
const handleClose = useCallback(() => {
|
|
90
|
-
handleRef.current = null;
|
|
91
|
-
setHandle(null);
|
|
92
112
|
setOpen(false);
|
|
93
113
|
onClose?.();
|
|
94
114
|
}, [setOpen, onClose]);
|
|
95
115
|
|
|
96
116
|
const stableOnClose = useStableCallback(handleClose);
|
|
97
117
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
const createPopupHandle = useCallback(
|
|
119
|
+
(startHidden: boolean) =>
|
|
120
|
+
openPopup({
|
|
121
|
+
researchId,
|
|
122
|
+
params: stableParams,
|
|
123
|
+
brand: stableBrand,
|
|
124
|
+
theme,
|
|
125
|
+
host,
|
|
126
|
+
onReady: stableOnReady,
|
|
127
|
+
onSubmit: stableOnSubmit,
|
|
128
|
+
onNavigate: stableOnNavigate,
|
|
129
|
+
onClose: stableOnClose,
|
|
130
|
+
onError: stableOnError,
|
|
131
|
+
_startHidden: startHidden,
|
|
132
|
+
}),
|
|
133
|
+
[
|
|
102
134
|
researchId,
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
stableParams,
|
|
136
|
+
stableBrand,
|
|
105
137
|
theme,
|
|
106
138
|
host,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
handleRef.current = null;
|
|
134
|
-
setHandle(null);
|
|
139
|
+
stableOnReady,
|
|
140
|
+
stableOnSubmit,
|
|
141
|
+
stableOnNavigate,
|
|
142
|
+
stableOnClose,
|
|
143
|
+
stableOnError,
|
|
144
|
+
]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const setPopupHandle = useCallback(
|
|
148
|
+
(startHidden: boolean) => {
|
|
149
|
+
const newHandle = createPopupHandle(startHidden);
|
|
150
|
+
handleRef.current = newHandle;
|
|
151
|
+
setHandle(newHandle);
|
|
152
|
+
return newHandle;
|
|
153
|
+
},
|
|
154
|
+
[createPopupHandle]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const currentHandle = handleRef.current;
|
|
159
|
+
if (!currentHandle) return;
|
|
160
|
+
|
|
161
|
+
if (isOpen) {
|
|
162
|
+
currentHandle.show();
|
|
163
|
+
} else {
|
|
164
|
+
currentHandle.hide();
|
|
135
165
|
}
|
|
136
|
-
}, []);
|
|
166
|
+
}, [isOpen]);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
// Stable callback/value wrappers are part of this hook's contract.
|
|
170
|
+
// When any iframe-defining input changes, recreate the underlying embed.
|
|
171
|
+
const nextHandle = setPopupHandle(
|
|
172
|
+
handleRef.current ? !handleRef.current.isOpen : initialHiddenRef.current
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
if (handleRef.current === nextHandle) {
|
|
177
|
+
destroyPopup(nextHandle);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, [destroyPopup, setPopupHandle]);
|
|
181
|
+
|
|
182
|
+
const ensurePopup = useCallback(() => {
|
|
183
|
+
if (handleRef.current) return handleRef.current;
|
|
184
|
+
|
|
185
|
+
return setPopupHandle(false);
|
|
186
|
+
}, [setPopupHandle]);
|
|
137
187
|
|
|
138
188
|
const openFn = useCallback(() => {
|
|
139
189
|
if (isControlled) {
|
|
140
190
|
onOpenChange?.(true);
|
|
141
191
|
} else {
|
|
142
|
-
|
|
192
|
+
ensurePopup().show();
|
|
143
193
|
setInternalOpen(true);
|
|
144
194
|
}
|
|
145
|
-
}, [isControlled, onOpenChange
|
|
195
|
+
}, [ensurePopup, isControlled, onOpenChange]);
|
|
146
196
|
|
|
147
197
|
const closeFn = useCallback(() => {
|
|
148
198
|
if (isControlled) {
|
|
149
199
|
onOpenChange?.(false);
|
|
150
200
|
} else {
|
|
151
|
-
|
|
201
|
+
handleRef.current?.hide();
|
|
152
202
|
setInternalOpen(false);
|
|
153
203
|
}
|
|
154
|
-
}, [isControlled, onOpenChange
|
|
204
|
+
}, [isControlled, onOpenChange]);
|
|
155
205
|
|
|
156
206
|
const toggleFn = useCallback(() => {
|
|
157
207
|
if (isOpen) {
|
|
@@ -161,25 +211,6 @@ export function usePopup(options: UsePopupOptions): UsePopupReturn {
|
|
|
161
211
|
}
|
|
162
212
|
}, [isOpen, openFn, closeFn]);
|
|
163
213
|
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
if (!isControlled) return;
|
|
166
|
-
|
|
167
|
-
if (controlledOpen && !handleRef.current) {
|
|
168
|
-
createPopup();
|
|
169
|
-
} else if (!controlledOpen && handleRef.current) {
|
|
170
|
-
destroyPopup();
|
|
171
|
-
}
|
|
172
|
-
}, [controlledOpen, isControlled, createPopup, destroyPopup]);
|
|
173
|
-
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
return () => {
|
|
176
|
-
if (handleRef.current) {
|
|
177
|
-
handleRef.current.destroy();
|
|
178
|
-
handleRef.current = null;
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}, []);
|
|
182
|
-
|
|
183
214
|
return {
|
|
184
215
|
open: openFn,
|
|
185
216
|
close: closeFn,
|