react-resizable-panels 0.0.59 → 0.0.60

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,11 +116,8 @@ 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;
@@ -135,7 +126,9 @@ function PanelGroupWithForwardedRef({
135
126
  layout: number[];
136
127
  onLayout: PanelGroupOnLayout | null;
137
128
  panelDataArray: PanelData[];
129
+ storage: PanelGroupStorage;
138
130
  }>({
131
+ autoSaveId,
139
132
  direction,
140
133
  dragState,
141
134
  id: groupId,
@@ -143,7 +136,8 @@ function PanelGroupWithForwardedRef({
143
136
  keyboardResizeByPixels,
144
137
  layout,
145
138
  onLayout,
146
- panelDataArray,
139
+ panelDataArray: [],
140
+ storage,
147
141
  });
148
142
 
149
143
  const devWarningsRef = useRef<{
@@ -201,6 +195,8 @@ function PanelGroupWithForwardedRef({
201
195
  if (!areEqual(prevLayout, safeLayout)) {
202
196
  setLayout(safeLayout);
203
197
 
198
+ committedValuesRef.current.layout = safeLayout;
199
+
204
200
  if (onLayout) {
205
201
  onLayout(
206
202
  safeLayout.map((sizePercentage) => ({
@@ -226,23 +222,28 @@ function PanelGroupWithForwardedRef({
226
222
  );
227
223
 
228
224
  useIsomorphicLayoutEffect(() => {
225
+ committedValuesRef.current.autoSaveId = autoSaveId;
229
226
  committedValuesRef.current.direction = direction;
230
227
  committedValuesRef.current.dragState = dragState;
231
228
  committedValuesRef.current.id = groupId;
232
- committedValuesRef.current.layout = layout;
233
229
  committedValuesRef.current.onLayout = onLayout;
234
- committedValuesRef.current.panelDataArray = panelDataArray;
230
+ committedValuesRef.current.storage = storage;
231
+
232
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
233
+ // TODO [217] Move these values into a separate ref
235
234
  });
236
235
 
237
236
  useWindowSplitterPanelGroupBehavior({
238
237
  committedValuesRef,
239
238
  groupId,
240
239
  layout,
241
- panelDataArray,
240
+ panelDataArray: committedValuesRef.current.panelDataArray,
242
241
  setLayout,
243
242
  });
244
243
 
245
244
  useEffect(() => {
245
+ const { panelDataArray } = committedValuesRef.current;
246
+
246
247
  // If this panel has been configured to persist sizing information, save sizes to local storage.
247
248
  if (autoSaveId) {
248
249
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -258,79 +259,11 @@ function PanelGroupWithForwardedRef({
258
259
  }
259
260
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
260
261
  }
261
- }, [autoSaveId, layout, panelDataArray, storage]);
262
+ }, [autoSaveId, layout, storage]);
262
263
 
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
264
  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
- }
272
-
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
- }
265
+ const { panelDataArray } = committedValuesRef.current;
312
266
 
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
267
  const constraints = panelDataArray.map(({ constraints }) => constraints);
335
268
  if (!shouldMonitorPixelBasedConstraints(constraints)) {
336
269
  // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
@@ -358,6 +291,8 @@ function PanelGroupWithForwardedRef({
358
291
  if (!areEqual(prevLayout, nextLayout)) {
359
292
  setLayout(nextLayout);
360
293
 
294
+ committedValuesRef.current.layout = nextLayout;
295
+
361
296
  if (onLayout) {
362
297
  onLayout(
363
298
  nextLayout.map((sizePercentage) => ({
@@ -385,11 +320,13 @@ function PanelGroupWithForwardedRef({
385
320
  resizeObserver.disconnect();
386
321
  };
387
322
  }
388
- }, [groupId, panelDataArray]);
323
+ }, [groupId]);
389
324
 
390
325
  // DEV warnings
391
326
  useEffect(() => {
392
327
  if (isDevelopment) {
328
+ const { panelDataArray } = committedValuesRef.current;
329
+
393
330
  const {
394
331
  didLogIdAndOrderWarning,
395
332
  didLogPanelConstraintsWarning,
@@ -397,8 +334,6 @@ function PanelGroupWithForwardedRef({
397
334
  } = devWarningsRef.current;
398
335
 
399
336
  if (!didLogIdAndOrderWarning) {
400
- const { panelDataArray } = committedValuesRef.current;
401
-
402
337
  const panelIds = panelDataArray.map(({ id }) => id);
403
338
 
404
339
  devWarningsRef.current.prevPanelIds = panelIds;
@@ -458,18 +393,6 @@ function PanelGroupWithForwardedRef({
458
393
  panelDataArray,
459
394
  } = committedValuesRef.current;
460
395
 
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
- }
472
-
473
396
  if (panelData.constraints.collapsible) {
474
397
  const panelConstraintsArray = panelDataArray.map(
475
398
  (panelData) => panelData.constraints
@@ -508,6 +431,8 @@ function PanelGroupWithForwardedRef({
508
431
  if (!compareLayouts(prevLayout, nextLayout)) {
509
432
  setLayout(nextLayout);
510
433
 
434
+ committedValuesRef.current.layout = nextLayout;
435
+
511
436
  if (onLayout) {
512
437
  onLayout(
513
438
  nextLayout.map((sizePercentage) => ({
@@ -542,18 +467,6 @@ function PanelGroupWithForwardedRef({
542
467
  panelDataArray,
543
468
  } = committedValuesRef.current;
544
469
 
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
- }
556
-
557
470
  if (panelData.constraints.collapsible) {
558
471
  const panelConstraintsArray = panelDataArray.map(
559
472
  (panelData) => panelData.constraints
@@ -595,6 +508,8 @@ function PanelGroupWithForwardedRef({
595
508
  if (!compareLayouts(prevLayout, nextLayout)) {
596
509
  setLayout(nextLayout);
597
510
 
511
+ committedValuesRef.current.layout = nextLayout;
512
+
598
513
  if (onLayout) {
599
514
  onLayout(
600
515
  nextLayout.map((sizePercentage) => ({
@@ -643,6 +558,8 @@ function PanelGroupWithForwardedRef({
643
558
  // This API should never read from committedValuesRef
644
559
  const getPanelStyle = useCallback(
645
560
  (panelData: PanelData) => {
561
+ const { panelDataArray } = committedValuesRef.current;
562
+
646
563
  const panelIndex = panelDataArray.indexOf(panelData);
647
564
 
648
565
  return computePanelFlexBoxStyle({
@@ -652,7 +569,7 @@ function PanelGroupWithForwardedRef({
652
569
  panelIndex,
653
570
  });
654
571
  },
655
- [dragState, layout, panelDataArray]
572
+ [dragState, layout]
656
573
  );
657
574
 
658
575
  // External APIs are safe to memoize via committed values ref
@@ -684,22 +601,97 @@ function PanelGroupWithForwardedRef({
684
601
  );
685
602
 
686
603
  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
- }
604
+ const {
605
+ autoSaveId,
606
+ id: groupId,
607
+ layout: prevLayout,
608
+ onLayout,
609
+ panelDataArray,
610
+ storage,
611
+ } = committedValuesRef.current;
612
+
613
+ panelDataArray.push(panelData);
614
+ panelDataArray.sort((panelA, panelB) => {
615
+ const orderA = panelA.order;
616
+ const orderB = panelB.order;
617
+ if (orderA == null && orderB == null) {
618
+ return 0;
619
+ } else if (orderA == null) {
620
+ return -1;
621
+ } else if (orderB == null) {
622
+ return 1;
623
+ } else {
624
+ return orderA - orderB;
625
+ }
626
+ });
627
+
628
+ // Wait until all panels have registered before we try to compute layout;
629
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
630
+ const panelElements = getPanelElementsForGroup(groupId);
631
+ if (panelElements.length !== panelDataArray.length) {
632
+ return;
633
+ }
634
+
635
+ // If this panel has been configured to persist sizing information,
636
+ // default size should be restored from local storage if possible.
637
+ let unsafeLayout: number[] | null = null;
638
+ if (autoSaveId) {
639
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
640
+ }
641
+
642
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
643
+ if (groupSizePixels <= 0) {
644
+ if (
645
+ shouldMonitorPixelBasedConstraints(
646
+ panelDataArray.map(({ constraints }) => constraints)
647
+ )
648
+ ) {
649
+ // Wait until the group has rendered a non-zero size before computing layout.
650
+ return;
651
+ }
652
+ }
653
+
654
+ if (unsafeLayout == null) {
655
+ unsafeLayout = calculateUnsafeDefaultLayout({
656
+ groupSizePixels,
657
+ panelDataArray,
701
658
  });
659
+ }
660
+
661
+ // Validate even saved layouts in case something has changed since last render
662
+ // e.g. for pixel groups, this could be the size of the window
663
+ const nextLayout = validatePanelGroupLayout({
664
+ groupSizePixels,
665
+ layout: unsafeLayout,
666
+ panelConstraints: panelDataArray.map(
667
+ (panelData) => panelData.constraints
668
+ ),
702
669
  });
670
+
671
+ if (!areEqual(prevLayout, nextLayout)) {
672
+ setLayout(nextLayout);
673
+
674
+ committedValuesRef.current.layout = nextLayout;
675
+
676
+ if (onLayout) {
677
+ onLayout(
678
+ nextLayout.map((sizePercentage) => ({
679
+ sizePercentage,
680
+ sizePixels: convertPercentageToPixels(
681
+ sizePercentage,
682
+ groupSizePixels
683
+ ),
684
+ }))
685
+ );
686
+ }
687
+
688
+ callPanelCallbacks(
689
+ groupId,
690
+ panelDataArray,
691
+ nextLayout,
692
+ panelIdToLastNotifiedMixedSizesMapRef.current
693
+ );
694
+ }
703
695
  }, []);
704
696
 
705
697
  const registerResizeHandle = useCallback((dragHandleId: string) => {
@@ -789,6 +781,8 @@ function PanelGroupWithForwardedRef({
789
781
  if (layoutChanged) {
790
782
  setLayout(nextLayout);
791
783
 
784
+ committedValuesRef.current.layout = nextLayout;
785
+
792
786
  if (onLayout) {
793
787
  onLayout(
794
788
  nextLayout.map((sizePercentage) => ({
@@ -820,19 +814,6 @@ function PanelGroupWithForwardedRef({
820
814
  panelDataArray,
821
815
  } = committedValuesRef.current;
822
816
 
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
- }
835
-
836
817
  const panelConstraintsArray = panelDataArray.map(
837
818
  (panelData) => panelData.constraints
838
819
  );
@@ -863,6 +844,8 @@ function PanelGroupWithForwardedRef({
863
844
  if (!compareLayouts(prevLayout, nextLayout)) {
864
845
  setLayout(nextLayout);
865
846
 
847
+ committedValuesRef.current.layout = nextLayout;
848
+
866
849
  if (onLayout) {
867
850
  onLayout(
868
851
  nextLayout.map((sizePercentage) => ({
@@ -912,48 +895,107 @@ function PanelGroupWithForwardedRef({
912
895
  setDragState(null);
913
896
  }, []);
914
897
 
898
+ const unregisterPanelRef = useRef<{
899
+ pendingPanelIds: Set<string>;
900
+ timeout: NodeJS.Timeout | null;
901
+ }>({
902
+ pendingPanelIds: new Set(),
903
+ timeout: null,
904
+ });
915
905
  const unregisterPanel = useCallback((panelData: PanelData) => {
916
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
906
+ const {
907
+ id: groupId,
908
+ layout: prevLayout,
909
+ onLayout,
910
+ panelDataArray,
911
+ } = committedValuesRef.current;
917
912
 
918
- setPanelDataArray((panelDataArray) => {
919
- const index = panelDataArray.indexOf(panelData);
920
- if (index >= 0) {
921
- panelDataArray = [...panelDataArray];
922
- panelDataArray.splice(index, 1);
923
- }
913
+ const index = panelDataArray.indexOf(panelData);
914
+ if (index >= 0) {
915
+ panelDataArray.splice(index, 1);
916
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
917
+ }
924
918
 
925
- return panelDataArray;
926
- });
927
- }, []);
919
+ if (unregisterPanelRef.current.timeout != null) {
920
+ clearTimeout(unregisterPanelRef.current.timeout);
921
+ }
928
922
 
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;
923
+ // Batch panel unmounts so that we only calculate layout once;
924
+ // This is more efficient and avoids misleading warnings in development mode.
925
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
926
+ unregisterPanelRef.current.timeout = setTimeout(() => {
927
+ const { pendingPanelIds } = unregisterPanelRef.current;
928
+ const map = panelIdToLastNotifiedMixedSizesMapRef.current;
929
+
930
+ // TRICKY
931
+ // Strict effects mode
932
+ let unmountDueToStrictMode = false;
933
+ pendingPanelIds.forEach((panelId) => {
934
+ pendingPanelIds.delete(panelId);
935
+
936
+ if (panelDataArray.find(({ id }) => id === panelId) == null) {
937
+ unmountDueToStrictMode = true;
938
+
939
+ // TRICKY
940
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
941
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
942
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
943
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
942
944
  }
943
- case "resize": {
944
- resizePanel(current.panelData, current.mixedSizes!);
945
- break;
945
+ });
946
+
947
+ if (!unmountDueToStrictMode) {
948
+ return;
949
+ }
950
+
951
+ if (panelDataArray.length === 0) {
952
+ // The group is unmounting; skip layout calculation.
953
+ return;
954
+ }
955
+
956
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
957
+
958
+ let unsafeLayout: number[] = calculateUnsafeDefaultLayout({
959
+ groupSizePixels,
960
+ panelDataArray,
961
+ });
962
+
963
+ // Validate even saved layouts in case something has changed since last render
964
+ // e.g. for pixel groups, this could be the size of the window
965
+ const nextLayout = validatePanelGroupLayout({
966
+ groupSizePixels,
967
+ layout: unsafeLayout,
968
+ panelConstraints: panelDataArray.map(
969
+ (panelData) => panelData.constraints
970
+ ),
971
+ });
972
+
973
+ if (!areEqual(prevLayout, nextLayout)) {
974
+ setLayout(nextLayout);
975
+
976
+ committedValuesRef.current.layout = nextLayout;
977
+
978
+ if (onLayout) {
979
+ onLayout(
980
+ nextLayout.map((sizePercentage) => ({
981
+ sizePercentage,
982
+ sizePixels: convertPercentageToPixels(
983
+ sizePercentage,
984
+ groupSizePixels
985
+ ),
986
+ }))
987
+ );
946
988
  }
989
+
990
+ callPanelCallbacks(
991
+ groupId,
992
+ panelDataArray,
993
+ nextLayout,
994
+ panelIdToLastNotifiedMixedSizesMapRef.current
995
+ );
947
996
  }
948
- }
949
- }, [
950
- collapsePanel,
951
- expandPanel,
952
- imperativeApiQueue,
953
- layout,
954
- panelDataArray,
955
- resizePanel,
956
- ]);
997
+ }, 0);
998
+ }, []);
957
999
 
958
1000
  const context = useMemo(
959
1001
  () => ({
@@ -0,0 +1,5 @@
1
+ export function getPanelElementsForGroup(groupId: string): HTMLDivElement[] {
2
+ return Array.from(
3
+ document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`)
4
+ );
5
+ }