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