react-resizable-panels 0.0.59 → 0.0.61

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/src/PanelGroup.ts CHANGED
@@ -18,6 +18,7 @@ import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
18
18
  import debounce from "./utils/debounce";
19
19
  import { determinePivotIndices } from "./utils/determinePivotIndices";
20
20
  import { calculateAvailablePanelSizeInPixels } from "./utils/dom/calculateAvailablePanelSizeInPixels";
21
+ import { getPanelElementsForGroup } from "./utils/dom/getPanelElementsForGroup";
21
22
  import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement";
22
23
  import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
23
24
  import { isKeyDown, isMouseEvent, isTouchEvent } from "./utils/events";
@@ -70,7 +71,7 @@ const defaultStorage: PanelGroupStorage = {
70
71
  };
71
72
 
72
73
  export type PanelGroupProps = PropsWithChildren<{
73
- autoSaveId?: string;
74
+ autoSaveId?: string | null;
74
75
  className?: string;
75
76
  dataAttributes?: DataAttributes;
76
77
  direction: Direction;
@@ -83,18 +84,12 @@ export type PanelGroupProps = PropsWithChildren<{
83
84
  tagName?: ElementType;
84
85
  }>;
85
86
 
86
- type ImperativeApiQueue = {
87
- type: "collapse" | "expand" | "resize";
88
- mixedSizes?: Partial<MixedSizes>;
89
- panelData: PanelData;
90
- };
91
-
92
87
  const debounceMap: {
93
88
  [key: string]: typeof savePanelGroupLayout;
94
89
  } = {};
95
90
 
96
91
  function PanelGroupWithForwardedRef({
97
- autoSaveId,
92
+ autoSaveId = null,
98
93
  children,
99
94
  className: classNameFromProps = "",
100
95
  dataAttributes,
@@ -114,7 +109,6 @@ function PanelGroupWithForwardedRef({
114
109
 
115
110
  const [dragState, setDragState] = useState<DragState | null>(null);
116
111
  const [layout, setLayout] = useState<number[]>([]);
117
- const [panelDataArray, setPanelDataArray] = useState<PanelData[]>([]);
118
112
 
119
113
  const panelIdToLastNotifiedMixedSizesMapRef = useRef<
120
114
  Record<string, MixedSizes>
@@ -122,28 +116,32 @@ function PanelGroupWithForwardedRef({
122
116
  const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
123
117
  const prevDeltaRef = useRef<number>(0);
124
118
 
125
- const [imperativeApiQueue, setImperativeApiQueue] = useState<
126
- ImperativeApiQueue[]
127
- >([]);
128
-
129
119
  const committedValuesRef = useRef<{
120
+ autoSaveId: string | null;
130
121
  direction: Direction;
131
122
  dragState: DragState | null;
132
123
  id: string;
133
124
  keyboardResizeByPercentage: number | null;
134
125
  keyboardResizeByPixels: number | null;
135
- layout: number[];
136
126
  onLayout: PanelGroupOnLayout | null;
137
- panelDataArray: PanelData[];
127
+ storage: PanelGroupStorage;
138
128
  }>({
129
+ autoSaveId,
139
130
  direction,
140
131
  dragState,
141
132
  id: groupId,
142
133
  keyboardResizeByPercentage,
143
134
  keyboardResizeByPixels,
144
- layout,
145
135
  onLayout,
146
- panelDataArray,
136
+ storage,
137
+ });
138
+
139
+ const eagerValuesRef = useRef<{
140
+ layout: number[];
141
+ panelDataArray: PanelData[];
142
+ }>({
143
+ layout,
144
+ panelDataArray: [],
147
145
  });
148
146
 
149
147
  const devWarningsRef = useRef<{
@@ -161,7 +159,8 @@ function PanelGroupWithForwardedRef({
161
159
  () => ({
162
160
  getId: () => committedValuesRef.current.id,
163
161
  getLayout: () => {
164
- const { id: groupId, layout } = committedValuesRef.current;
162
+ const { id: groupId } = committedValuesRef.current;
163
+ const { layout } = eagerValuesRef.current;
165
164
 
166
165
  const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
167
166
 
@@ -176,12 +175,8 @@ function PanelGroupWithForwardedRef({
176
175
  });
177
176
  },
178
177
  setLayout: (mixedSizes: Partial<MixedSizes>[]) => {
179
- const {
180
- id: groupId,
181
- layout: prevLayout,
182
- onLayout,
183
- panelDataArray,
184
- } = committedValuesRef.current;
178
+ const { id: groupId, onLayout } = committedValuesRef.current;
179
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
185
180
 
186
181
  const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
187
182
 
@@ -201,6 +196,8 @@ function PanelGroupWithForwardedRef({
201
196
  if (!areEqual(prevLayout, safeLayout)) {
202
197
  setLayout(safeLayout);
203
198
 
199
+ eagerValuesRef.current.layout = safeLayout;
200
+
204
201
  if (onLayout) {
205
202
  onLayout(
206
203
  safeLayout.map((sizePercentage) => ({
@@ -226,23 +223,29 @@ function PanelGroupWithForwardedRef({
226
223
  );
227
224
 
228
225
  useIsomorphicLayoutEffect(() => {
226
+ committedValuesRef.current.autoSaveId = autoSaveId;
229
227
  committedValuesRef.current.direction = direction;
230
228
  committedValuesRef.current.dragState = dragState;
231
229
  committedValuesRef.current.id = groupId;
232
- committedValuesRef.current.layout = layout;
233
230
  committedValuesRef.current.onLayout = onLayout;
234
- committedValuesRef.current.panelDataArray = panelDataArray;
231
+ committedValuesRef.current.storage = storage;
232
+
233
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
234
+ // TODO [217] Move these values into a separate ref
235
235
  });
236
236
 
237
237
  useWindowSplitterPanelGroupBehavior({
238
238
  committedValuesRef,
239
+ eagerValuesRef,
239
240
  groupId,
240
241
  layout,
241
- panelDataArray,
242
+ panelDataArray: eagerValuesRef.current.panelDataArray,
242
243
  setLayout,
243
244
  });
244
245
 
245
246
  useEffect(() => {
247
+ const { panelDataArray } = eagerValuesRef.current;
248
+
246
249
  // If this panel has been configured to persist sizing information, save sizes to local storage.
247
250
  if (autoSaveId) {
248
251
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -258,79 +261,11 @@ function PanelGroupWithForwardedRef({
258
261
  }
259
262
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
260
263
  }
261
- }, [autoSaveId, layout, panelDataArray, storage]);
264
+ }, [autoSaveId, layout, storage]);
262
265
 
263
- // Once all panels have registered themselves,
264
- // Compute the initial sizes based on default weights.
265
- // This assumes that panels register during initial mount (no conditional rendering)!
266
266
  useIsomorphicLayoutEffect(() => {
267
- const { id: groupId, layout, onLayout } = committedValuesRef.current;
268
- if (layout.length === panelDataArray.length) {
269
- // Only compute (or restore) default layout once per panel configuration.
270
- return;
271
- }
267
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
272
268
 
273
- // If this panel has been configured to persist sizing information,
274
- // default size should be restored from local storage if possible.
275
- let unsafeLayout: number[] | null = null;
276
- if (autoSaveId) {
277
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
278
- }
279
-
280
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
281
- if (groupSizePixels <= 0) {
282
- if (
283
- shouldMonitorPixelBasedConstraints(
284
- panelDataArray.map(({ constraints }) => constraints)
285
- )
286
- ) {
287
- // Wait until the group has rendered a non-zero size before computing layout.
288
- return;
289
- }
290
- }
291
-
292
- if (unsafeLayout == null) {
293
- unsafeLayout = calculateUnsafeDefaultLayout({
294
- groupSizePixels,
295
- panelDataArray,
296
- });
297
- }
298
-
299
- // Validate even saved layouts in case something has changed since last render
300
- // e.g. for pixel groups, this could be the size of the window
301
- const validatedLayout = validatePanelGroupLayout({
302
- groupSizePixels,
303
- layout: unsafeLayout,
304
- panelConstraints: panelDataArray.map(
305
- (panelData) => panelData.constraints
306
- ),
307
- });
308
-
309
- if (!areEqual(layout, validatedLayout)) {
310
- setLayout(validatedLayout);
311
- }
312
-
313
- if (onLayout) {
314
- onLayout(
315
- validatedLayout.map((sizePercentage) => ({
316
- sizePercentage,
317
- sizePixels: convertPercentageToPixels(
318
- sizePercentage,
319
- groupSizePixels
320
- ),
321
- }))
322
- );
323
- }
324
-
325
- callPanelCallbacks(
326
- groupId,
327
- panelDataArray,
328
- validatedLayout,
329
- panelIdToLastNotifiedMixedSizesMapRef.current
330
- );
331
- }, [autoSaveId, layout, panelDataArray, storage]);
332
-
333
- useIsomorphicLayoutEffect(() => {
334
269
  const constraints = panelDataArray.map(({ constraints }) => constraints);
335
270
  if (!shouldMonitorPixelBasedConstraints(constraints)) {
336
271
  // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
@@ -345,7 +280,7 @@ function PanelGroupWithForwardedRef({
345
280
  const resizeObserver = new ResizeObserver(() => {
346
281
  const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
347
282
 
348
- const { layout: prevLayout, onLayout } = committedValuesRef.current;
283
+ const { onLayout } = committedValuesRef.current;
349
284
 
350
285
  const nextLayout = validatePanelGroupLayout({
351
286
  groupSizePixels,
@@ -358,6 +293,8 @@ function PanelGroupWithForwardedRef({
358
293
  if (!areEqual(prevLayout, nextLayout)) {
359
294
  setLayout(nextLayout);
360
295
 
296
+ eagerValuesRef.current.layout = nextLayout;
297
+
361
298
  if (onLayout) {
362
299
  onLayout(
363
300
  nextLayout.map((sizePercentage) => ({
@@ -385,11 +322,13 @@ function PanelGroupWithForwardedRef({
385
322
  resizeObserver.disconnect();
386
323
  };
387
324
  }
388
- }, [groupId, panelDataArray]);
325
+ }, [groupId]);
389
326
 
390
327
  // DEV warnings
391
328
  useEffect(() => {
392
329
  if (isDevelopment) {
330
+ const { panelDataArray } = eagerValuesRef.current;
331
+
393
332
  const {
394
333
  didLogIdAndOrderWarning,
395
334
  didLogPanelConstraintsWarning,
@@ -397,8 +336,6 @@ function PanelGroupWithForwardedRef({
397
336
  } = devWarningsRef.current;
398
337
 
399
338
  if (!didLogIdAndOrderWarning) {
400
- const { panelDataArray } = committedValuesRef.current;
401
-
402
339
  const panelIds = panelDataArray.map(({ id }) => id);
403
340
 
404
341
  devWarningsRef.current.prevPanelIds = panelIds;
@@ -452,23 +389,8 @@ function PanelGroupWithForwardedRef({
452
389
  // External APIs are safe to memoize via committed values ref
453
390
  const collapsePanel = useCallback(
454
391
  (panelData: PanelData) => {
455
- const {
456
- layout: prevLayout,
457
- onLayout,
458
- panelDataArray,
459
- } = committedValuesRef.current;
460
-
461
- // See issues/211
462
- if (panelDataArray.find(({ id }) => id === panelData.id) == null) {
463
- setImperativeApiQueue((prev) => [
464
- ...prev,
465
- {
466
- panelData,
467
- type: "collapse",
468
- },
469
- ]);
470
- return;
471
- }
392
+ const { onLayout } = committedValuesRef.current;
393
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
472
394
 
473
395
  if (panelData.constraints.collapsible) {
474
396
  const panelConstraintsArray = panelDataArray.map(
@@ -508,6 +430,8 @@ function PanelGroupWithForwardedRef({
508
430
  if (!compareLayouts(prevLayout, nextLayout)) {
509
431
  setLayout(nextLayout);
510
432
 
433
+ eagerValuesRef.current.layout = nextLayout;
434
+
511
435
  if (onLayout) {
512
436
  onLayout(
513
437
  nextLayout.map((sizePercentage) => ({
@@ -536,23 +460,8 @@ function PanelGroupWithForwardedRef({
536
460
  // External APIs are safe to memoize via committed values ref
537
461
  const expandPanel = useCallback(
538
462
  (panelData: PanelData) => {
539
- const {
540
- layout: prevLayout,
541
- onLayout,
542
- panelDataArray,
543
- } = committedValuesRef.current;
544
-
545
- // See issues/211
546
- if (panelDataArray.find(({ id }) => id === panelData.id) == null) {
547
- setImperativeApiQueue((prev) => [
548
- ...prev,
549
- {
550
- panelData,
551
- type: "expand",
552
- },
553
- ]);
554
- return;
555
- }
463
+ const { onLayout } = committedValuesRef.current;
464
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
556
465
 
557
466
  if (panelData.constraints.collapsible) {
558
467
  const panelConstraintsArray = panelDataArray.map(
@@ -595,6 +504,8 @@ function PanelGroupWithForwardedRef({
595
504
  if (!compareLayouts(prevLayout, nextLayout)) {
596
505
  setLayout(nextLayout);
597
506
 
507
+ eagerValuesRef.current.layout = nextLayout;
508
+
598
509
  if (onLayout) {
599
510
  onLayout(
600
511
  nextLayout.map((sizePercentage) => ({
@@ -623,7 +534,7 @@ function PanelGroupWithForwardedRef({
623
534
  // External APIs are safe to memoize via committed values ref
624
535
  const getPanelSize = useCallback(
625
536
  (panelData: PanelData) => {
626
- const { layout, panelDataArray } = committedValuesRef.current;
537
+ const { layout, panelDataArray } = eagerValuesRef.current;
627
538
 
628
539
  const { panelSizePercentage, panelSizePixels } = panelDataHelper(
629
540
  groupId,
@@ -643,6 +554,8 @@ function PanelGroupWithForwardedRef({
643
554
  // This API should never read from committedValuesRef
644
555
  const getPanelStyle = useCallback(
645
556
  (panelData: PanelData) => {
557
+ const { panelDataArray } = eagerValuesRef.current;
558
+
646
559
  const panelIndex = panelDataArray.indexOf(panelData);
647
560
 
648
561
  return computePanelFlexBoxStyle({
@@ -652,13 +565,13 @@ function PanelGroupWithForwardedRef({
652
565
  panelIndex,
653
566
  });
654
567
  },
655
- [dragState, layout, panelDataArray]
568
+ [dragState, layout]
656
569
  );
657
570
 
658
571
  // External APIs are safe to memoize via committed values ref
659
572
  const isPanelCollapsed = useCallback(
660
573
  (panelData: PanelData) => {
661
- const { layout, panelDataArray } = committedValuesRef.current;
574
+ const { layout, panelDataArray } = eagerValuesRef.current;
662
575
 
663
576
  const { collapsedSizePercentage, collapsible, panelSizePercentage } =
664
577
  panelDataHelper(groupId, panelDataArray, panelData, layout);
@@ -673,7 +586,7 @@ function PanelGroupWithForwardedRef({
673
586
  // External APIs are safe to memoize via committed values ref
674
587
  const isPanelExpanded = useCallback(
675
588
  (panelData: PanelData) => {
676
- const { layout, panelDataArray } = committedValuesRef.current;
589
+ const { layout, panelDataArray } = eagerValuesRef.current;
677
590
 
678
591
  const { collapsedSizePercentage, collapsible, panelSizePercentage } =
679
592
  panelDataHelper(groupId, panelDataArray, panelData, layout);
@@ -684,22 +597,99 @@ function PanelGroupWithForwardedRef({
684
597
  );
685
598
 
686
599
  const registerPanel = useCallback((panelData: PanelData) => {
687
- setPanelDataArray((prevPanelDataArray) => {
688
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
689
- return nextPanelDataArray.sort((panelA, panelB) => {
690
- const orderA = panelA.order;
691
- const orderB = panelB.order;
692
- if (orderA == null && orderB == null) {
693
- return 0;
694
- } else if (orderA == null) {
695
- return -1;
696
- } else if (orderB == null) {
697
- return 1;
698
- } else {
699
- return orderA - orderB;
700
- }
600
+ const {
601
+ autoSaveId,
602
+ id: groupId,
603
+ onLayout,
604
+ storage,
605
+ } = committedValuesRef.current;
606
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
607
+
608
+ panelDataArray.push(panelData);
609
+ panelDataArray.sort((panelA, panelB) => {
610
+ const orderA = panelA.order;
611
+ const orderB = panelB.order;
612
+ if (orderA == null && orderB == null) {
613
+ return 0;
614
+ } else if (orderA == null) {
615
+ return -1;
616
+ } else if (orderB == null) {
617
+ return 1;
618
+ } else {
619
+ return orderA - orderB;
620
+ }
621
+ });
622
+
623
+ // Wait until all panels have registered before we try to compute layout;
624
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
625
+ const panelElements = getPanelElementsForGroup(groupId);
626
+ if (panelElements.length !== panelDataArray.length) {
627
+ return;
628
+ }
629
+
630
+ // If this panel has been configured to persist sizing information,
631
+ // default size should be restored from local storage if possible.
632
+ let unsafeLayout: number[] | null = null;
633
+ if (autoSaveId) {
634
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
635
+ }
636
+
637
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
638
+ if (groupSizePixels <= 0) {
639
+ if (
640
+ shouldMonitorPixelBasedConstraints(
641
+ panelDataArray.map(({ constraints }) => constraints)
642
+ )
643
+ ) {
644
+ // Wait until the group has rendered a non-zero size before computing layout.
645
+ return;
646
+ }
647
+ }
648
+
649
+ if (unsafeLayout == null) {
650
+ unsafeLayout = calculateUnsafeDefaultLayout({
651
+ groupSizePixels,
652
+ panelDataArray,
701
653
  });
654
+ }
655
+
656
+ // Validate even saved layouts in case something has changed since last render
657
+ // e.g. for pixel groups, this could be the size of the window
658
+ const nextLayout = validatePanelGroupLayout({
659
+ groupSizePixels,
660
+ layout: unsafeLayout,
661
+ panelConstraints: panelDataArray.map(
662
+ (panelData) => panelData.constraints
663
+ ),
702
664
  });
665
+
666
+ // Offscreen mode makes this a bit weird;
667
+ // Panels unregister when hidden and re-register when shown again,
668
+ // but the overall layout doesn't change between these two cases.
669
+ setLayout(nextLayout);
670
+
671
+ eagerValuesRef.current.layout = nextLayout;
672
+
673
+ if (!areEqual(prevLayout, nextLayout)) {
674
+ if (onLayout) {
675
+ onLayout(
676
+ nextLayout.map((sizePercentage) => ({
677
+ sizePercentage,
678
+ sizePixels: convertPercentageToPixels(
679
+ sizePercentage,
680
+ groupSizePixels
681
+ ),
682
+ }))
683
+ );
684
+ }
685
+
686
+ callPanelCallbacks(
687
+ groupId,
688
+ panelDataArray,
689
+ nextLayout,
690
+ panelIdToLastNotifiedMixedSizesMapRef.current
691
+ );
692
+ }
703
693
  }, []);
704
694
 
705
695
  const registerResizeHandle = useCallback((dragHandleId: string) => {
@@ -713,9 +703,8 @@ function PanelGroupWithForwardedRef({
713
703
  keyboardResizeByPercentage,
714
704
  keyboardResizeByPixels,
715
705
  onLayout,
716
- panelDataArray,
717
- layout: prevLayout,
718
706
  } = committedValuesRef.current;
707
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
719
708
 
720
709
  const { initialLayout } = dragState ?? {};
721
710
 
@@ -789,6 +778,8 @@ function PanelGroupWithForwardedRef({
789
778
  if (layoutChanged) {
790
779
  setLayout(nextLayout);
791
780
 
781
+ eagerValuesRef.current.layout = nextLayout;
782
+
792
783
  if (onLayout) {
793
784
  onLayout(
794
785
  nextLayout.map((sizePercentage) => ({
@@ -814,24 +805,9 @@ function PanelGroupWithForwardedRef({
814
805
  // External APIs are safe to memoize via committed values ref
815
806
  const resizePanel = useCallback(
816
807
  (panelData: PanelData, mixedSizes: Partial<MixedSizes>) => {
817
- const {
818
- layout: prevLayout,
819
- onLayout,
820
- panelDataArray,
821
- } = committedValuesRef.current;
808
+ const { onLayout } = committedValuesRef.current;
822
809
 
823
- // See issues/211
824
- if (panelDataArray.find(({ id }) => id === panelData.id) == null) {
825
- setImperativeApiQueue((prev) => [
826
- ...prev,
827
- {
828
- panelData,
829
- mixedSizes,
830
- type: "resize",
831
- },
832
- ]);
833
- return;
834
- }
810
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
835
811
 
836
812
  const panelConstraintsArray = panelDataArray.map(
837
813
  (panelData) => panelData.constraints
@@ -863,6 +839,8 @@ function PanelGroupWithForwardedRef({
863
839
  if (!compareLayouts(prevLayout, nextLayout)) {
864
840
  setLayout(nextLayout);
865
841
 
842
+ eagerValuesRef.current.layout = nextLayout;
843
+
866
844
  if (onLayout) {
867
845
  onLayout(
868
846
  nextLayout.map((sizePercentage) => ({
@@ -888,7 +866,8 @@ function PanelGroupWithForwardedRef({
888
866
 
889
867
  const startDragging = useCallback(
890
868
  (dragHandleId: string, event: ResizeEvent) => {
891
- const { direction, layout } = committedValuesRef.current;
869
+ const { direction } = committedValuesRef.current;
870
+ const { layout } = eagerValuesRef.current;
892
871
 
893
872
  const handleElement = getResizeHandleElement(dragHandleId)!;
894
873
 
@@ -912,48 +891,103 @@ function PanelGroupWithForwardedRef({
912
891
  setDragState(null);
913
892
  }, []);
914
893
 
894
+ const unregisterPanelRef = useRef<{
895
+ pendingPanelIds: Set<string>;
896
+ timeout: NodeJS.Timeout | null;
897
+ }>({
898
+ pendingPanelIds: new Set(),
899
+ timeout: null,
900
+ });
915
901
  const unregisterPanel = useCallback((panelData: PanelData) => {
916
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
902
+ const { id: groupId, onLayout } = committedValuesRef.current;
903
+ const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;
917
904
 
918
- setPanelDataArray((panelDataArray) => {
919
- const index = panelDataArray.indexOf(panelData);
920
- if (index >= 0) {
921
- panelDataArray = [...panelDataArray];
922
- panelDataArray.splice(index, 1);
923
- }
905
+ const index = panelDataArray.indexOf(panelData);
906
+ if (index >= 0) {
907
+ panelDataArray.splice(index, 1);
908
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
909
+ }
924
910
 
925
- return panelDataArray;
926
- });
927
- }, []);
911
+ if (unregisterPanelRef.current.timeout != null) {
912
+ clearTimeout(unregisterPanelRef.current.timeout);
913
+ }
928
914
 
929
- // Handle imperative API calls that were made before panels were registered
930
- useIsomorphicLayoutEffect(() => {
931
- const queue = imperativeApiQueue;
932
- while (queue.length > 0) {
933
- const current = queue.shift()!;
934
- switch (current.type) {
935
- case "collapse": {
936
- collapsePanel(current.panelData);
937
- break;
938
- }
939
- case "expand": {
940
- expandPanel(current.panelData);
941
- break;
915
+ // Batch panel unmounts so that we only calculate layout once;
916
+ // This is more efficient and avoids misleading warnings in development mode.
917
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
918
+ unregisterPanelRef.current.timeout = setTimeout(() => {
919
+ const { pendingPanelIds } = unregisterPanelRef.current;
920
+ const map = panelIdToLastNotifiedMixedSizesMapRef.current;
921
+
922
+ // TRICKY
923
+ // Strict effects mode
924
+ let unmountDueToStrictMode = false;
925
+ pendingPanelIds.forEach((panelId) => {
926
+ pendingPanelIds.delete(panelId);
927
+
928
+ if (panelDataArray.find(({ id }) => id === panelId) == null) {
929
+ unmountDueToStrictMode = true;
930
+
931
+ // TRICKY
932
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
933
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
934
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
935
+ delete map[panelData.id];
942
936
  }
943
- case "resize": {
944
- resizePanel(current.panelData, current.mixedSizes!);
945
- break;
937
+ });
938
+
939
+ if (!unmountDueToStrictMode) {
940
+ return;
941
+ }
942
+
943
+ if (panelDataArray.length === 0) {
944
+ // The group is unmounting; skip layout calculation.
945
+ return;
946
+ }
947
+
948
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
949
+
950
+ let unsafeLayout: number[] = calculateUnsafeDefaultLayout({
951
+ groupSizePixels,
952
+ panelDataArray,
953
+ });
954
+
955
+ // Validate even saved layouts in case something has changed since last render
956
+ // e.g. for pixel groups, this could be the size of the window
957
+ const nextLayout = validatePanelGroupLayout({
958
+ groupSizePixels,
959
+ layout: unsafeLayout,
960
+ panelConstraints: panelDataArray.map(
961
+ (panelData) => panelData.constraints
962
+ ),
963
+ });
964
+
965
+ if (!areEqual(prevLayout, nextLayout)) {
966
+ setLayout(nextLayout);
967
+
968
+ eagerValuesRef.current.layout = nextLayout;
969
+
970
+ if (onLayout) {
971
+ onLayout(
972
+ nextLayout.map((sizePercentage) => ({
973
+ sizePercentage,
974
+ sizePixels: convertPercentageToPixels(
975
+ sizePercentage,
976
+ groupSizePixels
977
+ ),
978
+ }))
979
+ );
946
980
  }
981
+
982
+ callPanelCallbacks(
983
+ groupId,
984
+ panelDataArray,
985
+ nextLayout,
986
+ panelIdToLastNotifiedMixedSizesMapRef.current
987
+ );
947
988
  }
948
- }
949
- }, [
950
- collapsePanel,
951
- expandPanel,
952
- imperativeApiQueue,
953
- layout,
954
- panelDataArray,
955
- resizePanel,
956
- ]);
989
+ }, 0);
990
+ }, []);
957
991
 
958
992
  const context = useMemo(
959
993
  () => ({