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.
@@ -67,6 +67,7 @@ function PanelWithForwardedRef({
67
67
  expandPanel,
68
68
  getPanelSize,
69
69
  getPanelStyle,
70
+ groupId,
70
71
  isPanelCollapsed,
71
72
  registerPanel,
72
73
  resizePanel,
@@ -132,6 +133,7 @@ function PanelWithForwardedRef({
132
133
  // CSS selectors
133
134
  "data-panel": "",
134
135
  "data-panel-id": panelId,
136
+ "data-panel-group-id": groupId,
135
137
  // e2e test attributes
136
138
  "data-panel-collapsible": undefined,
137
139
  "data-panel-size": undefined
@@ -144,8 +146,6 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
144
146
  PanelWithForwardedRef.displayName = "Panel";
145
147
  Panel.displayName = "forwardRef(Panel)";
146
148
 
147
- const PRECISION = 10;
148
-
149
149
  function convertPixelsToPercentage(pixels, groupSizePixels) {
150
150
  return pixels / groupSizePixels * 100;
151
151
  }
@@ -223,6 +223,8 @@ function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, gr
223
223
  };
224
224
  }
225
225
 
226
+ const PRECISION = 10;
227
+
226
228
  function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
227
229
  actual = parseFloat(actual.toFixed(fractionDigits));
228
230
  expected = parseFloat(expected.toFixed(fractionDigits));
@@ -616,15 +618,10 @@ function useWindowSplitterPanelGroupBehavior({
616
618
  });
617
619
  useEffect(() => {
618
620
  const {
619
- direction,
620
621
  panelDataArray
621
622
  } = committedValuesRef.current;
622
623
  const groupElement = getPanelGroupElement(groupId);
623
624
  assert(groupElement != null, `No group found for id "${groupId}"`);
624
- const {
625
- height,
626
- width
627
- } = groupElement.getBoundingClientRect();
628
625
  const handles = getResizeHandleElementsForGroup(groupId);
629
626
  const cleanupFunctions = handles.map(handle => {
630
627
  const handleId = handle.getAttribute("data-panel-resize-handle-id");
@@ -644,21 +641,19 @@ function useWindowSplitterPanelGroupBehavior({
644
641
  if (index >= 0) {
645
642
  const panelData = panelDataArray[index];
646
643
  const size = layout[index];
647
- if (size != null) {
648
- var _getPercentageSizeFro;
644
+ if (size != null && panelData.constraints.collapsible) {
645
+ var _getPercentageSizeFro, _getPercentageSizeFro2;
649
646
  const groupSizePixels = getAvailableGroupSizePixels(groupId);
650
- const minSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
647
+ const collapsedSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
648
+ sizePercentage: panelData.constraints.collapsedSizePercentage,
649
+ sizePixels: panelData.constraints.collapsedSizePixels
650
+ }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
651
+ const minSize = (_getPercentageSizeFro2 = getPercentageSizeFromMixedSizes({
651
652
  sizePercentage: panelData.constraints.minSizePercentage,
652
653
  sizePixels: panelData.constraints.minSizePixels
653
- }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
654
- let delta = 0;
655
- if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
656
- delta = direction === "horizontal" ? width : height;
657
- } else {
658
- delta = -(direction === "horizontal" ? width : height);
659
- }
654
+ }, groupSizePixels)) !== null && _getPercentageSizeFro2 !== void 0 ? _getPercentageSizeFro2 : 0;
660
655
  const nextLayout = adjustLayoutByDelta({
661
- delta,
656
+ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size,
662
657
  groupSizePixels,
663
658
  layout,
664
659
  panelConstraints: panelDataArray.map(panelData => panelData.constraints),
@@ -779,6 +774,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
779
774
  }
780
775
  }
781
776
 
777
+ function calculateUnsafeDefaultLayout({
778
+ groupSizePixels,
779
+ panelDataArray
780
+ }) {
781
+ const layout = Array(panelDataArray.length);
782
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
783
+ let numPanelsWithSizes = 0;
784
+ let remainingSize = 100;
785
+
786
+ // Distribute default sizes first
787
+ for (let index = 0; index < panelDataArray.length; index++) {
788
+ const {
789
+ defaultSizePercentage
790
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
791
+ if (defaultSizePercentage != null) {
792
+ numPanelsWithSizes++;
793
+ layout[index] = defaultSizePercentage;
794
+ remainingSize -= defaultSizePercentage;
795
+ }
796
+ }
797
+
798
+ // Remaining size should be distributed evenly between panels without default sizes
799
+ for (let index = 0; index < panelDataArray.length; index++) {
800
+ const {
801
+ defaultSizePercentage
802
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
803
+ if (defaultSizePercentage != null) {
804
+ continue;
805
+ }
806
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
807
+ const size = remainingSize / numRemainingPanels;
808
+ numPanelsWithSizes++;
809
+ layout[index] = size;
810
+ remainingSize -= size;
811
+ }
812
+ return layout;
813
+ }
814
+
782
815
  function convertPercentageToPixels(percentage, groupSizePixels) {
783
816
  return percentage / 100 * groupSizePixels;
784
817
  }
@@ -929,6 +962,10 @@ function debounce(callback, durationMs = 10) {
929
962
  return callable;
930
963
  }
931
964
 
965
+ function getPanelElementsForGroup(groupId) {
966
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
967
+ }
968
+
932
969
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
933
970
  // or on a browser with cookies/storage disabled.
934
971
  // In either case, this function avoids accessing localStorage until needed,
@@ -984,6 +1021,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
984
1021
  } catch (error) {}
985
1022
  return null;
986
1023
  }
1024
+ function loadPanelLayout(autoSaveId, panels, storage) {
1025
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
1026
+ if (state) {
1027
+ var _state$key;
1028
+ const key = getSerializationKey(panels);
1029
+ return (_state$key = state[key]) !== null && _state$key !== void 0 ? _state$key : null;
1030
+ }
1031
+ return null;
1032
+ }
987
1033
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
988
1034
  const key = getSerializationKey(panels);
989
1035
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -995,6 +1041,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
995
1041
  }
996
1042
  }
997
1043
 
1044
+ function shouldMonitorPixelBasedConstraints(constraints) {
1045
+ return constraints.some(constraints => {
1046
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1047
+ });
1048
+ }
1049
+
998
1050
  // All units must be in percentages; pixel values should be pre-converted
999
1051
  function validatePanelGroupLayout({
1000
1052
  groupSizePixels,
@@ -1063,7 +1115,7 @@ const defaultStorage = {
1063
1115
  };
1064
1116
  const debounceMap = {};
1065
1117
  function PanelGroupWithForwardedRef({
1066
- autoSaveId,
1118
+ autoSaveId = null,
1067
1119
  children,
1068
1120
  className: classNameFromProps = "",
1069
1121
  dataAttributes,
@@ -1080,11 +1132,11 @@ function PanelGroupWithForwardedRef({
1080
1132
  const groupId = useUniqueId(idFromProps);
1081
1133
  const [dragState, setDragState] = useState(null);
1082
1134
  const [layout, setLayout] = useState([]);
1083
- const [panelDataArray, setPanelDataArray] = useState([]);
1084
1135
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1085
1136
  const panelSizeBeforeCollapseRef = useRef(new Map());
1086
1137
  const prevDeltaRef = useRef(0);
1087
1138
  const committedValuesRef = useRef({
1139
+ autoSaveId,
1088
1140
  direction,
1089
1141
  dragState,
1090
1142
  id: groupId,
@@ -1092,7 +1144,8 @@ function PanelGroupWithForwardedRef({
1092
1144
  keyboardResizeByPixels,
1093
1145
  layout,
1094
1146
  onLayout,
1095
- panelDataArray
1147
+ panelDataArray: [],
1148
+ storage
1096
1149
  });
1097
1150
  useRef({
1098
1151
  didLogIdAndOrderWarning: false,
@@ -1130,6 +1183,7 @@ function PanelGroupWithForwardedRef({
1130
1183
  });
1131
1184
  if (!areEqual(prevLayout, safeLayout)) {
1132
1185
  setLayout(safeLayout);
1186
+ committedValuesRef.current.layout = safeLayout;
1133
1187
  if (onLayout) {
1134
1188
  onLayout(safeLayout.map(sizePercentage => ({
1135
1189
  sizePercentage,
@@ -1140,14 +1194,19 @@ function PanelGroupWithForwardedRef({
1140
1194
  }
1141
1195
  }
1142
1196
  }), []);
1197
+
1143
1198
  useWindowSplitterPanelGroupBehavior({
1144
1199
  committedValuesRef,
1145
1200
  groupId,
1146
1201
  layout,
1147
- panelDataArray,
1202
+ panelDataArray: committedValuesRef.current.panelDataArray,
1148
1203
  setLayout
1149
1204
  });
1150
1205
  useEffect(() => {
1206
+ const {
1207
+ panelDataArray
1208
+ } = committedValuesRef.current;
1209
+
1151
1210
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1152
1211
  if (autoSaveId) {
1153
1212
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1160,7 +1219,7 @@ function PanelGroupWithForwardedRef({
1160
1219
  }
1161
1220
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1162
1221
  }
1163
- }, [autoSaveId, layout, panelDataArray, storage]);
1222
+ }, [autoSaveId, layout, storage]);
1164
1223
 
1165
1224
  // DEV warnings
1166
1225
  useEffect(() => {
@@ -1197,6 +1256,7 @@ function PanelGroupWithForwardedRef({
1197
1256
  });
1198
1257
  if (!compareLayouts(prevLayout, nextLayout)) {
1199
1258
  setLayout(nextLayout);
1259
+ committedValuesRef.current.layout = nextLayout;
1200
1260
  if (onLayout) {
1201
1261
  onLayout(nextLayout.map(sizePercentage => ({
1202
1262
  sizePercentage,
@@ -1241,6 +1301,7 @@ function PanelGroupWithForwardedRef({
1241
1301
  });
1242
1302
  if (!compareLayouts(prevLayout, nextLayout)) {
1243
1303
  setLayout(nextLayout);
1304
+ committedValuesRef.current.layout = nextLayout;
1244
1305
  if (onLayout) {
1245
1306
  onLayout(nextLayout.map(sizePercentage => ({
1246
1307
  sizePercentage,
@@ -1271,6 +1332,9 @@ function PanelGroupWithForwardedRef({
1271
1332
 
1272
1333
  // This API should never read from committedValuesRef
1273
1334
  const getPanelStyle = useCallback(panelData => {
1335
+ const {
1336
+ panelDataArray
1337
+ } = committedValuesRef.current;
1274
1338
  const panelIndex = panelDataArray.indexOf(panelData);
1275
1339
  return computePanelFlexBoxStyle({
1276
1340
  dragState,
@@ -1278,7 +1342,7 @@ function PanelGroupWithForwardedRef({
1278
1342
  panelData: panelDataArray,
1279
1343
  panelIndex
1280
1344
  });
1281
- }, [dragState, layout, panelDataArray]);
1345
+ }, [dragState, layout]);
1282
1346
 
1283
1347
  // External APIs are safe to memoize via committed values ref
1284
1348
  const isPanelCollapsed = useCallback(panelData => {
@@ -1308,22 +1372,76 @@ function PanelGroupWithForwardedRef({
1308
1372
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1309
1373
  }, [groupId]);
1310
1374
  const registerPanel = useCallback(panelData => {
1311
- setPanelDataArray(prevPanelDataArray => {
1312
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1313
- return nextPanelDataArray.sort((panelA, panelB) => {
1314
- const orderA = panelA.order;
1315
- const orderB = panelB.order;
1316
- if (orderA == null && orderB == null) {
1317
- return 0;
1318
- } else if (orderA == null) {
1319
- return -1;
1320
- } else if (orderB == null) {
1321
- return 1;
1322
- } else {
1323
- return orderA - orderB;
1324
- }
1375
+ const {
1376
+ autoSaveId,
1377
+ id: groupId,
1378
+ layout: prevLayout,
1379
+ onLayout,
1380
+ panelDataArray,
1381
+ storage
1382
+ } = committedValuesRef.current;
1383
+ panelDataArray.push(panelData);
1384
+ panelDataArray.sort((panelA, panelB) => {
1385
+ const orderA = panelA.order;
1386
+ const orderB = panelB.order;
1387
+ if (orderA == null && orderB == null) {
1388
+ return 0;
1389
+ } else if (orderA == null) {
1390
+ return -1;
1391
+ } else if (orderB == null) {
1392
+ return 1;
1393
+ } else {
1394
+ return orderA - orderB;
1395
+ }
1396
+ });
1397
+
1398
+ // Wait until all panels have registered before we try to compute layout;
1399
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1400
+ const panelElements = getPanelElementsForGroup(groupId);
1401
+ if (panelElements.length !== panelDataArray.length) {
1402
+ return;
1403
+ }
1404
+
1405
+ // If this panel has been configured to persist sizing information,
1406
+ // default size should be restored from local storage if possible.
1407
+ let unsafeLayout = null;
1408
+ if (autoSaveId) {
1409
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1410
+ }
1411
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1412
+ if (groupSizePixels <= 0) {
1413
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1414
+ constraints
1415
+ }) => constraints))) {
1416
+ // Wait until the group has rendered a non-zero size before computing layout.
1417
+ return;
1418
+ }
1419
+ }
1420
+ if (unsafeLayout == null) {
1421
+ unsafeLayout = calculateUnsafeDefaultLayout({
1422
+ groupSizePixels,
1423
+ panelDataArray
1325
1424
  });
1425
+ }
1426
+
1427
+ // Validate even saved layouts in case something has changed since last render
1428
+ // e.g. for pixel groups, this could be the size of the window
1429
+ const nextLayout = validatePanelGroupLayout({
1430
+ groupSizePixels,
1431
+ layout: unsafeLayout,
1432
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1326
1433
  });
1434
+ if (!areEqual(prevLayout, nextLayout)) {
1435
+ setLayout(nextLayout);
1436
+ committedValuesRef.current.layout = nextLayout;
1437
+ if (onLayout) {
1438
+ onLayout(nextLayout.map(sizePercentage => ({
1439
+ sizePercentage,
1440
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1441
+ })));
1442
+ }
1443
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1444
+ }
1327
1445
  }, []);
1328
1446
  const registerResizeHandle = useCallback(dragHandleId => {
1329
1447
  return function resizeHandler(event) {
@@ -1393,6 +1511,7 @@ function PanelGroupWithForwardedRef({
1393
1511
  }
1394
1512
  if (layoutChanged) {
1395
1513
  setLayout(nextLayout);
1514
+ committedValuesRef.current.layout = nextLayout;
1396
1515
  if (onLayout) {
1397
1516
  onLayout(nextLayout.map(sizePercentage => ({
1398
1517
  sizePercentage,
@@ -1430,6 +1549,7 @@ function PanelGroupWithForwardedRef({
1430
1549
  });
1431
1550
  if (!compareLayouts(prevLayout, nextLayout)) {
1432
1551
  setLayout(nextLayout);
1552
+ committedValuesRef.current.layout = nextLayout;
1433
1553
  if (onLayout) {
1434
1554
  onLayout(nextLayout.map(sizePercentage => ({
1435
1555
  sizePercentage,
@@ -1457,16 +1577,84 @@ function PanelGroupWithForwardedRef({
1457
1577
  resetGlobalCursorStyle();
1458
1578
  setDragState(null);
1459
1579
  }, []);
1580
+ const unregisterPanelRef = useRef({
1581
+ pendingPanelIds: new Set(),
1582
+ timeout: null
1583
+ });
1460
1584
  const unregisterPanel = useCallback(panelData => {
1461
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1462
- setPanelDataArray(panelDataArray => {
1463
- const index = panelDataArray.indexOf(panelData);
1464
- if (index >= 0) {
1465
- panelDataArray = [...panelDataArray];
1466
- panelDataArray.splice(index, 1);
1585
+ const {
1586
+ id: groupId,
1587
+ layout: prevLayout,
1588
+ onLayout,
1589
+ panelDataArray
1590
+ } = committedValuesRef.current;
1591
+ const index = panelDataArray.indexOf(panelData);
1592
+ if (index >= 0) {
1593
+ panelDataArray.splice(index, 1);
1594
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1595
+ }
1596
+ if (unregisterPanelRef.current.timeout != null) {
1597
+ clearTimeout(unregisterPanelRef.current.timeout);
1598
+ }
1599
+
1600
+ // Batch panel unmounts so that we only calculate layout once;
1601
+ // This is more efficient and avoids misleading warnings in development mode.
1602
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1603
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1604
+ const {
1605
+ pendingPanelIds
1606
+ } = unregisterPanelRef.current;
1607
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1608
+
1609
+ // TRICKY
1610
+ // Strict effects mode
1611
+ let unmountDueToStrictMode = false;
1612
+ pendingPanelIds.forEach(panelId => {
1613
+ pendingPanelIds.delete(panelId);
1614
+ if (panelDataArray.find(({
1615
+ id
1616
+ }) => id === panelId) == null) {
1617
+ unmountDueToStrictMode = true;
1618
+
1619
+ // TRICKY
1620
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1621
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1622
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1623
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1624
+ }
1625
+ });
1626
+ if (!unmountDueToStrictMode) {
1627
+ return;
1467
1628
  }
1468
- return panelDataArray;
1469
- });
1629
+ if (panelDataArray.length === 0) {
1630
+ // The group is unmounting; skip layout calculation.
1631
+ return;
1632
+ }
1633
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1634
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1635
+ groupSizePixels,
1636
+ panelDataArray
1637
+ });
1638
+
1639
+ // Validate even saved layouts in case something has changed since last render
1640
+ // e.g. for pixel groups, this could be the size of the window
1641
+ const nextLayout = validatePanelGroupLayout({
1642
+ groupSizePixels,
1643
+ layout: unsafeLayout,
1644
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1645
+ });
1646
+ if (!areEqual(prevLayout, nextLayout)) {
1647
+ setLayout(nextLayout);
1648
+ committedValuesRef.current.layout = nextLayout;
1649
+ if (onLayout) {
1650
+ onLayout(nextLayout.map(sizePercentage => ({
1651
+ sizePercentage,
1652
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1653
+ })));
1654
+ }
1655
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1656
+ }
1657
+ }, 0);
1470
1658
  }, []);
1471
1659
  const context = useMemo(() => ({
1472
1660
  collapsePanel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-resizable-panels",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "React components for resizable panel groups/layouts",
5
5
  "author": "Brian Vaughn <brian.david.vaughn@gmail.com>",
6
6
  "license": "MIT",
package/src/Panel.ts CHANGED
@@ -115,6 +115,7 @@ export function PanelWithForwardedRef({
115
115
  expandPanel,
116
116
  getPanelSize,
117
117
  getPanelStyle,
118
+ groupId,
118
119
  isPanelCollapsed,
119
120
  registerPanel,
120
121
  resizePanel,
@@ -250,6 +251,7 @@ export function PanelWithForwardedRef({
250
251
  // CSS selectors
251
252
  "data-panel": "",
252
253
  "data-panel-id": panelId,
254
+ "data-panel-group-id": groupId,
253
255
 
254
256
  // e2e test attributes
255
257
  "data-panel-collapsible": isDevelopment