@window-splitter/react 0.8.0

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.
Files changed (81) hide show
  1. package/.storybook/main.ts +17 -0
  2. package/.storybook/preview.ts +14 -0
  3. package/.tshy/build.json +8 -0
  4. package/.tshy/commonjs.json +19 -0
  5. package/.tshy/esm.json +18 -0
  6. package/.turbo/turbo-build.log +5 -0
  7. package/.turbo/turbo-lint.log +5 -0
  8. package/CHANGELOG.md +606 -0
  9. package/README.md +53 -0
  10. package/dist/commonjs/ReactWindowSplitter.d.ts +19 -0
  11. package/dist/commonjs/ReactWindowSplitter.d.ts.map +1 -0
  12. package/dist/commonjs/ReactWindowSplitter.js +558 -0
  13. package/dist/commonjs/ReactWindowSplitter.js.map +1 -0
  14. package/dist/commonjs/ReactWindowSplitter.stories.d.ts +47 -0
  15. package/dist/commonjs/ReactWindowSplitter.stories.d.ts.map +1 -0
  16. package/dist/commonjs/ReactWindowSplitter.stories.js +330 -0
  17. package/dist/commonjs/ReactWindowSplitter.stories.js.map +1 -0
  18. package/dist/commonjs/ReactWindowSplitter.test.d.ts +2 -0
  19. package/dist/commonjs/ReactWindowSplitter.test.d.ts.map +1 -0
  20. package/dist/commonjs/ReactWindowSplitter.test.js +219 -0
  21. package/dist/commonjs/ReactWindowSplitter.test.js.map +1 -0
  22. package/dist/commonjs/bundle.size.d.ts +2 -0
  23. package/dist/commonjs/bundle.size.d.ts.map +1 -0
  24. package/dist/commonjs/bundle.size.js +29 -0
  25. package/dist/commonjs/bundle.size.js.map +1 -0
  26. package/dist/commonjs/index.d.ts +3 -0
  27. package/dist/commonjs/index.d.ts.map +1 -0
  28. package/dist/commonjs/index.js +18 -0
  29. package/dist/commonjs/index.js.map +1 -0
  30. package/dist/commonjs/package.json +3 -0
  31. package/dist/commonjs/useIndexedChildren.d.ts +8 -0
  32. package/dist/commonjs/useIndexedChildren.d.ts.map +1 -0
  33. package/dist/commonjs/useIndexedChildren.js +81 -0
  34. package/dist/commonjs/useIndexedChildren.js.map +1 -0
  35. package/dist/commonjs/useMove.d.ts +8 -0
  36. package/dist/commonjs/useMove.d.ts.map +1 -0
  37. package/dist/commonjs/useMove.js +19 -0
  38. package/dist/commonjs/useMove.js.map +1 -0
  39. package/dist/esm/ReactWindowSplitter.d.ts +19 -0
  40. package/dist/esm/ReactWindowSplitter.d.ts.map +1 -0
  41. package/dist/esm/ReactWindowSplitter.js +532 -0
  42. package/dist/esm/ReactWindowSplitter.js.map +1 -0
  43. package/dist/esm/ReactWindowSplitter.stories.d.ts +47 -0
  44. package/dist/esm/ReactWindowSplitter.stories.d.ts.map +1 -0
  45. package/dist/esm/ReactWindowSplitter.stories.js +286 -0
  46. package/dist/esm/ReactWindowSplitter.stories.js.map +1 -0
  47. package/dist/esm/ReactWindowSplitter.test.d.ts +2 -0
  48. package/dist/esm/ReactWindowSplitter.test.d.ts.map +1 -0
  49. package/dist/esm/ReactWindowSplitter.test.js +191 -0
  50. package/dist/esm/ReactWindowSplitter.test.js.map +1 -0
  51. package/dist/esm/bundle.size.d.ts +2 -0
  52. package/dist/esm/bundle.size.d.ts.map +1 -0
  53. package/dist/esm/bundle.size.js +4 -0
  54. package/dist/esm/bundle.size.js.map +1 -0
  55. package/dist/esm/index.d.ts +3 -0
  56. package/dist/esm/index.d.ts.map +1 -0
  57. package/dist/esm/index.js +2 -0
  58. package/dist/esm/index.js.map +1 -0
  59. package/dist/esm/package.json +3 -0
  60. package/dist/esm/useIndexedChildren.d.ts +8 -0
  61. package/dist/esm/useIndexedChildren.d.ts.map +1 -0
  62. package/dist/esm/useIndexedChildren.js +54 -0
  63. package/dist/esm/useIndexedChildren.js.map +1 -0
  64. package/dist/esm/useMove.d.ts +8 -0
  65. package/dist/esm/useMove.d.ts.map +1 -0
  66. package/dist/esm/useMove.js +16 -0
  67. package/dist/esm/useMove.js.map +1 -0
  68. package/eslint.config.js +3 -0
  69. package/package.json +108 -0
  70. package/rsbuild.config.js +25 -0
  71. package/src/ReactWindowSplitter.stories.tsx +705 -0
  72. package/src/ReactWindowSplitter.test.tsx +384 -0
  73. package/src/ReactWindowSplitter.tsx +884 -0
  74. package/src/__snapshots__/ReactWindowSplitter.test.tsx.snap +58 -0
  75. package/src/bundle.size.ts +4 -0
  76. package/src/index.ts +2 -0
  77. package/src/useIndexedChildren.tsx +79 -0
  78. package/src/useMove.ts +22 -0
  79. package/tsconfig.json +6 -0
  80. package/vitest.config.ts +20 -0
  81. package/vitest.setup.ts +7 -0
