react-resizable-panels 0.0.58 → 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;
@@ -88,7 +89,7 @@ const debounceMap: {
88
89
  } = {};
89
90
 
90
91
  function PanelGroupWithForwardedRef({
91
- autoSaveId,
92
+ autoSaveId = null,
92
93
  children,
93
94
  className: classNameFromProps = "",
94
95
  dataAttributes,
@@ -108,7 +109,6 @@ function PanelGroupWithForwardedRef({
108
109
 
109
110
  const [dragState, setDragState] = useState<DragState | null>(null);
110
111
  const [layout, setLayout] = useState<number[]>([]);
111
- const [panelDataArray, setPanelDataArray] = useState<PanelData[]>([]);
112
112
 
113
113
  const panelIdToLastNotifiedMixedSizesMapRef = useRef<
114
114
  Record<string, MixedSizes>
@@ -117,6 +117,7 @@ function PanelGroupWithForwardedRef({
117
117
  const prevDeltaRef = useRef<number>(0);
118
118
 
119
119
  const committedValuesRef = useRef<{
120
+ autoSaveId: string | null;
120
121
  direction: Direction;
121
122
  dragState: DragState | null;
122
123
  id: string;
@@ -125,7 +126,9 @@ function PanelGroupWithForwardedRef({
125
126
  layout: number[];
126
127
  onLayout: PanelGroupOnLayout | null;
127
128
  panelDataArray: PanelData[];
129
+ storage: PanelGroupStorage;
128
130
  }>({
131
+ autoSaveId,
129
132
  direction,
130
133
  dragState,
131
134
  id: groupId,
@@ -133,7 +136,8 @@ function PanelGroupWithForwardedRef({
133
136
  keyboardResizeByPixels,
134
137
  layout,
135
138
  onLayout,
136
- panelDataArray,
139
+ panelDataArray: [],
140
+ storage,
137
141
  });
138
142
 
139
143
  const devWarningsRef = useRef<{
@@ -191,6 +195,8 @@ function PanelGroupWithForwardedRef({
191
195
  if (!areEqual(prevLayout, safeLayout)) {
192
196
  setLayout(safeLayout);
193
197
 
198
+ committedValuesRef.current.layout = safeLayout;
199
+
194
200
  if (onLayout) {
195
201
  onLayout(
196
202
  safeLayout.map((sizePercentage) => ({
@@ -216,23 +222,28 @@ function PanelGroupWithForwardedRef({
216
222
  );
217
223
 
218
224
  useIsomorphicLayoutEffect(() => {
225
+ committedValuesRef.current.autoSaveId = autoSaveId;
219
226
  committedValuesRef.current.direction = direction;
220
227
  committedValuesRef.current.dragState = dragState;
221
228
  committedValuesRef.current.id = groupId;
222
- committedValuesRef.current.layout = layout;
223
229
  committedValuesRef.current.onLayout = onLayout;
224
- 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
225
234
  });
226
235
 
227
236
  useWindowSplitterPanelGroupBehavior({
228
237
  committedValuesRef,
229
238
  groupId,
230
239
  layout,
231
- panelDataArray,
240
+ panelDataArray: committedValuesRef.current.panelDataArray,
232
241
  setLayout,
233
242
  });
234
243
 
235
244
  useEffect(() => {
245
+ const { panelDataArray } = committedValuesRef.current;
246
+
236
247
  // If this panel has been configured to persist sizing information, save sizes to local storage.
237
248
  if (autoSaveId) {
238
249
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -248,73 +259,11 @@ function PanelGroupWithForwardedRef({
248
259
  }
249
260
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
250
261
  }
251
- }, [autoSaveId, layout, panelDataArray, storage]);
262
+ }, [autoSaveId, layout, storage]);
252
263
 
253
- // Once all panels have registered themselves,
254
- // Compute the initial sizes based on default weights.
255
- // This assumes that panels register during initial mount (no conditional rendering)!
256
264
  useIsomorphicLayoutEffect(() => {
257
- const { id: groupId, layout, onLayout } = committedValuesRef.current;
258
- if (layout.length === panelDataArray.length) {
259
- // Only compute (or restore) default layout once per panel configuration.
260
- return;
261
- }
262
-
263
- // If this panel has been configured to persist sizing information,
264
- // default size should be restored from local storage if possible.
265
- let unsafeLayout: number[] | null = null;
266
- if (autoSaveId) {
267
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
268
- }
269
-
270
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
271
- if (groupSizePixels <= 0) {
272
- // Wait until the group has rendered a non-zero size before computing layout.
273
- return;
274
- }
275
-
276
- if (unsafeLayout == null) {
277
- unsafeLayout = calculateUnsafeDefaultLayout({
278
- groupSizePixels,
279
- panelDataArray,
280
- });
281
- }
282
-
283
- // Validate even saved layouts in case something has changed since last render
284
- // e.g. for pixel groups, this could be the size of the window
285
- const validatedLayout = validatePanelGroupLayout({
286
- groupSizePixels,
287
- layout: unsafeLayout,
288
- panelConstraints: panelDataArray.map(
289
- (panelData) => panelData.constraints
290
- ),
291
- });
292
-
293
- if (!areEqual(layout, validatedLayout)) {
294
- setLayout(validatedLayout);
295
- }
296
-
297
- if (onLayout) {
298
- onLayout(
299
- validatedLayout.map((sizePercentage) => ({
300
- sizePercentage,
301
- sizePixels: convertPercentageToPixels(
302
- sizePercentage,
303
- groupSizePixels
304
- ),
305
- }))
306
- );
307
- }
308
-
309
- callPanelCallbacks(
310
- groupId,
311
- panelDataArray,
312
- validatedLayout,
313
- panelIdToLastNotifiedMixedSizesMapRef.current
314
- );
315
- }, [autoSaveId, layout, panelDataArray, storage]);
265
+ const { panelDataArray } = committedValuesRef.current;
316
266
 
317
- useIsomorphicLayoutEffect(() => {
318
267
  const constraints = panelDataArray.map(({ constraints }) => constraints);
319
268
  if (!shouldMonitorPixelBasedConstraints(constraints)) {
320
269
  // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
@@ -342,6 +291,8 @@ function PanelGroupWithForwardedRef({
342
291
  if (!areEqual(prevLayout, nextLayout)) {
343
292
  setLayout(nextLayout);
344
293
 
294
+ committedValuesRef.current.layout = nextLayout;
295
+
345
296
  if (onLayout) {
346
297
  onLayout(
347
298
  nextLayout.map((sizePercentage) => ({
@@ -369,11 +320,13 @@ function PanelGroupWithForwardedRef({
369
320
  resizeObserver.disconnect();
370
321
  };
371
322
  }
372
- }, [groupId, panelDataArray]);
323
+ }, [groupId]);
373
324
 
374
325
  // DEV warnings
375
326
  useEffect(() => {
376
327
  if (isDevelopment) {
328
+ const { panelDataArray } = committedValuesRef.current;
329
+
377
330
  const {
378
331
  didLogIdAndOrderWarning,
379
332
  didLogPanelConstraintsWarning,
@@ -381,8 +334,6 @@ function PanelGroupWithForwardedRef({
381
334
  } = devWarningsRef.current;
382
335
 
383
336
  if (!didLogIdAndOrderWarning) {
384
- const { panelDataArray } = committedValuesRef.current;
385
-
386
337
  const panelIds = panelDataArray.map(({ id }) => id);
387
338
 
388
339
  devWarningsRef.current.prevPanelIds = panelIds;
@@ -480,6 +431,8 @@ function PanelGroupWithForwardedRef({
480
431
  if (!compareLayouts(prevLayout, nextLayout)) {
481
432
  setLayout(nextLayout);
482
433
 
434
+ committedValuesRef.current.layout = nextLayout;
435
+
483
436
  if (onLayout) {
484
437
  onLayout(
485
438
  nextLayout.map((sizePercentage) => ({
@@ -555,6 +508,8 @@ function PanelGroupWithForwardedRef({
555
508
  if (!compareLayouts(prevLayout, nextLayout)) {
556
509
  setLayout(nextLayout);
557
510
 
511
+ committedValuesRef.current.layout = nextLayout;
512
+
558
513
  if (onLayout) {
559
514
  onLayout(
560
515
  nextLayout.map((sizePercentage) => ({
@@ -603,6 +558,8 @@ function PanelGroupWithForwardedRef({
603
558
  // This API should never read from committedValuesRef
604
559
  const getPanelStyle = useCallback(
605
560
  (panelData: PanelData) => {
561
+ const { panelDataArray } = committedValuesRef.current;
562
+
606
563
  const panelIndex = panelDataArray.indexOf(panelData);
607
564
 
608
565
  return computePanelFlexBoxStyle({
@@ -612,7 +569,7 @@ function PanelGroupWithForwardedRef({
612
569
  panelIndex,
613
570
  });
614
571
  },
615
- [dragState, layout, panelDataArray]
572
+ [dragState, layout]
616
573
  );
617
574
 
618
575
  // External APIs are safe to memoize via committed values ref
@@ -644,22 +601,97 @@ function PanelGroupWithForwardedRef({
644
601
  );
645
602
 
646
603
  const registerPanel = useCallback((panelData: PanelData) => {
647
- setPanelDataArray((prevPanelDataArray) => {
648
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
649
- return nextPanelDataArray.sort((panelA, panelB) => {
650
- const orderA = panelA.order;
651
- const orderB = panelB.order;
652
- if (orderA == null && orderB == null) {
653
- return 0;
654
- } else if (orderA == null) {
655
- return -1;
656
- } else if (orderB == null) {
657
- return 1;
658
- } else {
659
- return orderA - orderB;
660
- }
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,
661
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
+ ),
662
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
+ }
663
695
  }, []);
664
696
 
665
697
  const registerResizeHandle = useCallback((dragHandleId: string) => {
@@ -749,6 +781,8 @@ function PanelGroupWithForwardedRef({
749
781
  if (layoutChanged) {
750
782
  setLayout(nextLayout);
751
783
 
784
+ committedValuesRef.current.layout = nextLayout;
785
+
752
786
  if (onLayout) {
753
787
  onLayout(
754
788
  nextLayout.map((sizePercentage) => ({
@@ -810,6 +844,8 @@ function PanelGroupWithForwardedRef({
810
844
  if (!compareLayouts(prevLayout, nextLayout)) {
811
845
  setLayout(nextLayout);
812
846
 
847
+ committedValuesRef.current.layout = nextLayout;
848
+
813
849
  if (onLayout) {
814
850
  onLayout(
815
851
  nextLayout.map((sizePercentage) => ({
@@ -859,18 +895,106 @@ function PanelGroupWithForwardedRef({
859
895
  setDragState(null);
860
896
  }, []);
861
897
 
898
+ const unregisterPanelRef = useRef<{
899
+ pendingPanelIds: Set<string>;
900
+ timeout: NodeJS.Timeout | null;
901
+ }>({
902
+ pendingPanelIds: new Set(),
903
+ timeout: null,
904
+ });
862
905
  const unregisterPanel = useCallback((panelData: PanelData) => {
863
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
906
+ const {
907
+ id: groupId,
908
+ layout: prevLayout,
909
+ onLayout,
910
+ panelDataArray,
911
+ } = committedValuesRef.current;
912
+
913
+ const index = panelDataArray.indexOf(panelData);
914
+ if (index >= 0) {
915
+ panelDataArray.splice(index, 1);
916
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
917
+ }
918
+
919
+ if (unregisterPanelRef.current.timeout != null) {
920
+ clearTimeout(unregisterPanelRef.current.timeout);
921
+ }
922
+
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];
944
+ }
945
+ });
864
946
 
865
- setPanelDataArray((panelDataArray) => {
866
- const index = panelDataArray.indexOf(panelData);
867
- if (index >= 0) {
868
- panelDataArray = [...panelDataArray];
869
- panelDataArray.splice(index, 1);
947
+ if (!unmountDueToStrictMode) {
948
+ return;
870
949
  }
871
950
 
872
- return panelDataArray;
873
- });
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
+ );
988
+ }
989
+
990
+ callPanelCallbacks(
991
+ groupId,
992
+ panelDataArray,
993
+ nextLayout,
994
+ panelIdToLastNotifiedMixedSizesMapRef.current
995
+ );
996
+ }
997
+ }, 0);
874
998
  }, []);
875
999
 
876
1000
  const context = useMemo(
@@ -1,6 +1,5 @@
1
1
  import { isDevelopment } from "#is-development";
2
2
  import { PanelData } from "../Panel";
3
- import { PRECISION } from "../constants";
4
3
  import { Direction } from "../types";
5
4
  import { adjustLayoutByDelta } from "../utils/adjustLayoutByDelta";
6
5
  import { assert } from "../utils/assert";
@@ -12,6 +11,7 @@ import { getPanelGroupElement } from "../utils/dom/getPanelGroupElement";
12
11
  import { getResizeHandleElementsForGroup } from "../utils/dom/getResizeHandleElementsForGroup";
13
12
  import { getResizeHandlePanelIds } from "../utils/dom/getResizeHandlePanelIds";
14
13
  import { getPercentageSizeFromMixedSizes } from "../utils/getPercentageSizeFromMixedSizes";
14
+ import { fuzzyNumbersEqual } from "../utils/numbers/fuzzyNumbersEqual";
15
15
  import { RefObject, useEffect, useRef } from "../vendor/react";
16
16
  import useIsomorphicLayoutEffect from "./useIsomorphicEffect";
17
17
 
@@ -95,13 +95,11 @@ export function useWindowSplitterPanelGroupBehavior({
95
95
  }, [groupId, layout, panelDataArray]);
96
96
 
97
97
  useEffect(() => {
98
- const { direction, panelDataArray } = committedValuesRef.current!;
98
+ const { panelDataArray } = committedValuesRef.current!;
99
99
 
100
100
  const groupElement = getPanelGroupElement(groupId);
101
101
  assert(groupElement != null, `No group found for id "${groupId}"`);
102
102
 
103
- const { height, width } = groupElement.getBoundingClientRect();
104
-
105
103
  const handles = getResizeHandleElementsForGroup(groupId);
106
104
  const cleanupFunctions = handles.map((handle) => {
107
105
  const handleId = handle.getAttribute("data-panel-resize-handle-id")!;
@@ -130,9 +128,18 @@ export function useWindowSplitterPanelGroupBehavior({
130
128
  if (index >= 0) {
131
129
  const panelData = panelDataArray[index];
132
130
  const size = layout[index];
133
- if (size != null) {
131
+ if (size != null && panelData.constraints.collapsible) {
134
132
  const groupSizePixels = getAvailableGroupSizePixels(groupId);
135
133
 
134
+ const collapsedSize =
135
+ getPercentageSizeFromMixedSizes(
136
+ {
137
+ sizePercentage:
138
+ panelData.constraints.collapsedSizePercentage,
139
+ sizePixels: panelData.constraints.collapsedSizePixels,
140
+ },
141
+ groupSizePixels
142
+ ) ?? 0;
136
143
  const minSize =
137
144
  getPercentageSizeFromMixedSizes(
138
145
  {
@@ -142,17 +149,10 @@ export function useWindowSplitterPanelGroupBehavior({
142
149
  groupSizePixels
143
150
  ) ?? 0;
144
151
 
145
- let delta = 0;
146
- if (
147
- size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)
148
- ) {
149
- delta = direction === "horizontal" ? width : height;
150
- } else {
151
- delta = -(direction === "horizontal" ? width : height);
152
- }
153
-
154
152
  const nextLayout = adjustLayoutByDelta({
155
- delta,
153
+ delta: fuzzyNumbersEqual(size, collapsedSize)
154
+ ? minSize - collapsedSize
155
+ : collapsedSize - size,
156
156
  groupSizePixels,
157
157
  layout,
158
158
  panelConstraints: panelDataArray.map(
@@ -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
+ }