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.
@@ -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
@@ -772,6 +774,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
772
774
  }
773
775
  }
774
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
+
775
815
  function convertPercentageToPixels(percentage, groupSizePixels) {
776
816
  return percentage / 100 * groupSizePixels;
777
817
  }
@@ -922,6 +962,10 @@ function debounce(callback, durationMs = 10) {
922
962
  return callable;
923
963
  }
924
964
 
965
+ function getPanelElementsForGroup(groupId) {
966
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
967
+ }
968
+
925
969
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
926
970
  // or on a browser with cookies/storage disabled.
927
971
  // In either case, this function avoids accessing localStorage until needed,
@@ -977,6 +1021,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
977
1021
  } catch (error) {}
978
1022
  return null;
979
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
+ }
980
1033
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
981
1034
  const key = getSerializationKey(panels);
982
1035
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -988,6 +1041,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
988
1041
  }
989
1042
  }
990
1043
 
1044
+ function shouldMonitorPixelBasedConstraints(constraints) {
1045
+ return constraints.some(constraints => {
1046
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1047
+ });
1048
+ }
1049
+
991
1050
  // All units must be in percentages; pixel values should be pre-converted
992
1051
  function validatePanelGroupLayout({
993
1052
  groupSizePixels,
@@ -1056,7 +1115,7 @@ const defaultStorage = {
1056
1115
  };
1057
1116
  const debounceMap = {};
1058
1117
  function PanelGroupWithForwardedRef({
1059
- autoSaveId,
1118
+ autoSaveId = null,
1060
1119
  children,
1061
1120
  className: classNameFromProps = "",
1062
1121
  dataAttributes,
@@ -1073,12 +1132,11 @@ function PanelGroupWithForwardedRef({
1073
1132
  const groupId = useUniqueId(idFromProps);
1074
1133
  const [dragState, setDragState] = useState(null);
1075
1134
  const [layout, setLayout] = useState([]);
1076
- const [panelDataArray, setPanelDataArray] = useState([]);
1077
1135
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1078
1136
  const panelSizeBeforeCollapseRef = useRef(new Map());
1079
1137
  const prevDeltaRef = useRef(0);
1080
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1081
1138
  const committedValuesRef = useRef({
1139
+ autoSaveId,
1082
1140
  direction,
1083
1141
  dragState,
1084
1142
  id: groupId,
@@ -1086,7 +1144,8 @@ function PanelGroupWithForwardedRef({
1086
1144
  keyboardResizeByPixels,
1087
1145
  layout,
1088
1146
  onLayout,
1089
- panelDataArray
1147
+ panelDataArray: [],
1148
+ storage
1090
1149
  });
1091
1150
  useRef({
1092
1151
  didLogIdAndOrderWarning: false,
@@ -1124,6 +1183,7 @@ function PanelGroupWithForwardedRef({
1124
1183
  });
1125
1184
  if (!areEqual(prevLayout, safeLayout)) {
1126
1185
  setLayout(safeLayout);
1186
+ committedValuesRef.current.layout = safeLayout;
1127
1187
  if (onLayout) {
1128
1188
  onLayout(safeLayout.map(sizePercentage => ({
1129
1189
  sizePercentage,
@@ -1134,14 +1194,19 @@ function PanelGroupWithForwardedRef({
1134
1194
  }
1135
1195
  }
1136
1196
  }), []);
1197
+
1137
1198
  useWindowSplitterPanelGroupBehavior({
1138
1199
  committedValuesRef,
1139
1200
  groupId,
1140
1201
  layout,
1141
- panelDataArray,
1202
+ panelDataArray: committedValuesRef.current.panelDataArray,
1142
1203
  setLayout
1143
1204
  });
1144
1205
  useEffect(() => {
1206
+ const {
1207
+ panelDataArray
1208
+ } = committedValuesRef.current;
1209
+
1145
1210
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1146
1211
  if (autoSaveId) {
1147
1212
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1154,7 +1219,7 @@ function PanelGroupWithForwardedRef({
1154
1219
  }
1155
1220
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1156
1221
  }
1157
- }, [autoSaveId, layout, panelDataArray, storage]);
1222
+ }, [autoSaveId, layout, storage]);
1158
1223
 
1159
1224
  // DEV warnings
1160
1225
  useEffect(() => {
@@ -1167,17 +1232,6 @@ function PanelGroupWithForwardedRef({
1167
1232
  onLayout,
1168
1233
  panelDataArray
1169
1234
  } = committedValuesRef.current;
1170
-
1171
- // See issues/211
1172
- if (panelDataArray.find(({
1173
- id
1174
- }) => id === panelData.id) == null) {
1175
- setImperativeApiQueue(prev => [...prev, {
1176
- panelData,
1177
- type: "collapse"
1178
- }]);
1179
- return;
1180
- }
1181
1235
  if (panelData.constraints.collapsible) {
1182
1236
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1183
1237
  const {
@@ -1202,6 +1256,7 @@ function PanelGroupWithForwardedRef({
1202
1256
  });
1203
1257
  if (!compareLayouts(prevLayout, nextLayout)) {
1204
1258
  setLayout(nextLayout);
1259
+ committedValuesRef.current.layout = nextLayout;
1205
1260
  if (onLayout) {
1206
1261
  onLayout(nextLayout.map(sizePercentage => ({
1207
1262
  sizePercentage,
@@ -1221,17 +1276,6 @@ function PanelGroupWithForwardedRef({
1221
1276
  onLayout,
1222
1277
  panelDataArray
1223
1278
  } = committedValuesRef.current;
1224
-
1225
- // See issues/211
1226
- if (panelDataArray.find(({
1227
- id
1228
- }) => id === panelData.id) == null) {
1229
- setImperativeApiQueue(prev => [...prev, {
1230
- panelData,
1231
- type: "expand"
1232
- }]);
1233
- return;
1234
- }
1235
1279
  if (panelData.constraints.collapsible) {
1236
1280
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1237
1281
  const {
@@ -1257,6 +1301,7 @@ function PanelGroupWithForwardedRef({
1257
1301
  });
1258
1302
  if (!compareLayouts(prevLayout, nextLayout)) {
1259
1303
  setLayout(nextLayout);
1304
+ committedValuesRef.current.layout = nextLayout;
1260
1305
  if (onLayout) {
1261
1306
  onLayout(nextLayout.map(sizePercentage => ({
1262
1307
  sizePercentage,
@@ -1287,6 +1332,9 @@ function PanelGroupWithForwardedRef({
1287
1332
 
1288
1333
  // This API should never read from committedValuesRef
1289
1334
  const getPanelStyle = useCallback(panelData => {
1335
+ const {
1336
+ panelDataArray
1337
+ } = committedValuesRef.current;
1290
1338
  const panelIndex = panelDataArray.indexOf(panelData);
1291
1339
  return computePanelFlexBoxStyle({
1292
1340
  dragState,
@@ -1294,7 +1342,7 @@ function PanelGroupWithForwardedRef({
1294
1342
  panelData: panelDataArray,
1295
1343
  panelIndex
1296
1344
  });
1297
- }, [dragState, layout, panelDataArray]);
1345
+ }, [dragState, layout]);
1298
1346
 
1299
1347
  // External APIs are safe to memoize via committed values ref
1300
1348
  const isPanelCollapsed = useCallback(panelData => {
@@ -1324,22 +1372,76 @@ function PanelGroupWithForwardedRef({
1324
1372
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1325
1373
  }, [groupId]);
1326
1374
  const registerPanel = useCallback(panelData => {
1327
- setPanelDataArray(prevPanelDataArray => {
1328
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1329
- return nextPanelDataArray.sort((panelA, panelB) => {
1330
- const orderA = panelA.order;
1331
- const orderB = panelB.order;
1332
- if (orderA == null && orderB == null) {
1333
- return 0;
1334
- } else if (orderA == null) {
1335
- return -1;
1336
- } else if (orderB == null) {
1337
- return 1;
1338
- } else {
1339
- return orderA - orderB;
1340
- }
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
1341
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)
1342
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
+ }
1343
1445
  }, []);
1344
1446
  const registerResizeHandle = useCallback(dragHandleId => {
1345
1447
  return function resizeHandler(event) {
@@ -1409,6 +1511,7 @@ function PanelGroupWithForwardedRef({
1409
1511
  }
1410
1512
  if (layoutChanged) {
1411
1513
  setLayout(nextLayout);
1514
+ committedValuesRef.current.layout = nextLayout;
1412
1515
  if (onLayout) {
1413
1516
  onLayout(nextLayout.map(sizePercentage => ({
1414
1517
  sizePercentage,
@@ -1427,18 +1530,6 @@ function PanelGroupWithForwardedRef({
1427
1530
  onLayout,
1428
1531
  panelDataArray
1429
1532
  } = committedValuesRef.current;
1430
-
1431
- // See issues/211
1432
- if (panelDataArray.find(({
1433
- id
1434
- }) => id === panelData.id) == null) {
1435
- setImperativeApiQueue(prev => [...prev, {
1436
- panelData,
1437
- mixedSizes,
1438
- type: "resize"
1439
- }]);
1440
- return;
1441
- }
1442
1533
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1443
1534
  const {
1444
1535
  groupSizePixels,
@@ -1458,6 +1549,7 @@ function PanelGroupWithForwardedRef({
1458
1549
  });
1459
1550
  if (!compareLayouts(prevLayout, nextLayout)) {
1460
1551
  setLayout(nextLayout);
1552
+ committedValuesRef.current.layout = nextLayout;
1461
1553
  if (onLayout) {
1462
1554
  onLayout(nextLayout.map(sizePercentage => ({
1463
1555
  sizePercentage,
@@ -1485,16 +1577,84 @@ function PanelGroupWithForwardedRef({
1485
1577
  resetGlobalCursorStyle();
1486
1578
  setDragState(null);
1487
1579
  }, []);
1580
+ const unregisterPanelRef = useRef({
1581
+ pendingPanelIds: new Set(),
1582
+ timeout: null
1583
+ });
1488
1584
  const unregisterPanel = useCallback(panelData => {
1489
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1490
- setPanelDataArray(panelDataArray => {
1491
- const index = panelDataArray.indexOf(panelData);
1492
- if (index >= 0) {
1493
- panelDataArray = [...panelDataArray];
1494
- 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;
1495
1628
  }
1496
- return panelDataArray;
1497
- });
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);
1498
1658
  }, []);
1499
1659
  const context = useMemo(() => ({
1500
1660
  collapsePanel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-resizable-panels",
3
- "version": "0.0.59",
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