@@ -0,0 +1,884 @@
1
+ "use client";
2
+
3
+ import {
4
+ getPanelDomAttributes,
5
+ getPanelResizerDomAttributes,
6
+ measureGroupChildren,
7
+ PanelGroupHandle,
8
+ PanelHandle,
9
+ SharedPanelGroupProps,
10
+ SharedPanelProps,
11
+ SharedPanelResizerProps,
12
+ mergeAttributes,
13
+ } from "@window-splitter/interface";
14
+ import React, {
15
+ useEffect,
16
+ useImperativeHandle,
17
+ createContext,
18
+ useRef,
19
+ useState,
20
+ useMemo,
21
+ useContext,
22
+ } from "react";
23
+ import {
24
+ buildTemplate,
25
+ getCollapsiblePanelForHandleId,
26
+ getGroupSize,
27
+ getPanelWithId,
28
+ groupMachine,
29
+ GroupMachineContextValue,
30
+ initializePanel,
31
+ InitializePanelHandleData,
32
+ isPanelData,
33
+ Item,
34
+ PanelData,
35
+ parseUnit,
36
+ prepareItems,
37
+ prepareSnapshot,
38
+ getPanelGroupPixelSizes,
39
+ getPanelGroupPercentageSizes,
40
+ getPanelPixelSize,
41
+ getPanelPercentageSize,
42
+ getCursor,
43
+ haveConstraintsChangedForPanel,
44
+ PanelHandleData,
45
+ ParsedPixelUnit,
46
+ haveConstraintsChangedForPanelHandle,
47
+ GroupMachineInput,
48
+ GroupMachineEvent,
49
+ State,
50
+ initializePanelHandleData,
51
+ } from "@window-splitter/state";
52
+ import {
53
+ useEffectEvent,
54
+ useId,
55
+ useLayoutEffect,
56
+ mergeRefs,
57
+ } from "@react-aria/utils";
58
+ import { useIndex, useIndexedChildren } from "./useIndexedChildren.js";
59
+ import { useMove } from "./useMove.js";
60
+
61
+ // #region Components
62
+
63
+ const GroupMachineState = createContext<{ current: State | undefined }>({
64
+ current: undefined,
65
+ });
66
+ const GroupMachineContext = createContext<GroupMachineContextValue | undefined>(
67
+ undefined
68
+ );
69
+ const GroupMachineStateContextRef = createContext<
70
+ React.MutableRefObject<GroupMachineContextValue>
71
+ >({
72
+ current: undefined,
73
+ } as unknown as React.MutableRefObject<GroupMachineContextValue>);
74
+ const GroupMachineActor = createContext<(e: GroupMachineEvent) => void>(
75
+ () => {}
76
+ );
77
+ const GroupMachine = {
78
+ useSelector<R>(
79
+ selector: (data: { context: GroupMachineContextValue }) => R
80
+ ): R {
81
+ const context = useContext(GroupMachineContext);
82
+ if (!context) {
83
+ throw new Error(
84
+ "GroupMachineContext must be used within a GroupMachineProvider"
85
+ );
86
+ }
87
+ return selector({ context });
88
+ },
89
+ useActorRef: () => {
90
+ const send = useContext(GroupMachineActor);
91
+ return { send };
92
+ },
93
+ useContextRef: () => {
94
+ const ref = useContext(GroupMachineStateContextRef);
95
+ if (!ref) {
96
+ throw new Error(
97
+ "GroupMachineContext must be used within a GroupMachineProvider"
98
+ );
99
+ }
100
+ return ref;
101
+ },
102
+ Provider: ({
103
+ input,
104
+ children,
105
+ }: {
106
+ input: GroupMachineInput;
107
+ children: React.ReactNode;
108
+ }) => {
109
+ const [intiialValue, send, state] = useMemo(
110
+ () =>
111
+ groupMachine(input, (value) => {
112
+ currentContextRef.current = value;
113
+ setCurrentValue({ ...value });
114
+ }),
115
+ // We only want this to run once, we dont care about changes to the input
116
+ // eslint-disable-next-line react-hooks/exhaustive-deps
117
+ []
118
+ );
119
+ const currentContextRef = useRef(intiialValue);
120
+ const [currentValue, setCurrentValue] = useState(intiialValue);
121
+
122
+ useEffect(() => {
123
+ send({ type: "unlockGroup" });
124
+
125
+ return () => {
126
+ // When we were on xstate we could fire off event during unmount and if the parent group
127
+ // unmounted too the events would never apply. With out own state machine code it doesn't
128
+ // do that so we have to lock the group as it unmounts so it's children don't de-register.
129
+ send({ type: "lockGroup" });
130
+ };
131
+ }, [send]);
132
+
133
+ return (
134
+ <GroupMachineState.Provider value={state}>
135
+ <GroupMachineStateContextRef.Provider value={currentContextRef}>
136
+ <GroupMachineContext.Provider value={currentValue}>
137
+ <GroupMachineActor.Provider value={send}>
138
+ {children}
139
+ </GroupMachineActor.Provider>
140
+ </GroupMachineContext.Provider>
141
+ </GroupMachineStateContextRef.Provider>
142
+ </GroupMachineState.Provider>
143
+ );
144
+ },
145
+ };
146
+
147
+ // function useDebugGroupMachineContext({ id }: { id: string }) {
148
+ // const value = GroupMachineContext.useSelector((state) => state.value);
149
+ // const context = GroupMachineContext.useSelector((state) => state.context);
150
+ // console.log(
151
+ // "GROUP CONTEXT",
152
+ // buildTemplate(context),
153
+ // context.size,
154
+ // context.items.map((item) =>
155
+ // isPanelData(item)
156
+ // ? {
157
+ // type: item.currentValue.type,
158
+ // value: item.currentValue.value.toNumber(),
159
+ // }
160
+ // : { type: item.size.type, value: item.size.value.toNumber() }
161
+ // )
162
+ // );
163
+ // }
164
+
165
+ export interface PanelGroupProps
166
+ extends React.HTMLAttributes<HTMLDivElement>,
167
+ SharedPanelGroupProps {
168
+ /** Imperative handle to control the group */
169
+ handle?: React.Ref<PanelGroupHandle>;
170
+ }
171
+
172
+ const InitialMapContext = createContext<Item[]>([]);
173
+ const PreRenderContext = createContext(false);
174
+
175
+ function PrerenderTree({
176
+ children,
177
+ onPrerender,
178
+ }: {
179
+ children: React.ReactNode;
180
+ onPrerender: () => void;
181
+ }) {
182
+ const [shouldPrerender, setShouldPrerender] = React.useState(true);
183
+
184
+ useLayoutEffect(() => {
185
+ setShouldPrerender(false);
186
+ onPrerender();
187
+ // We only want this to run once
188
+ // eslint-disable-next-line react-hooks/exhaustive-deps
189
+ }, []);
190
+
191
+ return shouldPrerender ? (
192
+ <div className="opacity-0 absolute">
193
+ <PreRenderContext.Provider value>{children}</PreRenderContext.Provider>
194
+ </div>
195
+ ) : null;
196
+ }
197
+
198
+ function useGroupItem<T extends Item>(
199
+ itemArg: Omit<T, "id"> & { id?: string }
200
+ ): T {
201
+ const isPrerender = React.useContext(PreRenderContext);
202
+ const initialMap = React.useContext(InitialMapContext);
203
+ const generatedId = useId();
204
+ const id = itemArg.id || generatedId;
205
+ const { index } = useIndex();
206
+ const item = { ...itemArg, id } as T;
207
+
208
+ if (isPrerender) {
209
+ if (!initialMap.find((i) => i.id === item.id)) {
210
+ initialMap.push(item);
211
+ }
212
+ return item;
213
+ }
214
+
215
+ // The way this hooks is called is never conditional so the usage here is fine
216
+ /* eslint-disable react-hooks/rules-of-hooks */
217
+ const currentItem = GroupMachine.useSelector(
218
+ ({ context }) => context.items[index]
219
+ ) as T;
220
+ const { send } = GroupMachine.useActorRef();
221
+ const machineRef = GroupMachine.useContextRef();
222
+
223
+ const onCollapseChangeRef = isPanelData(itemArg)
224
+ ? itemArg.onCollapseChange
225
+ : undefined;
226
+ const onResizeRef = isPanelData(itemArg) ? itemArg.onResize : undefined;
227
+ const onUnmountRef = useRef<(() => void) | undefined>(undefined);
228
+
229
+ // We register panels before layout so it looks good
230
+ useLayoutEffect(() => {
231
+ const context = machineRef.current;
232
+
233
+ if (!context) {
234
+ return;
235
+ }
236
+
237
+ let contextItem: Item | undefined;
238
+
239
+ if (itemArg.id) {
240
+ contextItem = context.items.find((i) => i.id === itemArg.id);
241
+
242
+ if (!contextItem) {
243
+ if (!itemArg.id) {
244
+ throw new Error(
245
+ "When using dynamic panels you must provide an id on the items. This applies to React strict mode as well."
246
+ );
247
+ }
248
+
249
+ if (isPanelData(itemArg)) {
250
+ send({
251
+ type: "registerDynamicPanel",
252
+ data: { ...itemArg, order: index },
253
+ });
254
+ } else {
255
+ send({
256
+ type: "registerPanelHandle",
257
+ data: initializePanelHandleData({
258
+ ...(itemArg as unknown as InitializePanelHandleData),
259
+ order: index,
260
+ }),
261
+ });
262
+ }
263
+ } else if (onCollapseChangeRef || onResizeRef) {
264
+ send({
265
+ type: "rebindPanelCallbacks",
266
+ data: {
267
+ id: itemArg.id,
268
+ onCollapseChange: onCollapseChangeRef,
269
+ onResize: onResizeRef,
270
+ },
271
+ });
272
+ }
273
+ } else {
274
+ contextItem = context.items[index];
275
+ }
276
+
277
+ const unmountId = contextItem?.id || itemArg.id;
278
+
279
+ onUnmountRef.current = () => {
280
+ if (!unmountId) return;
281
+
282
+ const el = document.querySelector(
283
+ `[data-splitter-id="${unmountId}"]`
284
+ ) as HTMLElement;
285
+
286
+ if (el || !unmountId) {
287
+ return;
288
+ }
289
+
290
+ if (isPanelData(itemArg)) {
291
+ send({ type: "unregisterPanel", id: unmountId });
292
+ } else {
293
+ send({ type: "unregisterPanelHandle", id: unmountId });
294
+ }
295
+ };
296
+ }, [index, itemArg, machineRef, send, onCollapseChangeRef, onResizeRef]);
297
+
298
+ // And unregister after layout so we can tell if the element was actually removed.
299
+ React.useEffect(() => {
300
+ return () => {
301
+ onUnmountRef.current?.();
302
+ onUnmountRef.current = undefined;
303
+ };
304
+ }, []);
305
+
306
+ return currentItem || item;
307
+ /* eslint-enable react-hooks/rules-of-hooks */
308
+ }
309
+
310
+ /** A group of panels that has constraints and a user can resize */
311
+ export const PanelGroup = React.forwardRef<HTMLDivElement, PanelGroupProps>(
312
+ function PanelGroup({ children, ...props }, ref) {
313
+ const [hasPreRendered, setHasPreRendered] = useState(false);
314
+ const initialMap = useRef<Item[]>([]);
315
+ const indexedChildren = useIndexedChildren(children);
316
+
317
+ return (
318
+ <InitialMapContext.Provider value={initialMap.current}>
319
+ {!hasPreRendered && (
320
+ <PrerenderTree onPrerender={() => setHasPreRendered(true)}>
321
+ {indexedChildren}
322
+ </PrerenderTree>
323
+ )}
324
+
325
+ <PanelGroupImpl ref={ref} initialItems={initialMap} {...props}>
326
+ {indexedChildren}
327
+ </PanelGroupImpl>
328
+ </InitialMapContext.Provider>
329
+ );
330
+ }
331
+ );
332
+
333
+ const PanelGroupImpl = React.forwardRef<
334
+ HTMLDivElement,
335
+ PanelGroupProps & {
336
+ initialItems: { current: Item[] };
337
+ }
338
+ >(function PanelGroupImpl(
339
+ {
340
+ autosaveId,
341
+ autosaveStrategy = "localStorage",
342
+ snapshot: snapshotProp,
343
+ initialItems,
344
+ ...props
345
+ },
346
+ ref
347
+ ) {
348
+ const defaultGroupId = `panel-group-${useId()}`;
349
+ const groupId = autosaveId || props.id || defaultGroupId;
350
+ const [snapshot, setSnapshot] = React.useState<
351
+ GroupMachineContextValue | true | undefined
352
+ >(snapshotProp);
353
+
354
+ if (
355
+ typeof window !== "undefined" &&
356
+ autosaveId &&
357
+ !snapshot &&
358
+ autosaveStrategy === "localStorage"
359
+ ) {
360
+ const localSnapshot = localStorage.getItem(autosaveId);
361
+
362
+ if (localSnapshot) {
363
+ setSnapshot(JSON.parse(localSnapshot));
364
+ } else {
365
+ setSnapshot(true);
366
+ }
367
+ }
368
+
369
+ const snapshotMemo = useMemo(() => {
370
+ return typeof snapshot === "object" ? prepareSnapshot(snapshot) : undefined;
371
+ }, [snapshot]);
372
+
373
+ return (
374
+ <GroupMachine.Provider
375
+ input={{
376
+ orientation: props.orientation,
377
+ groupId,
378
+ items: initialItems.current,
379
+ autosaveStrategy,
380
+ ...(typeof snapshotMemo === "object" ? snapshotMemo : undefined),
381
+ }}
382
+ >
383
+ <PanelGroupImplementation ref={ref} {...props} />
384
+ </GroupMachine.Provider>
385
+ );
386
+ });
387
+
388
+ const PanelGroupImplementation = React.forwardRef<
389
+ HTMLDivElement,
390
+ PanelGroupProps
391
+ >(function PanelGroupImplementation(
392
+ { handle, orientation: orientationProp, ...props },
393
+ outerRef
394
+ ) {
395
+ const { send } = GroupMachine.useActorRef();
396
+ const machineRef = GroupMachine.useContextRef();
397
+ const innerRef = React.useRef<HTMLDivElement>(null);
398
+ const ref = mergeRefs(outerRef, innerRef);
399
+ const orientation = GroupMachine.useSelector(
400
+ (state) => state.context.orientation
401
+ );
402
+ const groupId = GroupMachine.useSelector((state) => state.context.groupId);
403
+ const template = GroupMachine.useSelector((state) =>
404
+ buildTemplate(state.context)
405
+ );
406
+
407
+ // When the prop for `orientation` updates also update the state machine
408
+ if (orientationProp && orientationProp !== orientation) {
409
+ send({ type: "setOrientation", orientation: orientationProp });
410
+ }
411
+
412
+ // Track the size of the group
413
+ useLayoutEffect(() => {
414
+ const { current: el } = innerRef;
415
+
416
+ if (!el) {
417
+ return;
418
+ }
419
+
420
+ const observer = new ResizeObserver((entries) => {
421
+ const entry = entries[0];
422
+
423
+ if (!entry) {
424
+ return;
425
+ }
426
+
427
+ send({ type: "setSize", size: entry.contentRect });
428
+ });
429
+
430
+ observer.observe(el);
431
+
432
+ send({ type: "setSize", size: el.getBoundingClientRect() });
433
+
434
+ return () => {
435
+ observer.disconnect();
436
+ };
437
+ }, [send, innerRef, groupId]);
438
+
439
+ const childIds = GroupMachine.useSelector((state) =>
440
+ state.context.items.map((i) => i.id).join(",")
441
+ );
442
+ useLayoutEffect(() => {
443
+ return measureGroupChildren(groupId, (childrenSizes) => {
444
+ send({ type: "setActualItemsSize", childrenSizes });
445
+ });
446
+ }, [send, groupId, childIds]);
447
+
448
+ // useDebugGroupMachineContext({ id: groupId });
449
+
450
+ const fallbackHandleRef = React.useRef<PanelGroupHandle>(null);
451
+ const state = useContext(GroupMachineState);
452
+
453
+ useImperativeHandle(handle || fallbackHandleRef, () => {
454
+ return {
455
+ getId: () => groupId,
456
+ getPixelSizes: () => getPanelGroupPixelSizes(machineRef.current),
457
+ getPercentageSizes: () =>
458
+ getPanelGroupPercentageSizes(machineRef.current),
459
+ setSizes: (updates) => {
460
+ const context = machineRef.current;
461
+
462
+ for (let index = 0; index < updates.length; index++) {
463
+ const item = context.items[index];
464
+ const update = updates[index];
465
+
466
+ if (item && isPanelData(item) && update) {
467
+ send({
468
+ type: "setPanelPixelSize",
469
+ panelId: item.id,
470
+ size: update,
471
+ });
472
+ }
473
+ }
474
+ },
475
+ getTemplate: () => {
476
+ const context = machineRef.current;
477
+ return buildTemplate({ ...context, items: prepareItems(context) });
478
+ },
479
+ getState: () => (state.current === "idle" ? "idle" : "dragging"),
480
+ };
481
+ });
482
+
483
+ return (
484
+ <div
485
+ ref={ref}
486
+ data-group-id={groupId}
487
+ data-group-orientation={orientation}
488
+ {...mergeAttributes(props, {
489
+ style: {
490
+ display: "grid",
491
+ gridTemplateColumns:
492
+ orientation === "horizontal" ? template : undefined,
493
+ gridTemplateRows: orientation === "vertical" ? template : undefined,
494
+ height: "100%",
495
+ },
496
+ })}
497
+ />
498
+ );
499
+ });
500
+
501
+ export interface PanelProps
502
+ extends SharedPanelProps<boolean>,
503
+ Omit<React.HTMLAttributes<HTMLDivElement>, "onResize"> {
504
+ /** Imperative handle to control the panel */
505
+ handle?: React.Ref<PanelHandle>;
506
+ }
507
+
508
+ /** A panel within a `PanelGroup` */
509
+ export const Panel = React.forwardRef<HTMLDivElement, PanelProps>(
510
+ function Panel(
511
+ {
512
+ defaultCollapsed,
513
+ min,
514
+ max,
515
+ default: defaultSize,
516
+ collapsedSize,
517
+ onCollapseChange,
518
+ onResize,
519
+ collapseAnimation,
520
+ isStaticAtRest,
521
+ ...props
522
+ },
523
+ outerRef
524
+ ) {
525
+ const { collapsible = false, collapsed } = props;
526
+ const isPrerender = React.useContext(PreRenderContext);
527
+ const onCollapseChangeRef = React.useRef(onCollapseChange);
528
+ useEffect(() => {
529
+ onCollapseChangeRef.current = onCollapseChange;
530
+ }, [onCollapseChange]);
531
+ const onResizeRef = React.useRef(onResize);
532
+ useEffect(() => {
533
+ onResizeRef.current = onResize;
534
+ }, [onResize]);
535
+ const panelDataRef = React.useMemo(() => {
536
+ return initializePanel({
537
+ min: min,
538
+ max: max,
539
+ collapsible: collapsible,
540
+ collapsed: collapsed,
541
+ collapsedSize: collapsedSize,
542
+ onCollapseChange: onCollapseChangeRef,
543
+ collapseAnimation: collapseAnimation,
544
+ onResize: onResizeRef,
545
+ id: props.id,
546
+ defaultCollapsed,
547
+ default: defaultSize,
548
+ isStaticAtRest,
549
+ });
550
+ }, [
551
+ collapseAnimation,
552
+ collapsed,
553
+ collapsedSize,
554
+ collapsible,
555
+ defaultCollapsed,
556
+ max,
557
+ min,
558
+ props.id,
559
+ defaultSize,
560
+ isStaticAtRest,
561
+ ]);
562
+
563
+ const { id: panelId } = useGroupItem(panelDataRef);
564
+
565
+ if (isPrerender) {
566
+ return null;
567
+ }
568
+
569
+ return (
570
+ <PanelVisible
571
+ ref={outerRef}
572
+ panelProp={panelDataRef}
573
+ {...props}
574
+ panelId={panelId}
575
+ />
576
+ );
577
+ }
578
+ );
579
+
580
+ const PanelVisible = React.forwardRef<
581
+ HTMLDivElement,
582
+ Omit<
583
+ PanelProps,
584
+ | "collapsedSize"
585
+ | "onCollapseChange"
586
+ | "defaultCollapsed"
587
+ | "min"
588
+ | "max"
589
+ | "collapseAnimation"
590
+ | "onResize"
591
+ > & {
592
+ panelId: string;
593
+ /** The latest prop that was received by the `Panel` component */
594
+ panelProp: Omit<PanelData, "id">;
595
+ }
596
+ >(function PanelVisible(
597
+ { collapsible = false, collapsed, handle, panelId, panelProp, ...props },
598
+ outerRef
599
+ ) {
600
+ const innerRef = React.useRef<HTMLDivElement>(null);
601
+ const ref = mergeRefs(outerRef, innerRef);
602
+ const { send } = GroupMachine.useActorRef();
603
+ const machineRef = GroupMachine.useContextRef();
604
+ const groupId = GroupMachine.useSelector((state) => state.context.groupId);
605
+ const panel = GroupMachine.useSelector(({ context }) => {
606
+ try {
607
+ return getPanelWithId(context, panelId);
608
+ } catch {
609
+ return undefined;
610
+ }
611
+ });
612
+
613
+ const contraintChanged =
614
+ panel && haveConstraintsChangedForPanel(panelProp, panel);
615
+
616
+ const onConstraintChange = useEffectEvent(() => {
617
+ if (contraintChanged) {
618
+ send({
619
+ type: "updateConstraints",
620
+ data: { ...panelProp, id: panel.id },
621
+ });
622
+ }
623
+ });
624
+
625
+ React.useEffect(() => {
626
+ if (contraintChanged) {
627
+ onConstraintChange();
628
+ }
629
+ }, [send, contraintChanged, onConstraintChange]);
630
+
631
+ // For controlled collapse we track if the `collapse` prop changes
632
+ // and update the state machine if it does.
633
+ React.useEffect(() => {
634
+ if (typeof collapsed !== "undefined") {
635
+ const context = machineRef.current;
636
+
637
+ if (context.items.length === 0) {
638
+ return;
639
+ }
640
+
641
+ const p = getPanelWithId(context, panelId);
642
+
643
+ if (collapsed === true && !p.collapsed) {
644
+ send({ type: "collapsePanel", panelId, controlled: true });
645
+ } else if (collapsed === false && p.collapsed) {
646
+ send({ type: "expandPanel", panelId, controlled: true });
647
+ }
648
+ }
649
+ }, [send, collapsed, panelId, machineRef]);
650
+
651
+ const fallbackHandleRef = React.useRef<PanelHandle>(null);
652
+
653
+ useImperativeHandle(handle || fallbackHandleRef, () => {
654
+ return {
655
+ getId: () => panelId,
656
+ collapse: () => {
657
+ if (collapsible) {
658
+ // TODO: setting controlled here might be wrong
659
+ send({ type: "collapsePanel", panelId, controlled: true });
660
+ }
661
+ },
662
+ isCollapsed: () => Boolean(collapsible && panel?.collapsed),
663
+ expand: () => {
664
+ if (collapsible) {
665
+ send({ type: "expandPanel", panelId, controlled: true });
666
+ }
667
+ },
668
+ isExpanded: () => Boolean(collapsible && !panel?.collapsed),
669
+ getPixelSize: () => {
670
+ const context = machineRef.current;
671
+ return getPanelPixelSize(context, panelId);
672
+ },
673
+ setSize: (size) => {
674
+ send({ type: "setPanelPixelSize", panelId, size });
675
+ },
676
+ getPercentageSize: () => {
677
+ const context = machineRef.current;
678
+ return getPanelPercentageSize(context, panelId);
679
+ },
680
+ };
681
+ });
682
+
683
+ return (
684
+ <div
685
+ ref={ref}
686
+ {...mergeAttributes(
687
+ props,
688
+ getPanelDomAttributes({
689
+ groupId,
690
+ id: panelId,
691
+ collapsed,
692
+ collapsible: panel?.collapsible,
693
+ }),
694
+ {
695
+ style: {
696
+ minWidth: 0,
697
+ minHeight: 0,
698
+ overflow: "hidden",
699
+ },
700
+ }
701
+ )}
702
+ />
703
+ );
704
+ });
705
+
706
+ export interface PanelResizerProps
707
+ extends SharedPanelResizerProps,
708
+ Omit<
709
+ React.HTMLAttributes<HTMLDivElement>,
710
+ "onDragStart" | "onDrag" | "onDragEnd"
711
+ > {}
712
+
713
+ /** A resize handle to place between panels. */
714
+ export const PanelResizer = React.forwardRef<HTMLDivElement, PanelResizerProps>(
715
+ function PanelResizer(props, ref) {
716
+ const { size = "0px" } = props;
717
+ const isPrerender = React.useContext(PreRenderContext);
718
+ const data = React.useMemo(
719
+ () => ({
720
+ type: "handle" as const,
721
+ size: parseUnit(size) as ParsedPixelUnit,
722
+ id: props.id,
723
+ }),
724
+ [size, props.id]
725
+ );
726
+
727
+ useGroupItem(data);
728
+
729
+ if (isPrerender) {
730
+ return null;
731
+ }
732
+
733
+ return <PanelResizerVisible ref={ref} panelHandleProp={data} {...props} />;
734
+ }
735
+ );
736
+
737
+ const PanelResizerVisible = React.forwardRef<
738
+ HTMLDivElement,
739
+ PanelResizerProps & { panelHandleProp: Omit<PanelHandleData, "id"> }
740
+ >(function PanelResizerVisible(
741
+ {
742
+ size = "0px",
743
+ disabled,
744
+ onDragStart,
745
+ onDrag,
746
+ onDragEnd,
747
+ panelHandleProp,
748
+ ...props
749
+ },
750
+ outerRef
751
+ ) {
752
+ const innerRef = React.useRef<HTMLDivElement>(null);
753
+ const ref = mergeRefs(outerRef, innerRef);
754
+ const unit = parseUnit(size);
755
+ const { send } = GroupMachine.useActorRef();
756
+ const { index } = useIndex();
757
+ const handleId = GroupMachine.useSelector(
758
+ ({ context }) => context.items[index]?.id || ""
759
+ );
760
+ const panelHandle = GroupMachine.useSelector(
761
+ ({ context }) => context.items[index] as PanelHandleData
762
+ );
763
+ const panelBeforeHandle = GroupMachine.useSelector(({ context }) => {
764
+ const panel = context.items[index - 1];
765
+ if (!panel) return undefined;
766
+ if (!isPanelData(panel)) return undefined;
767
+ return panel;
768
+ });
769
+ const collapsiblePanel = GroupMachine.useSelector(({ context }) => {
770
+ try {
771
+ return getCollapsiblePanelForHandleId(context, handleId);
772
+ } catch {
773
+ return undefined;
774
+ }
775
+ });
776
+ const orientation = GroupMachine.useSelector(
777
+ (state) => state.context.orientation
778
+ );
779
+ const groupsSize = GroupMachine.useSelector((state) =>
780
+ getGroupSize(state.context)
781
+ );
782
+ const overshoot = GroupMachine.useSelector(
783
+ (state) => state.context.dragOvershoot
784
+ );
785
+ const activeDragHandleId = GroupMachine.useSelector(
786
+ (state) => state.context.activeDragHandleId
787
+ );
788
+ const groupId = GroupMachine.useSelector((state) => state.context.groupId);
789
+ const [isDragging, setIsDragging] = React.useState(false);
790
+
791
+ let cursor: React.CSSProperties["cursor"];
792
+
793
+ if (disabled) {
794
+ cursor = "default";
795
+ } else {
796
+ cursor = getCursor({ dragOvershoot: overshoot, orientation });
797
+ }
798
+
799
+ const { moveProps } = useMove({
800
+ onMoveStart: () => {
801
+ send({ type: "dragHandleStart", handleId: handleId });
802
+ onDragStart?.();
803
+ setIsDragging(true);
804
+ document.body.style.cursor = cursor || "auto";
805
+ },
806
+ onMove: (e) => {
807
+ send({ type: "dragHandle", handleId: handleId, value: e });
808
+ onDrag?.();
809
+ },
810
+ onMoveEnd: () => {
811
+ send({ type: "dragHandleEnd", handleId: handleId });
812
+ onDragEnd?.();
813
+ setIsDragging(false);
814
+ document.body.style.cursor = "auto";
815
+ },
816
+ });
817
+
818
+ const contraintChanged =
819
+ panelHandle &&
820
+ haveConstraintsChangedForPanelHandle(panelHandleProp, panelHandle);
821
+
822
+ const onConstraintChange = useEffectEvent(() => {
823
+ if (contraintChanged) {
824
+ send({
825
+ type: "updateConstraints",
826
+ data: { ...panelHandleProp, id: handleId },
827
+ });
828
+ }
829
+ });
830
+
831
+ React.useEffect(() => {
832
+ if (contraintChanged) {
833
+ onConstraintChange();
834
+ }
835
+ }, [send, contraintChanged, onConstraintChange]);
836
+
837
+ const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
838
+ if (e.key === "Enter" && collapsiblePanel) {
839
+ if (collapsiblePanel.collapsed) {
840
+ send({ type: "expandPanel", panelId: collapsiblePanel.id });
841
+ } else {
842
+ send({ type: "collapsePanel", panelId: collapsiblePanel.id });
843
+ }
844
+ }
845
+ };
846
+
847
+ return (
848
+ <div
849
+ ref={ref as unknown as React.Ref<HTMLDivElement>}
850
+ {...mergeAttributes(
851
+ props,
852
+ disabled
853
+ ? {}
854
+ : mergeAttributes(moveProps as React.HTMLAttributes<HTMLDivElement>, {
855
+ onKeyDown,
856
+ tabIndex: 0,
857
+ }),
858
+ getPanelResizerDomAttributes({
859
+ groupId,
860
+ id: handleId,
861
+ orientation,
862
+ isDragging,
863
+ activeDragHandleId,
864
+ disabled,
865
+ controlsId: panelBeforeHandle?.id,
866
+ min: panelBeforeHandle?.min,
867
+ max: panelBeforeHandle?.max,
868
+ currentValue: panelBeforeHandle?.currentValue,
869
+ groupSize: groupsSize,
870
+ }),
871
+ {
872
+ style: {
873
+ cursor,
874
+ ...(orientation === "horizontal"
875
+ ? { width: unit.value.toNumber(), height: "100%" }
876
+ : { height: unit.value.toNumber(), width: "100%" }),
877
+ },
878
+ }
879
+ )}
880
+ />
881
+ );
882
+ });
883
+
884
+ // #endregion