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.
@@ -91,6 +91,7 @@ function PanelWithForwardedRef({
91
91
  expandPanel,
92
92
  getPanelSize,
93
93
  getPanelStyle,
94
+ groupId,
94
95
  isPanelCollapsed,
95
96
  registerPanel,
96
97
  resizePanel,
@@ -167,6 +168,7 @@ function PanelWithForwardedRef({
167
168
  // CSS selectors
168
169
  "data-panel": "",
169
170
  "data-panel-id": panelId,
171
+ "data-panel-group-id": groupId,
170
172
  // e2e test attributes
171
173
  "data-panel-collapsible": collapsible || undefined ,
172
174
  "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
@@ -807,6 +809,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
807
809
  }
808
810
  }
809
811
 
812
+ function calculateUnsafeDefaultLayout({
813
+ groupSizePixels,
814
+ panelDataArray
815
+ }) {
816
+ const layout = Array(panelDataArray.length);
817
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
818
+ let numPanelsWithSizes = 0;
819
+ let remainingSize = 100;
820
+
821
+ // Distribute default sizes first
822
+ for (let index = 0; index < panelDataArray.length; index++) {
823
+ const {
824
+ defaultSizePercentage
825
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
826
+ if (defaultSizePercentage != null) {
827
+ numPanelsWithSizes++;
828
+ layout[index] = defaultSizePercentage;
829
+ remainingSize -= defaultSizePercentage;
830
+ }
831
+ }
832
+
833
+ // Remaining size should be distributed evenly between panels without default sizes
834
+ for (let index = 0; index < panelDataArray.length; index++) {
835
+ const {
836
+ defaultSizePercentage
837
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
838
+ if (defaultSizePercentage != null) {
839
+ continue;
840
+ }
841
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
842
+ const size = remainingSize / numRemainingPanels;
843
+ numPanelsWithSizes++;
844
+ layout[index] = size;
845
+ remainingSize -= size;
846
+ }
847
+ return layout;
848
+ }
849
+
810
850
  function convertPercentageToPixels(percentage, groupSizePixels) {
811
851
  return percentage / 100 * groupSizePixels;
812
852
  }
@@ -957,6 +997,10 @@ function debounce(callback, durationMs = 10) {
957
997
  return callable;
958
998
  }
959
999
 
1000
+ function getPanelElementsForGroup(groupId) {
1001
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1002
+ }
1003
+
960
1004
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
961
1005
  // or on a browser with cookies/storage disabled.
962
1006
  // In either case, this function avoids accessing localStorage until needed,
@@ -1012,6 +1056,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
1012
1056
  } catch (error) {}
1013
1057
  return null;
1014
1058
  }
1059
+ function loadPanelLayout(autoSaveId, panels, storage) {
1060
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
1061
+ if (state) {
1062
+ var _state$key;
1063
+ const key = getSerializationKey(panels);
1064
+ return (_state$key = state[key]) !== null && _state$key !== void 0 ? _state$key : null;
1065
+ }
1066
+ return null;
1067
+ }
1015
1068
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1016
1069
  const key = getSerializationKey(panels);
1017
1070
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -1023,6 +1076,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1023
1076
  }
1024
1077
  }
1025
1078
 
