react-resizable-panels 1.0.0-rc.3 → 1.0.0-rc.4

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.
@@ -253,6 +253,229 @@ describe("PanelGroup", () => {
253
253
  expect(element.title).toBe("bar");
254
254
  });
255
255
 
256
+ describe("callbacks", () => {
257
+ describe("onCollapse", () => {
258
+ it("should be called on mount if a panels initial size is 0", () => {
259
+ let onCollapseLeft = jest.fn();
260
+ let onCollapseRight = jest.fn();
261
+
262
+ act(() => {
263
+ root.render(
264
+ <PanelGroup direction="horizontal">
265
+ <Panel collapsible defaultSize={0} onCollapse={onCollapseLeft} />
266
+ <PanelResizeHandle />
267
+ <Panel collapsible onCollapse={onCollapseRight} />
268
+ </PanelGroup>
269
+ );
270
+ });
271
+
272
+ expect(onCollapseLeft).toHaveBeenCalledTimes(1);
273
+ expect(onCollapseRight).not.toHaveBeenCalled();
274
+ });
275
+
276
+ it("should be called when a panel is collapsed", () => {
277
+ let onCollapse = jest.fn();
278
+
279
+ let panelRef = createRef<ImperativePanelHandle>();
280
+
281
+ act(() => {
282
+ root.render(
283
+ <PanelGroup direction="horizontal">
284
+ <Panel collapsible onCollapse={onCollapse} ref={panelRef} />
285
+ <PanelResizeHandle />
286
+ <Panel />
287
+ </PanelGroup>
288
+ );
289
+ });
290
+
291
+ expect(onCollapse).not.toHaveBeenCalled();
292
+
293
+ act(() => {
294
+ panelRef.current?.collapse();
295
+ });
296
+
297
+ expect(onCollapse).toHaveBeenCalledTimes(1);
298
+ });
299
+ });
300
+
301
+ describe("onExpand", () => {
302
+ it("should be called on mount if a collapsible panels initial size is not 0", () => {
303
+ let onExpandLeft = jest.fn();
304
+ let onExpandRight = jest.fn();
305
+
306
+ act(() => {
307
+ root.render(
308
+ <PanelGroup direction="horizontal">
309
+ <Panel collapsible onExpand={onExpandLeft} />
310
+ <PanelResizeHandle />
311
+ <Panel onExpand={onExpandRight} />
312
+ </PanelGroup>
313
+ );
314
+ });
315
+
316
+ expect(onExpandLeft).toHaveBeenCalledTimes(1);
317
+ expect(onExpandRight).not.toHaveBeenCalled();
318
+ });
319
+
320
+ it("should be called when a collapsible panel is expanded", () => {
321
+ let onExpand = jest.fn();
322
+
323
+ let panelRef = createRef<ImperativePanelHandle>();
324
+
325
+ act(() => {
326
+ root.render(
327
+ <PanelGroup direction="horizontal">
328
+ <Panel
329
+ collapsible
330
+ defaultSize={0}
331
+ onExpand={onExpand}
332
+ ref={panelRef}
333
+ />
334
+ <PanelResizeHandle />
335
+ <Panel />
336
+ </PanelGroup>
337
+ );
338
+ });
339
+
340
+ expect(onExpand).not.toHaveBeenCalled();
341
+
342
+ act(() => {
343
+ panelRef.current?.resize(25);
344
+ });
345
+
346
+ expect(onExpand).toHaveBeenCalledTimes(1);
347
+ });
348
+ });
349
+
350
+ describe("onResize", () => {
351
+ it("should be called on mount", () => {
352
+ let onResizeLeft = jest.fn();
353
+ let onResizeMiddle = jest.fn();
354
+ let onResizeRight = jest.fn();
355
+
356
+ act(() => {
357
+ root.render(
358
+ <PanelGroup direction="horizontal">
359
+ <Panel id="left" onResize={onResizeLeft} order={1} />
360
+ <PanelResizeHandle />
361
+ <Panel
362
+ defaultSize={50}
363
+ id="middle"
364
+ onResize={onResizeMiddle}
365
+ order={2}
366
+ />
367
+ <PanelResizeHandle />
368
+ <Panel id="right" onResize={onResizeRight} order={3} />
369
+ </PanelGroup>
370
+ );
371
+ });
372
+
373
+ expect(onResizeLeft).toHaveBeenCalledTimes(1);
374
+ expect(onResizeLeft).toHaveBeenCalledWith(25, undefined);
375
+ expect(onResizeMiddle).toHaveBeenCalledTimes(1);
376
+ expect(onResizeMiddle).toHaveBeenCalledWith(50, undefined);
377
+ expect(onResizeRight).toHaveBeenCalledTimes(1);
378
+ expect(onResizeRight).toHaveBeenCalledWith(25, undefined);
379
+ });
380
+
381
+ it("should be called when a panel is added or removed from the group", () => {
382
+ let onResizeLeft = jest.fn();
383
+ let onResizeMiddle = jest.fn();
384
+ let onResizeRight = jest.fn();
385
+
386
+ act(() => {
387
+ root.render(
388
+ <PanelGroup direction="horizontal">
389
+ <Panel
390
+ id="middle"
391
+ key="middle"
392
+ onResize={onResizeMiddle}
393
+ order={2}
394
+ />
395
+ </PanelGroup>
396
+ );
397
+ });
398
+
399
+ expect(onResizeLeft).not.toHaveBeenCalled();
400
+ expect(onResizeMiddle).toHaveBeenCalledWith(100, undefined);
401
+ expect(onResizeRight).not.toHaveBeenCalled();
402
+
403
+ onResizeLeft.mockReset();
404
+ onResizeMiddle.mockReset();
405
+ onResizeRight.mockReset();
406
+
407
+ act(() => {
408
+ root.render(
409
+ <PanelGroup direction="horizontal">
410
+ <Panel
411
+ id="left"
412
+ key="left"
413
+ maxSize={25}
414
+ minSize={25}
415
+ onResize={onResizeLeft}
416
+ order={1}
417
+ />
418
+ <PanelResizeHandle />
419
+ <Panel
420
+ id="middle"
421
+ key="middle"
422
+ onResize={onResizeMiddle}
423
+ order={2}
424
+ />
425
+ <PanelResizeHandle />
426
+ <Panel
427
+ id="right"
428
+ key="right"
429
+ maxSize={25}
430
+ minSize={25}
431
+ onResize={onResizeRight}
432
+ order={3}
433
+ />
434
+ </PanelGroup>
435
+ );
436
+ });
437
+
438
+ expect(onResizeLeft).toHaveBeenCalledTimes(1);
439
+ expect(onResizeLeft).toHaveBeenCalledWith(25, undefined);
440
+ expect(onResizeMiddle).toHaveBeenCalledTimes(1);
441
+ expect(onResizeMiddle).toHaveBeenCalledWith(50, 100);
442
+ expect(onResizeRight).toHaveBeenCalledTimes(1);
443
+ expect(onResizeRight).toHaveBeenCalledWith(25, undefined);
444
+
445
+ onResizeLeft.mockReset();
446
+ onResizeMiddle.mockReset();
447
+ onResizeRight.mockReset();
448
+
449
+ act(() => {
450
+ root.render(
451
+ <PanelGroup direction="horizontal">
452
+ <Panel
453
+ id="left"
454
+ key="left"
455
+ maxSize={25}
456
+ minSize={25}
457
+ onResize={onResizeLeft}
458
+ order={1}
459
+ />
460
+ <PanelResizeHandle />
461
+ <Panel
462
+ id="middle"
463
+ key="middle"
464
+ onResize={onResizeMiddle}
465
+ order={2}
466
+ />
467
+ </PanelGroup>
468
+ );
469
+ });
470
+
471
+ expect(onResizeLeft).not.toHaveBeenCalled();
472
+ expect(onResizeMiddle).toHaveBeenCalledTimes(1);
473
+ expect(onResizeMiddle).toHaveBeenCalledWith(75, 50);
474
+ expect(onResizeRight).not.toHaveBeenCalled();
475
+ });
476
+ });
477
+ });
478
+
256
479
  describe("DEV warnings", () => {
257
480
  it("should warn about server rendered panels with no default size", () => {
258
481
  jest.resetModules();
@@ -2,6 +2,7 @@ import { Root, createRoot } from "react-dom/client";
2
2
  import { act } from "react-dom/test-utils";
3
3
  import {
4
4
  ImperativePanelGroupHandle,
5
+ ImperativePanelHandle,
5
6
  Panel,
6
7
  PanelGroup,
7
8
  PanelResizeHandle,
@@ -130,6 +131,65 @@ describe("PanelGroup", () => {
130
131
  expect(element.title).toBe("bar");
131
132
  });
132
133
 
134
+ describe("callbacks", () => {
135
+ describe("onLayout", () => {
136
+ it("should be called with the initial group layout on mount", () => {
137
+ let onLayout = jest.fn();
138
+
139
+ act(() => {
140
+ root.render(
141
+ <PanelGroup direction="horizontal" onLayout={onLayout}>
142
+ <Panel defaultSize={35} />
143
+ <PanelResizeHandle />
144
+ <Panel defaultSize={65} />
145
+ </PanelGroup>
146
+ );
147
+ });
148
+
149
+ expect(onLayout).toHaveBeenCalledTimes(1);
150
+ expect(onLayout).toHaveBeenCalledWith([35, 65]);
151
+ });
152
+
153
+ it("should be called any time the group layout changes", () => {
154
+ let onLayout = jest.fn();
155
+ let panelGroupRef = createRef<ImperativePanelGroupHandle>();
156
+ let panelRef = createRef<ImperativePanelHandle>();
157
+
158
+ act(() => {
159
+ root.render(
160
+ <PanelGroup
161
+ direction="horizontal"
162
+ onLayout={onLayout}
163
+ ref={panelGroupRef}
164
+ >
165
+ <Panel defaultSize={35} ref={panelRef} />
166
+ <PanelResizeHandle />
167
+ <Panel defaultSize={65} />
168
+ </PanelGroup>
169
+ );
170
+ });
171
+
172
+ onLayout.mockReset();
173
+
174
+ act(() => {
175
+ panelGroupRef.current?.setLayout([25, 75]);
176
+ });
177
+
178
+ expect(onLayout).toHaveBeenCalledTimes(1);
179
+ expect(onLayout).toHaveBeenCalledWith([25, 75]);
180
+
181
+ onLayout.mockReset();
182
+
183
+ act(() => {
184
+ panelRef.current?.resize(50);
185
+ });
186
+
187
+ expect(onLayout).toHaveBeenCalledTimes(1);
188
+ expect(onLayout).toHaveBeenCalledWith([50, 50]);
189
+ });
190
+ });
191
+ });
192
+
133
193
  describe("DEV warnings", () => {
134
194
  it("should warn about unstable layouts without id and order props", () => {
135
195
  act(() => {
package/src/PanelGroup.ts CHANGED
@@ -16,7 +16,6 @@ import { computePanelFlexBoxStyle } from "./utils/computePanelFlexBoxStyle";
16
16
  import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
17
17
  import debounce from "./utils/debounce";
18
18
  import { determinePivotIndices } from "./utils/determinePivotIndices";
19
- import { getPanelElementsForGroup } from "./utils/dom/getPanelElementsForGroup";
20
19
  import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
21
20
  import { isKeyDown, isMouseEvent, isTouchEvent } from "./utils/events";
22
21
  import { getResizeEventCursorPosition } from "./utils/getResizeEventCursorPosition";
@@ -103,6 +102,7 @@ function PanelGroupWithForwardedRef({
103
102
 
104
103
  const [dragState, setDragState] = useState<DragState | null>(null);
105
104
  const [layout, setLayout] = useState<number[]>([]);
105
+ const [panelDataArray, setPanelDataArray] = useState<PanelData[]>([]);
106
106
 
107
107
  const panelIdToLastNotifiedSizeMapRef = useRef<Record<string, number>>({});
108
108
  const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
@@ -129,9 +129,11 @@ function PanelGroupWithForwardedRef({
129
129
  const eagerValuesRef = useRef<{
130
130
  layout: number[];
131
131
  panelDataArray: PanelData[];
132
+ panelDataArrayChanged: boolean;
132
133
  }>({
133
134
  layout,
134
135
  panelDataArray: [],
136
+ panelDataArrayChanged: false,
135
137
  });
136
138
 
137
139
  const devWarningsRef = useRef<{
@@ -463,26 +465,7 @@ function PanelGroupWithForwardedRef({
463
465
  }, []);
464
466
 
465
467
  const registerPanel = useCallback((panelData: PanelData) => {
466
- const {
467
- autoSaveId,
468
- id: groupId,
469
- onLayout,
470
- storage,
471
- } = committedValuesRef.current;
472
- const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
473
-
474
- // HACK
475
- // This appears to be triggered by some React Suspense+Offscreen+StrictMode bug;
476
- // see app.replay.io/recording/17b6e11d-4500-4173-b23d-61dfd141fed1
477
- const index = findPanelDataIndex(panelDataArray, panelData);
478
- if (index >= 0) {
479
- if (panelData.idIsFromProps) {
480
- console.warn(`Panel with id "${panelData.id}" registered twice`);
481
- } else {
482
- console.warn(`Panel registered twice`);
483
- }
484
- return;
485
- }
468
+ const { panelDataArray } = eagerValuesRef.current;
486
469
 
487
470
  panelDataArray.push(panelData);
488
471
  panelDataArray.sort((panelA, panelB) => {
@@ -499,54 +482,57 @@ function PanelGroupWithForwardedRef({
499
482
  }
500
483
  });
501
484
 
502
- // Wait until all panels have registered before we try to compute layout;
503
- // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
504
- const panelElements = getPanelElementsForGroup(groupId);
505
- if (panelElements.length !== panelDataArray.length) {
506
- return;
507
- }
485
+ eagerValuesRef.current.panelDataArrayChanged = true;
486
+ }, []);
508
487
 
509
- // If this panel has been configured to persist sizing information,
510
- // default size should be restored from local storage if possible.
511
- let unsafeLayout: number[] | null = null;
512
- if (autoSaveId) {
513
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
514
- }
488
+ // (Re)calculate group layout whenever panels are registered or unregistered.
489
+ // eslint-disable-next-line react-hooks/exhaustive-deps
490
+ useIsomorphicLayoutEffect(() => {
491
+ if (eagerValuesRef.current.panelDataArrayChanged) {
492
+ eagerValuesRef.current.panelDataArrayChanged = false;
515
493
 
516
- if (unsafeLayout == null) {
517
- unsafeLayout = calculateUnsafeDefaultLayout({
518
- panelDataArray,
494
+ const { autoSaveId, onLayout, storage } = committedValuesRef.current;
495
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
496
+
497
+ // If this panel has been configured to persist sizing information,
498
+ // default size should be restored from local storage if possible.
499
+ let unsafeLayout: number[] | null = null;
500
+ if (autoSaveId) {
501
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
502
+ }
503
+
504
+ if (unsafeLayout == null) {
505
+ unsafeLayout = calculateUnsafeDefaultLayout({
506
+ panelDataArray,
507
+ });
508
+ }
509
+
510
+ // Validate even saved layouts in case something has changed since last render
511
+ // e.g. for pixel groups, this could be the size of the window
512
+ const nextLayout = validatePanelGroupLayout({
513
+ layout: unsafeLayout,
514
+ panelConstraints: panelDataArray.map(
515
+ (panelData) => panelData.constraints
516
+ ),
519
517
  });
520
- }
521
518
 
522
- // Validate even saved layouts in case something has changed since last render
523
- // e.g. for pixel groups, this could be the size of the window
524
- const nextLayout = validatePanelGroupLayout({
525
- layout: unsafeLayout,
526
- panelConstraints: panelDataArray.map(
527
- (panelData) => panelData.constraints
528
- ),
529
- });
519
+ if (!areEqual(prevLayout, nextLayout)) {
520
+ setLayout(nextLayout);
530
521
 
531
- // Offscreen mode makes this a bit weird;
532
- // Panels unregister when hidden and re-register when shown again,
533
- // but the overall layout doesn't change between these two cases.
534
- setLayout(nextLayout);
522
+ eagerValuesRef.current.layout = nextLayout;
535
523
 
536
- eagerValuesRef.current.layout = nextLayout;
524
+ if (onLayout) {
525
+ onLayout(nextLayout);
526
+ }
537
527
 
538
- if (!areEqual(prevLayout, nextLayout)) {
539
- if (onLayout) {
540
- onLayout(nextLayout);
528
+ callPanelCallbacks(
529
+ panelDataArray,
530
+ nextLayout,
531
+ panelIdToLastNotifiedSizeMapRef.current
532
+ );
541
533
  }
542
-
543
- callPanelCallbacks(
544
- panelDataArray,
545
- nextLayout,
546
- panelIdToLastNotifiedSizeMapRef.current
547
- );
548
534
  }
549
- }, []);
535
+ });
550
536
 
551
537
  const registerResizeHandle = useCallback((dragHandleId: string) => {
552
538
  return function resizeHandler(event: ResizeEvent) {
@@ -723,90 +709,21 @@ function PanelGroupWithForwardedRef({
723
709
  setDragState(null);
724
710
  }, []);
725
711
 
726
- const unregisterPanelRef = useRef<{
727
- pendingPanelIds: Set<string>;
728
- timeout: NodeJS.Timeout | null;
729
- }>({
730
- pendingPanelIds: new Set(),
731
- timeout: null,
732
- });
733
712
  const unregisterPanel = useCallback((panelData: PanelData) => {
734
- const { onLayout } = committedValuesRef.current;
735
- const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
713
+ const { panelDataArray } = eagerValuesRef.current;
736
714
 
737
715
  const index = findPanelDataIndex(panelDataArray, panelData);
738
716
  if (index >= 0) {
739
717
  panelDataArray.splice(index, 1);
740
- unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
741
- }
742
-
743
- if (unregisterPanelRef.current.timeout != null) {
744
- clearTimeout(unregisterPanelRef.current.timeout);
745
- }
746
-
747
- // Batch panel unmounts so that we only calculate layout once;
748
- // This is more efficient and avoids misleading warnings in development mode.
749
- // We can't check the DOM to detect this because Panel elements have not yet been removed.
750
- unregisterPanelRef.current.timeout = setTimeout(() => {
751
- const { pendingPanelIds } = unregisterPanelRef.current;
752
- const panelIdToLastNotifiedSizeMap =
753
- panelIdToLastNotifiedSizeMapRef.current;
754
718
 
755
719
  // TRICKY
756
- // Strict effects mode
757
- let unmountDueToStrictMode = false;
758
- pendingPanelIds.forEach((panelId) => {
759
- pendingPanelIds.delete(panelId);
760
-
761
- if (panelDataArray.find(({ id }) => id === panelId) != null) {
762
- unmountDueToStrictMode = true;
763
- } else {
764
- // TRICKY
765
- // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
766
- // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
767
- // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
768
- delete panelIdToLastNotifiedSizeMap[panelId];
769
- }
770
- });
771
-
772
- if (unmountDueToStrictMode) {
773
- return;
774
- }
775
-
776
- if (panelDataArray.length === 0) {
777
- // The group is unmounting; skip layout calculation.
778
- return;
779
- }
780
-
781
- let unsafeLayout: number[] = calculateUnsafeDefaultLayout({
782
- panelDataArray,
783
- });
720
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
721
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
722
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
723
+ delete panelIdToLastNotifiedSizeMapRef.current[panelData.id];
784
724
 
785
- // Validate even saved layouts in case something has changed since last render
786
- // e.g. for pixel groups, this could be the size of the window
787
- const nextLayout = validatePanelGroupLayout({
788
- layout: unsafeLayout,
789
- panelConstraints: panelDataArray.map(
790
- (panelData) => panelData.constraints
791
- ),
792
- });
793
-
794
- if (!areEqual(prevLayout, nextLayout)) {
795
- setLayout(nextLayout);
796
-
797
- eagerValuesRef.current.layout = nextLayout;
798
-
799
- if (onLayout) {
800
- onLayout(nextLayout);
801
- }
802
-
803
- callPanelCallbacks(
804
- panelDataArray,
805
- nextLayout,
806
- panelIdToLastNotifiedSizeMapRef.current
807
- );
808
- }
809
- }, 0);
725
+ eagerValuesRef.current.panelDataArrayChanged = true;
726
+ }
810
727
  }, []);
811
728
 
812
729
  const context = useMemo(
@@ -65,4 +65,10 @@ describe("PanelResizeHandle", () => {
65
65
  expect(element.getAttribute("data-test-name")).toBe("foo");
66
66
  expect(element.title).toBe("bar");
67
67
  });
68
+
69
+ describe("callbacks", () => {
70
+ describe("onDragging", () => {
71
+ // TODO: Test this
72
+ });
73
+ });
68
74
  });