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,
@@ -143,6 +144,7 @@ function PanelWithForwardedRef({
143
144
  // CSS selectors
144
145
  "data-panel": "",
145
146
  "data-panel-id": panelId,
147
+ "data-panel-group-id": groupId,
146
148
  // e2e test attributes
147
149
  "data-panel-collapsible": collapsible || undefined ,
148
150
  "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
@@ -155,8 +157,6 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
155
157
  PanelWithForwardedRef.displayName = "Panel";
156
158
  Panel.displayName = "forwardRef(Panel)";
157
159
 
158
- const PRECISION = 10;
159
-
160
160
  function convertPixelsToPercentage(pixels, groupSizePixels) {
161
161
  return pixels / groupSizePixels * 100;
162
162
  }
@@ -234,6 +234,8 @@ function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, gr
234
234
  };
235
235
  }
236
236
 
237
+ const PRECISION = 10;
238
+
237
239
  function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
238
240
  actual = parseFloat(actual.toFixed(fractionDigits));
239
241
  expected = parseFloat(expected.toFixed(fractionDigits));
@@ -627,15 +629,10 @@ function useWindowSplitterPanelGroupBehavior({
627
629
  });
628
630
  useEffect(() => {
629
631
  const {
630
- direction,
631
632
  panelDataArray
632
633
  } = committedValuesRef.current;
633
634
  const groupElement = getPanelGroupElement(groupId);
634
635
  assert(groupElement != null, `No group found for id "${groupId}"`);
635
- const {
636
- height,
637
- width
638
- } = groupElement.getBoundingClientRect();
639
636
  const handles = getResizeHandleElementsForGroup(groupId);
640
637
  const cleanupFunctions = handles.map(handle => {
641
638
  const handleId = handle.getAttribute("data-panel-resize-handle-id");
@@ -655,21 +652,19 @@ function useWindowSplitterPanelGroupBehavior({
655
652
  if (index >= 0) {
656
653
  const panelData = panelDataArray[index];
657
654
  const size = layout[index];
658
- if (size != null) {
659
- var _getPercentageSizeFro;
655
+ if (size != null && panelData.constraints.collapsible) {
656
+ var _getPercentageSizeFro, _getPercentageSizeFro2;
660
657
  const groupSizePixels = getAvailableGroupSizePixels(groupId);
661
- const minSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
658
+ const collapsedSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
659
+ sizePercentage: panelData.constraints.collapsedSizePercentage,
660
+ sizePixels: panelData.constraints.collapsedSizePixels
661
+ }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
662
+ const minSize = (_getPercentageSizeFro2 = getPercentageSizeFromMixedSizes({
662
663
  sizePercentage: panelData.constraints.minSizePercentage,
663
664
  sizePixels: panelData.constraints.minSizePixels
664
- }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
665
- let delta = 0;
666
- if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
667
- delta = direction === "horizontal" ? width : height;
668
- } else {
669
- delta = -(direction === "horizontal" ? width : height);
670
- }
665
+ }, groupSizePixels)) !== null && _getPercentageSizeFro2 !== void 0 ? _getPercentageSizeFro2 : 0;
671
666
  const nextLayout = adjustLayoutByDelta({
672
- delta,
667
+ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size,
673
668
  groupSizePixels,
674
669
  layout,
675
670
  panelConstraints: panelDataArray.map(panelData => panelData.constraints),
@@ -790,6 +785,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
790
785
  }
791
786
  }
792
787
 
788
+ function calculateUnsafeDefaultLayout({
789
+ groupSizePixels,
790
+ panelDataArray
791
+ }) {
792
+ const layout = Array(panelDataArray.length);
793
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
794
+ let numPanelsWithSizes = 0;
795
+ let remainingSize = 100;
796
+
797
+ // Distribute default sizes first
798
+ for (let index = 0; index < panelDataArray.length; index++) {
799
+ const {
800
+ defaultSizePercentage
801
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
802
+ if (defaultSizePercentage != null) {
803
+ numPanelsWithSizes++;
804
+ layout[index] = defaultSizePercentage;
805
+ remainingSize -= defaultSizePercentage;
806
+ }
807
+ }
808
+
809
+ // Remaining size should be distributed evenly between panels without default sizes
810
+ for (let index = 0; index < panelDataArray.length; index++) {
811
+ const {
812
+ defaultSizePercentage
813
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
814
+ if (defaultSizePercentage != null) {
815
+ continue;
816
+ }
817
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
818
+ const size = remainingSize / numRemainingPanels;
819
+ numPanelsWithSizes++;
820
+ layout[index] = size;
821
+ remainingSize -= size;
822
+ }
823
+ return layout;
824
+ }
825
+
793
826
  function convertPercentageToPixels(percentage, groupSizePixels) {
794
827
  return percentage / 100 * groupSizePixels;
795
828
  }
@@ -940,6 +973,10 @@ function debounce(callback, durationMs = 10) {
940
973
  return callable;
941
974
  }
942
975
 
976
+ function getPanelElementsForGroup(groupId) {
977
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
978
+ }
979
+
943
980
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
944
981
  // or on a browser with cookies/storage disabled.
945
982
  // In either case, this function avoids accessing localStorage until needed,
@@ -995,6 +1032,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
995
1032
  } catch (error) {}
996
1033
  return null;
997
1034
  }
1035
+ function loadPanelLayout(autoSaveId, panels, storage) {
1036
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
1037
+ if (state) {
1038
+ var _state$key;
1039
+ const key = getSerializationKey(panels);
1040
+ return (_state$key = state[key]) !== null && _state$key !== void 0 ? _state$key : null;
1041
+ }
1042
+ return null;
1043
+ }
998
1044
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
999
1045
  const key = getSerializationKey(panels);
1000
1046
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -1006,6 +1052,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1006
1052
  }
1007
1053
  }
1008
1054
 
1055
+ function shouldMonitorPixelBasedConstraints(constraints) {
1056
+ return constraints.some(constraints => {
1057
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1058
+ });
1059
+ }
1060
+
1009
1061
  function validatePanelConstraints({
1010
1062
  groupSizePixels,
1011
1063
  panelConstraints,
@@ -1151,7 +1203,7 @@ const defaultStorage = {
1151
1203
  };
1152
1204
  const debounceMap = {};
1153
1205
  function PanelGroupWithForwardedRef({
1154
- autoSaveId,
1206
+ autoSaveId = null,
1155
1207
  children,
1156
1208
  className: classNameFromProps = "",
1157
1209
  dataAttributes,
@@ -1168,11 +1220,11 @@ function PanelGroupWithForwardedRef({
1168
1220
  const groupId = useUniqueId(idFromProps);
1169
1221
  const [dragState, setDragState] = useState(null);
1170
1222
  const [layout, setLayout] = useState([]);
1171
- const [panelDataArray, setPanelDataArray] = useState([]);
1172
1223
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1173
1224
  const panelSizeBeforeCollapseRef = useRef(new Map());
1174
1225
  const prevDeltaRef = useRef(0);
1175
1226
  const committedValuesRef = useRef({
1227
+ autoSaveId,
1176
1228
  direction,
1177
1229
  dragState,
1178
1230
  id: groupId,
@@ -1180,7 +1232,8 @@ function PanelGroupWithForwardedRef({
1180
1232
  keyboardResizeByPixels,
1181
1233
  layout,
1182
1234
  onLayout,
1183
- panelDataArray
1235
+ panelDataArray: [],
1236
+ storage
1184
1237
  });
1185
1238
  const devWarningsRef = useRef({
1186
1239
  didLogIdAndOrderWarning: false,
@@ -1218,6 +1271,7 @@ function PanelGroupWithForwardedRef({
1218
1271
  });
1219
1272
  if (!areEqual(prevLayout, safeLayout)) {
1220
1273
  setLayout(safeLayout);
1274
+ committedValuesRef.current.layout = safeLayout;
1221
1275
  if (onLayout) {
1222
1276
  onLayout(safeLayout.map(sizePercentage => ({
1223
1277
  sizePercentage,
@@ -1228,14 +1282,19 @@ function PanelGroupWithForwardedRef({
1228
1282
  }
1229
1283
  }
1230
1284
  }), []);
1285
+
1231
1286
  useWindowSplitterPanelGroupBehavior({
1232
1287
  committedValuesRef,
1233
1288
  groupId,
1234
1289
  layout,
1235
- panelDataArray,
1290
+ panelDataArray: committedValuesRef.current.panelDataArray,
1236
1291
  setLayout
1237
1292
  });
1238
1293
  useEffect(() => {
1294
+ const {
1295
+ panelDataArray
1296
+ } = committedValuesRef.current;
1297
+
1239
1298
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1240
1299
  if (autoSaveId) {
1241
1300
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1248,20 +1307,20 @@ function PanelGroupWithForwardedRef({
1248
1307
  }
1249
1308
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1250
1309
  }
1251
- }, [autoSaveId, layout, panelDataArray, storage]);
1310
+ }, [autoSaveId, layout, storage]);
1252
1311
 
1253
1312
  // DEV warnings
1254
1313
  useEffect(() => {
1255
1314
  {
1315
+ const {
1316
+ panelDataArray
1317
+ } = committedValuesRef.current;
1256
1318
  const {
1257
1319
  didLogIdAndOrderWarning,
1258
1320
  didLogPanelConstraintsWarning,
1259
1321
  prevPanelIds
1260
1322
  } = devWarningsRef.current;
1261
1323
  if (!didLogIdAndOrderWarning) {
1262
- const {
1263
- panelDataArray
1264
- } = committedValuesRef.current;
1265
1324
  const panelIds = panelDataArray.map(({
1266
1325
  id
1267
1326
  }) => id);
@@ -1327,6 +1386,7 @@ function PanelGroupWithForwardedRef({
1327
1386
  });
1328
1387
  if (!compareLayouts(prevLayout, nextLayout)) {
1329
1388
  setLayout(nextLayout);
1389
+ committedValuesRef.current.layout = nextLayout;
1330
1390
  if (onLayout) {
1331
1391
  onLayout(nextLayout.map(sizePercentage => ({
1332
1392
  sizePercentage,
@@ -1371,6 +1431,7 @@ function PanelGroupWithForwardedRef({
1371
1431
  });
1372
1432
  if (!compareLayouts(prevLayout, nextLayout)) {
1373
1433
  setLayout(nextLayout);
1434
+ committedValuesRef.current.layout = nextLayout;
1374
1435
  if (onLayout) {
1375
1436
  onLayout(nextLayout.map(sizePercentage => ({
1376
1437
  sizePercentage,
@@ -1401,6 +1462,9 @@ function PanelGroupWithForwardedRef({
1401
1462
 
1402
1463
  // This API should never read from committedValuesRef
1403
1464
  const getPanelStyle = useCallback(panelData => {
1465
+ const {
1466
+ panelDataArray
1467
+ } = committedValuesRef.current;
1404
1468
  const panelIndex = panelDataArray.indexOf(panelData);
1405
1469
  return computePanelFlexBoxStyle({
1406
1470
  dragState,
@@ -1408,7 +1472,7 @@ function PanelGroupWithForwardedRef({
1408
1472
  panelData: panelDataArray,
1409
1473
  panelIndex
1410
1474
  });
1411
- }, [dragState, layout, panelDataArray]);
1475
+ }, [dragState, layout]);
1412
1476
 
1413
1477
  // External APIs are safe to memoize via committed values ref
1414
1478
  const isPanelCollapsed = useCallback(panelData => {
@@ -1438,22 +1502,76 @@ function PanelGroupWithForwardedRef({
1438
1502
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1439
1503
  }, [groupId]);
1440
1504
  const registerPanel = useCallback(panelData => {
1441
- setPanelDataArray(prevPanelDataArray => {
1442
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1443
- return nextPanelDataArray.sort((panelA, panelB) => {
1444
- const orderA = panelA.order;
1445
- const orderB = panelB.order;
1446
- if (orderA == null && orderB == null) {
1447
- return 0;
1448
- } else if (orderA == null) {
1449
- return -1;
1450
- } else if (orderB == null) {
1451
- return 1;
1452
- } else {
1453
- return orderA - orderB;
1454
- }
1505
+ const {
1506
+ autoSaveId,
1507
+ id: groupId,
1508
+ layout: prevLayout,
1509
+ onLayout,
1510
+ panelDataArray,
1511
+ storage
1512
+ } = committedValuesRef.current;
1513
+ panelDataArray.push(panelData);
1514
+ panelDataArray.sort((panelA, panelB) => {
1515
+ const orderA = panelA.order;
1516
+ const orderB = panelB.order;
1517
+ if (orderA == null && orderB == null) {
1518
+ return 0;
1519
+ } else if (orderA == null) {
1520
+ return -1;
1521
+ } else if (orderB == null) {
1522
+ return 1;
1523
+ } else {
1524
+ return orderA - orderB;
1525
+ }
1526
+ });
1527
+
1528
+ // Wait until all panels have registered before we try to compute layout;
1529
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1530
+ const panelElements = getPanelElementsForGroup(groupId);
1531
+ if (panelElements.length !== panelDataArray.length) {
1532
+ return;
1533
+ }
1534
+
1535
+ // If this panel has been configured to persist sizing information,
1536
+ // default size should be restored from local storage if possible.
1537
+ let unsafeLayout = null;
1538
+ if (autoSaveId) {
1539
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1540
+ }
1541
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1542
+ if (groupSizePixels <= 0) {
1543
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1544
+ constraints
1545
+ }) => constraints))) {
1546
+ // Wait until the group has rendered a non-zero size before computing layout.
1547
+ return;
1548
+ }
1549
+ }
1550
+ if (unsafeLayout == null) {
1551
+ unsafeLayout = calculateUnsafeDefaultLayout({
1552
+ groupSizePixels,
1553
+ panelDataArray
1455
1554
  });
1555
+ }
1556
+
1557
+ // Validate even saved layouts in case something has changed since last render
1558
+ // e.g. for pixel groups, this could be the size of the window
1559
+ const nextLayout = validatePanelGroupLayout({
1560
+ groupSizePixels,
1561
+ layout: unsafeLayout,
1562
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1456
1563
  });
1564
+ if (!areEqual(prevLayout, nextLayout)) {
1565
+ setLayout(nextLayout);
1566
+ committedValuesRef.current.layout = nextLayout;
1567
+ if (onLayout) {
1568
+ onLayout(nextLayout.map(sizePercentage => ({
1569
+ sizePercentage,
1570
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1571
+ })));
1572
+ }
1573
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1574
+ }
1457
1575
  }, []);
1458
1576
  const registerResizeHandle = useCallback(dragHandleId => {
1459
1577
  return function resizeHandler(event) {
@@ -1523,6 +1641,7 @@ function PanelGroupWithForwardedRef({
1523
1641
  }
1524
1642
  if (layoutChanged) {
1525
1643
  setLayout(nextLayout);
1644
+ committedValuesRef.current.layout = nextLayout;
1526
1645
  if (onLayout) {
1527
1646
  onLayout(nextLayout.map(sizePercentage => ({
1528
1647
  sizePercentage,
@@ -1560,6 +1679,7 @@ function PanelGroupWithForwardedRef({
1560
1679
  });
1561
1680
  if (!compareLayouts(prevLayout, nextLayout)) {
1562
1681
  setLayout(nextLayout);
1682
+ committedValuesRef.current.layout = nextLayout;
1563
1683
  if (onLayout) {
1564
1684
  onLayout(nextLayout.map(sizePercentage => ({
1565
1685
  sizePercentage,
@@ -1587,16 +1707,84 @@ function PanelGroupWithForwardedRef({
1587
1707
  resetGlobalCursorStyle();
1588
1708
  setDragState(null);
1589
1709
  }, []);
1710
+ const unregisterPanelRef = useRef({
1711
+ pendingPanelIds: new Set(),
1712
+ timeout: null
1713
+ });
1590
1714
  const unregisterPanel = useCallback(panelData => {
1591
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1592
- setPanelDataArray(panelDataArray => {
1593
- const index = panelDataArray.indexOf(panelData);
1594
- if (index >= 0) {
1595
- panelDataArray = [...panelDataArray];
1596
- panelDataArray.splice(index, 1);
1715
+ const {
1716
+ id: groupId,
1717
+ layout: prevLayout,
1718
+ onLayout,
1719
+ panelDataArray
1720
+ } = committedValuesRef.current;
1721
+ const index = panelDataArray.indexOf(panelData);
1722
+ if (index >= 0) {
1723
+ panelDataArray.splice(index, 1);
1724
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1725
+ }
1726
+ if (unregisterPanelRef.current.timeout != null) {
1727
+ clearTimeout(unregisterPanelRef.current.timeout);
1728
+ }
1729
+
1730
+ // Batch panel unmounts so that we only calculate layout once;
1731
+ // This is more efficient and avoids misleading warnings in development mode.
1732
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1733
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1734
+ const {
1735
+ pendingPanelIds
1736
+ } = unregisterPanelRef.current;
1737
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1738
+
1739
+ // TRICKY
1740
+ // Strict effects mode
1741
+ let unmountDueToStrictMode = false;
1742
+ pendingPanelIds.forEach(panelId => {
1743
+ pendingPanelIds.delete(panelId);
1744
+ if (panelDataArray.find(({
1745
+ id
1746
+ }) => id === panelId) == null) {
1747
+ unmountDueToStrictMode = true;
1748
+
1749
+ // TRICKY
1750
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1751
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1752
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1753
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1754
+ }
1755
+ });
1756
+ if (!unmountDueToStrictMode) {
1757
+ return;
1597
1758
  }
1598
- return panelDataArray;
1599
- });
1759
+ if (panelDataArray.length === 0) {
1760
+ // The group is unmounting; skip layout calculation.
1761
+ return;
1762
+ }
1763
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1764
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1765
+ groupSizePixels,
1766
+ panelDataArray
1767
+ });
1768
+
1769
+ // Validate even saved layouts in case something has changed since last render
1770
+ // e.g. for pixel groups, this could be the size of the window
1771
+ const nextLayout = validatePanelGroupLayout({
1772
+ groupSizePixels,
1773
+ layout: unsafeLayout,
1774
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1775
+ });
1776
+ if (!areEqual(prevLayout, nextLayout)) {
1777
+ setLayout(nextLayout);
1778
+ committedValuesRef.current.layout = nextLayout;
1779
+ if (onLayout) {
1780
+ onLayout(nextLayout.map(sizePercentage => ({
1781
+ sizePercentage,
1782
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1783
+ })));
1784
+ }
1785
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1786
+ }
1787
+ }, 0);
1600
1788
  }, []);
1601
1789
  const context = useMemo(() => ({
1602
1790
  collapsePanel,