1079
+ function shouldMonitorPixelBasedConstraints(constraints) {
1080
+ return constraints.some(constraints => {
1081
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1082
+ });
1083
+ }
1084
+
1026
1085
  function validatePanelConstraints({
1027
1086
  groupSizePixels,
1028
1087
  panelConstraints,
@@ -1168,7 +1227,7 @@ const defaultStorage = {
1168
1227
  };
1169
1228
  const debounceMap = {};
1170
1229
  function PanelGroupWithForwardedRef({
1171
- autoSaveId,
1230
+ autoSaveId = null,
1172
1231
  children,
1173
1232
  className: classNameFromProps = "",
1174
1233
  dataAttributes,
@@ -1185,12 +1244,11 @@ function PanelGroupWithForwardedRef({
1185
1244
  const groupId = useUniqueId(idFromProps);
1186
1245
  const [dragState, setDragState] = useState(null);
1187
1246
  const [layout, setLayout] = useState([]);
1188
- const [panelDataArray, setPanelDataArray] = useState([]);
1189
1247
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1190
1248
  const panelSizeBeforeCollapseRef = useRef(new Map());
1191
1249
  const prevDeltaRef = useRef(0);
1192
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1193
1250
  const committedValuesRef = useRef({
1251
+ autoSaveId,
1194
1252
  direction,
1195
1253
  dragState,
1196
1254
  id: groupId,
@@ -1198,7 +1256,8 @@ function PanelGroupWithForwardedRef({
1198
1256
  keyboardResizeByPixels,
1199
1257
  layout,
1200
1258
  onLayout,
1201
- panelDataArray
1259
+ panelDataArray: [],
1260
+ storage
1202
1261
  });
1203
1262
  const devWarningsRef = useRef({
1204
1263
  didLogIdAndOrderWarning: false,
@@ -1236,6 +1295,7 @@ function PanelGroupWithForwardedRef({
1236
1295
  });
1237
1296
  if (!areEqual(prevLayout, safeLayout)) {
1238
1297
  setLayout(safeLayout);
1298
+ committedValuesRef.current.layout = safeLayout;
1239
1299
  if (onLayout) {
1240
1300
  onLayout(safeLayout.map(sizePercentage => ({
1241
1301
  sizePercentage,
@@ -1246,14 +1306,19 @@ function PanelGroupWithForwardedRef({
1246
1306
  }
1247
1307
  }
1248
1308
  }), []);
1309
+
1249
1310
  useWindowSplitterPanelGroupBehavior({
1250
1311
  committedValuesRef,
1251
1312
  groupId,
1252
1313
  layout,
1253
- panelDataArray,
1314
+ panelDataArray: committedValuesRef.current.panelDataArray,
1254
1315
  setLayout
1255
1316
  });
1256
1317
  useEffect(() => {
1318
+ const {
1319
+ panelDataArray
1320
+ } = committedValuesRef.current;
1321
+
1257
1322
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1258
1323
  if (autoSaveId) {
1259
1324
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1266,20 +1331,20 @@ function PanelGroupWithForwardedRef({
1266
1331
  }
1267
1332
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1268
1333
  }
1269
- }, [autoSaveId, layout, panelDataArray, storage]);
1334
+ }, [autoSaveId, layout, storage]);
1270
1335
 
1271
1336
  // DEV warnings
1272
1337
  useEffect(() => {
1273
1338
  {
1339
+ const {
1340
+ panelDataArray
1341
+ } = committedValuesRef.current;
1274
1342
  const {
1275
1343
  didLogIdAndOrderWarning,
1276
1344
  didLogPanelConstraintsWarning,
1277
1345
  prevPanelIds
1278
1346
  } = devWarningsRef.current;
1279
1347
  if (!didLogIdAndOrderWarning) {
1280
- const {
1281
- panelDataArray
1282
- } = committedValuesRef.current;
1283
1348
  const panelIds = panelDataArray.map(({
1284
1349
  id
1285
1350
  }) => id);
@@ -1321,17 +1386,6 @@ function PanelGroupWithForwardedRef({
1321
1386
  onLayout,
1322
1387
  panelDataArray
1323
1388
  } = committedValuesRef.current;
1324
-
1325
- // See issues/211
1326
- if (panelDataArray.find(({
1327
- id
1328
- }) => id === panelData.id) == null) {
1329
- setImperativeApiQueue(prev => [...prev, {
1330
- panelData,
1331
- type: "collapse"
1332
- }]);
1333
- return;
1334
- }
1335
1389
  if (panelData.constraints.collapsible) {
1336
1390
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1337
1391
  const {
@@ -1356,6 +1410,7 @@ function PanelGroupWithForwardedRef({
1356
1410
  });
1357
1411
  if (!compareLayouts(prevLayout, nextLayout)) {
1358
1412
  setLayout(nextLayout);
1413
+ committedValuesRef.current.layout = nextLayout;
1359
1414
  if (onLayout) {
1360
1415
  onLayout(nextLayout.map(sizePercentage => ({
1361
1416
  sizePercentage,
@@ -1375,17 +1430,6 @@ function PanelGroupWithForwardedRef({
1375
1430
  onLayout,
1376
1431
  panelDataArray
1377
1432
  } = committedValuesRef.current;
1378
-
1379
- // See issues/211
1380
- if (panelDataArray.find(({
1381
- id
1382
- }) => id === panelData.id) == null) {
1383
- setImperativeApiQueue(prev => [...prev, {
1384
- panelData,
1385
- type: "expand"
1386
- }]);
1387
- return;
1388
- }
1389
1433
  if (panelData.constraints.collapsible) {
1390
1434
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1391
1435
  const {
@@ -1411,6 +1455,7 @@ function PanelGroupWithForwardedRef({
1411
1455
  });
1412
1456
  if (!compareLayouts(prevLayout, nextLayout)) {
1413
1457
  setLayout(nextLayout);
1458
+ committedValuesRef.current.layout = nextLayout;
1414
1459
  if (onLayout) {
1415
1460
  onLayout(nextLayout.map(sizePercentage => ({
1416
1461
  sizePercentage,
@@ -1441,6 +1486,9 @@ function PanelGroupWithForwardedRef({
1441
1486
 
1442
1487
  // This API should never read from committedValuesRef
1443
1488
  const getPanelStyle = useCallback(panelData => {
1489
+ const {
1490
+ panelDataArray
1491
+ } = committedValuesRef.current;
1444
1492
  const panelIndex = panelDataArray.indexOf(panelData);
1445
1493
  return computePanelFlexBoxStyle({
1446
1494
  dragState,
@@ -1448,7 +1496,7 @@ function PanelGroupWithForwardedRef({
1448
1496
  panelData: panelDataArray,
1449
1497
  panelIndex
1450
1498
  });
1451
- }, [dragState, layout, panelDataArray]);
1499
+ }, [dragState, layout]);
1452
1500
 
1453
1501
  // External APIs are safe to memoize via committed values ref
1454
1502
  const isPanelCollapsed = useCallback(panelData => {
@@ -1478,22 +1526,76 @@ function PanelGroupWithForwardedRef({
1478
1526
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1479
1527
  }, [groupId]);
1480
1528
  const registerPanel = useCallback(panelData => {
1481
- setPanelDataArray(prevPanelDataArray => {
1482
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1483
- return nextPanelDataArray.sort((panelA, panelB) => {
1484
- const orderA = panelA.order;
1485
- const orderB = panelB.order;
1486
- if (orderA == null && orderB == null) {
1487
- return 0;
1488
- } else if (orderA == null) {
1489
- return -1;
1490
- } else if (orderB == null) {
1491
- return 1;
1492
- } else {
1493
- return orderA - orderB;
1494
- }
1529
+ const {
1530
+ autoSaveId,
1531
+ id: groupId,
1532
+ layout: prevLayout,
1533
+ onLayout,
1534
+ panelDataArray,
1535
+ storage
1536
+ } = committedValuesRef.current;
1537
+ panelDataArray.push(panelData);
1538
+ panelDataArray.sort((panelA, panelB) => {
1539
+ const orderA = panelA.order;
1540
+ const orderB = panelB.order;
1541
+ if (orderA == null && orderB == null) {
1542
+ return 0;
1543
+ } else if (orderA == null) {
1544
+ return -1;
1545
+ } else if (orderB == null) {
1546
+ return 1;
1547
+ } else {
1548
+ return orderA - orderB;
1549
+ }
1550
+ });
1551
+
1552
+ // Wait until all panels have registered before we try to compute layout;
1553
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1554
+ const panelElements = getPanelElementsForGroup(groupId);
1555
+ if (panelElements.length !== panelDataArray.length) {
1556
+ return;
1557
+ }
1558
+
1559
+ // If this panel has been configured to persist sizing information,
1560
+ // default size should be restored from local storage if possible.
1561
+ let unsafeLayout = null;
1562
+ if (autoSaveId) {
1563
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1564
+ }
1565
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1566
+ if (groupSizePixels <= 0) {
1567
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1568
+ constraints
1569
+ }) => constraints))) {
1570
+ // Wait until the group has rendered a non-zero size before computing layout.
1571
+ return;
1572
+ }
1573
+ }
1574
+ if (unsafeLayout == null) {
1575
+ unsafeLayout = calculateUnsafeDefaultLayout({
1576
+ groupSizePixels,
1577
+ panelDataArray
1495
1578
  });
1579
+ }
1580
+
1581
+ // Validate even saved layouts in case something has changed since last render
1582
+ // e.g. for pixel groups, this could be the size of the window
1583
+ const nextLayout = validatePanelGroupLayout({
1584
+ groupSizePixels,
1585
+ layout: unsafeLayout,
1586
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1496
1587
  });
1588
+ if (!areEqual(prevLayout, nextLayout)) {
1589
+ setLayout(nextLayout);
1590
+ committedValuesRef.current.layout = nextLayout;
1591
+ if (onLayout) {
1592
+ onLayout(nextLayout.map(sizePercentage => ({
1593
+ sizePercentage,
1594
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1595
+ })));
1596
+ }
1597
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1598
+ }
1497
1599
  }, []);
1498
1600
  const registerResizeHandle = useCallback(dragHandleId => {
1499
1601
  return function resizeHandler(event) {
@@ -1563,6 +1665,7 @@ function PanelGroupWithForwardedRef({
1563
1665
  }
1564
1666
  if (layoutChanged) {
1565
1667
  setLayout(nextLayout);
1668
+ committedValuesRef.current.layout = nextLayout;
1566
1669
  if (onLayout) {
1567
1670
  onLayout(nextLayout.map(sizePercentage => ({
1568
1671
  sizePercentage,
@@ -1581,18 +1684,6 @@ function PanelGroupWithForwardedRef({
1581
1684
  onLayout,
1582
1685
  panelDataArray
1583
1686
  } = committedValuesRef.current;
1584
-
1585
- // See issues/211
1586
- if (panelDataArray.find(({
1587
- id
1588
- }) => id === panelData.id) == null) {
1589
- setImperativeApiQueue(prev => [...prev, {
1590
- panelData,
1591
- mixedSizes,
1592
- type: "resize"
1593
- }]);
1594
- return;
1595
- }
1596
1687
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1597
1688
  const {
1598
1689
  groupSizePixels,
@@ -1612,6 +1703,7 @@ function PanelGroupWithForwardedRef({
1612
1703
  });
1613
1704
  if (!compareLayouts(prevLayout, nextLayout)) {
1614
1705
  setLayout(nextLayout);
1706
+ committedValuesRef.current.layout = nextLayout;
1615
1707
  if (onLayout) {
1616
1708
  onLayout(nextLayout.map(sizePercentage => ({
1617
1709
  sizePercentage,
@@ -1639,16 +1731,84 @@ function PanelGroupWithForwardedRef({
1639
1731
  resetGlobalCursorStyle();
1640
1732
  setDragState(null);
1641
1733
  }, []);
1734
+ const unregisterPanelRef = useRef({
1735
+ pendingPanelIds: new Set(),
1736
+ timeout: null
1737
+ });
1642
1738
  const unregisterPanel = useCallback(panelData => {
1643
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1644
- setPanelDataArray(panelDataArray => {
1645
- const index = panelDataArray.indexOf(panelData);
1646
- if (index >= 0) {
1647
- panelDataArray = [...panelDataArray];
1648
- panelDataArray.splice(index, 1);
1739
+ const {
1740
+ id: groupId,
1741
+ layout: prevLayout,
1742
+ onLayout,
1743
+ panelDataArray
1744
+ } = committedValuesRef.current;
1745
+ const index = panelDataArray.indexOf(panelData);
1746
+ if (index >= 0) {
1747
+ panelDataArray.splice(index, 1);
1748
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1749
+ }
1750
+ if (unregisterPanelRef.current.timeout != null) {
1751
+ clearTimeout(unregisterPanelRef.current.timeout);
1752
+ }
1753
+
1754
+ // Batch panel unmounts so that we only calculate layout once;
1755
+ // This is more efficient and avoids misleading warnings in development mode.
1756
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1757
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1758
+ const {
1759
+ pendingPanelIds
1760
+ } = unregisterPanelRef.current;
1761
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1762
+
1763
+ // TRICKY
1764
+ // Strict effects mode
1765
+ let unmountDueToStrictMode = false;
1766
+ pendingPanelIds.forEach(panelId => {
1767
+ pendingPanelIds.delete(panelId);
1768
+ if (panelDataArray.find(({
1769
+ id
1770
+ }) => id === panelId) == null) {
1771
+ unmountDueToStrictMode = true;
1772
+
1773
+ // TRICKY
1774
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1775
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1776
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1777
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1778
+ }
1779
+ });
1780
+ if (!unmountDueToStrictMode) {
1781
+ return;
1649
1782
  }
1650
- return panelDataArray;
1651
- });
1783
+ if (panelDataArray.length === 0) {
1784
+ // The group is unmounting; skip layout calculation.
1785
+ return;
1786
+ }
1787
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1788
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1789
+ groupSizePixels,
1790
+ panelDataArray
1791
+ });
1792
+
1793
+ // Validate even saved layouts in case something has changed since last render
1794
+ // e.g. for pixel groups, this could be the size of the window
1795
+ const nextLayout = validatePanelGroupLayout({
1796
+ groupSizePixels,
1797
+ layout: unsafeLayout,
1798
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1799
+ });
1800
+ if (!areEqual(prevLayout, nextLayout)) {
1801
+ setLayout(nextLayout);
1802
+ committedValuesRef.current.layout = nextLayout;
1803
+ if (onLayout) {
1804
+ onLayout(nextLayout.map(sizePercentage => ({
1805
+ sizePercentage,
1806
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1807
+ })));
1808
+ }
1809
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1810
+ }
1811
+ }, 0);
1652
1812
  }, []);
1653
1813
  const context = useMemo(() => ({
1654
1814
  collapsePanel,