react-resizable-panels 0.0.55 → 0.0.56

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.
Files changed (93) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/CHANGELOG.md +234 -90
  3. package/README.md +55 -49
  4. package/dist/declarations/src/Panel.d.ts +75 -20
  5. package/dist/declarations/src/PanelGroup.d.ts +29 -25
  6. package/dist/declarations/src/PanelResizeHandle.d.ts +1 -1
  7. package/dist/declarations/src/index.d.ts +5 -6
  8. package/dist/declarations/src/types.d.ts +3 -26
  9. package/dist/declarations/src/vendor/react.d.ts +4 -4
  10. package/dist/react-resizable-panels.browser.cjs.js +1241 -1035
  11. package/dist/react-resizable-panels.browser.cjs.mjs +1 -2
  12. package/dist/react-resizable-panels.browser.development.cjs.js +1367 -1081
  13. package/dist/react-resizable-panels.browser.development.cjs.mjs +1 -2
  14. package/dist/react-resizable-panels.browser.development.esm.js +1368 -1081
  15. package/dist/react-resizable-panels.browser.esm.js +1242 -1035
  16. package/dist/react-resizable-panels.cjs.js +1241 -1035
  17. package/dist/react-resizable-panels.cjs.js.map +1 -1
  18. package/dist/react-resizable-panels.cjs.mjs +1 -2
  19. package/dist/react-resizable-panels.development.cjs.js +1370 -1084
  20. package/dist/react-resizable-panels.development.cjs.mjs +1 -2
  21. package/dist/react-resizable-panels.development.esm.js +1371 -1084
  22. package/dist/react-resizable-panels.development.node.cjs.js +1151 -940
  23. package/dist/react-resizable-panels.development.node.cjs.mjs +1 -2
  24. package/dist/react-resizable-panels.development.node.esm.js +1152 -940
  25. package/dist/react-resizable-panels.esm.js +1242 -1035
  26. package/dist/react-resizable-panels.esm.js.map +1 -1
  27. package/dist/react-resizable-panels.node.cjs.js +1049 -912
  28. package/dist/react-resizable-panels.node.cjs.mjs +1 -2
  29. package/dist/react-resizable-panels.node.esm.js +1050 -912
  30. package/jest.config.js +10 -0
  31. package/package.json +3 -1
  32. package/src/Panel.test.tsx +308 -0
  33. package/src/Panel.ts +175 -123
  34. package/src/PanelGroup.test.tsx +210 -0
  35. package/src/PanelGroup.ts +730 -669
  36. package/src/PanelGroupContext.ts +33 -0
  37. package/src/PanelResizeHandle.ts +13 -8
  38. package/src/hooks/useUniqueId.ts +1 -1
  39. package/src/hooks/useWindowSplitterBehavior.ts +9 -164
  40. package/src/hooks/useWindowSplitterPanelGroupBehavior.ts +185 -0
  41. package/src/index.ts +19 -14
  42. package/src/types.ts +3 -30
  43. package/src/utils/adjustLayoutByDelta.test.ts +1808 -0
  44. package/src/utils/adjustLayoutByDelta.ts +211 -0
  45. package/src/utils/calculateAriaValues.test.ts +111 -0
  46. package/src/utils/calculateAriaValues.ts +67 -0
  47. package/src/utils/calculateDeltaPercentage.ts +68 -0
  48. package/src/utils/calculateDragOffsetPercentage.ts +30 -0
  49. package/src/utils/calculateUnsafeDefaultLayout.test.ts +92 -0
  50. package/src/utils/calculateUnsafeDefaultLayout.ts +55 -0
  51. package/src/utils/callPanelCallbacks.ts +81 -0
  52. package/src/utils/compareLayouts.test.ts +9 -0
  53. package/src/utils/compareLayouts.ts +12 -0
  54. package/src/utils/computePanelFlexBoxStyle.ts +44 -0
  55. package/src/utils/computePercentagePanelConstraints.test.ts +71 -0
  56. package/src/utils/computePercentagePanelConstraints.ts +56 -0
  57. package/src/utils/convertPercentageToPixels.test.ts +9 -0
  58. package/src/utils/convertPercentageToPixels.ts +6 -0
  59. package/src/utils/convertPixelConstraintsToPercentages.ts +55 -0
  60. package/src/utils/convertPixelsToPercentage.test.ts +9 -0
  61. package/src/utils/convertPixelsToPercentage.ts +6 -0
  62. package/src/utils/determinePivotIndices.ts +10 -0
  63. package/src/utils/dom/calculateAvailablePanelSizeInPixels.ts +29 -0
  64. package/src/utils/dom/getAvailableGroupSizePixels.ts +29 -0
  65. package/src/utils/dom/getPanelElement.ts +7 -0
  66. package/src/utils/dom/getPanelGroupElement.ts +7 -0
  67. package/src/utils/dom/getResizeHandleElement.ts +9 -0
  68. package/src/utils/dom/getResizeHandleElementIndex.ts +12 -0
  69. package/src/utils/dom/getResizeHandleElementsForGroup.ts +9 -0
  70. package/src/utils/dom/getResizeHandlePanelIds.ts +18 -0
  71. package/src/utils/events.ts +13 -0
  72. package/src/utils/getPercentageSizeFromMixedSizes.test.ts +47 -0
  73. package/src/utils/getPercentageSizeFromMixedSizes.ts +15 -0
  74. package/src/utils/getResizeEventCursorPosition.ts +19 -0
  75. package/src/utils/initializeDefaultStorage.ts +26 -0
  76. package/src/utils/numbers/fuzzyCompareNumbers.test.ts +16 -0
  77. package/src/utils/numbers/fuzzyCompareNumbers.ts +17 -0
  78. package/src/utils/numbers/fuzzyNumbersEqual.ts +9 -0
  79. package/src/utils/resizePanel.ts +41 -0
  80. package/src/utils/serialization.ts +9 -4
  81. package/src/utils/shouldMonitorPixelBasedConstraints.test.ts +23 -0
  82. package/src/utils/shouldMonitorPixelBasedConstraints.ts +13 -0
  83. package/src/utils/test-utils.ts +136 -0
  84. package/src/utils/validatePanelConstraints.test.ts +151 -0
  85. package/src/utils/validatePanelConstraints.ts +103 -0
  86. package/src/utils/validatePanelGroupLayout.test.ts +233 -0
  87. package/src/utils/validatePanelGroupLayout.ts +88 -0
  88. package/src/vendor/react.ts +4 -0
  89. package/.eslintrc.json +0 -22
  90. package/dist/declarations/src/utils/group.d.ts +0 -29
  91. package/src/PanelContexts.ts +0 -22
  92. package/src/utils/coordinates.ts +0 -149
  93. package/src/utils/group.ts +0 -614
@@ -7,6 +7,7 @@ import * as React from 'react';
7
7
  const {
8
8
  createElement,
9
9
  createContext,
10
+ createRef,
10
11
  forwardRef,
11
12
  useCallback,
12
13
  useContext,
@@ -21,6 +22,9 @@ const {
21
22
  // `toString()` prevents bundlers from trying to `import { useId } from 'react'`
22
23
  const useId = React["useId".toString()];
23
24
 
25
+ const PanelGroupContext = createContext(null);
26
+ PanelGroupContext.displayName = "PanelGroupContext";
27
+
24
28
  const useIsomorphicLayoutEffect = useLayoutEffect ;
25
29
 
26
30
  const wrappedUseId = typeof useId === "function" ? useId : () => null;
@@ -31,121 +35,140 @@ function useUniqueId(idFromParams = null) {
31
35
  if (idRef.current === null) {
32
36
  idRef.current = "" + counter++;
33
37
  }
34
- return idRef.current;
38
+ return idFromParams ?? idRef.current;
35
39
  }
36
40
 
37
- const PanelGroupContext = createContext(null);
38
- PanelGroupContext.displayName = "PanelGroupContext";
39
-
40
41
  function PanelWithForwardedRef({
41
- children = null,
42
+ children,
42
43
  className: classNameFromProps = "",
43
- collapsedSize = 0,
44
- collapsible = false,
45
- defaultSize = null,
44
+ collapsedSizePercentage,
45
+ collapsedSizePixels,
46
+ collapsible,
47
+ defaultSizePercentage,
48
+ defaultSizePixels,
46
49
  forwardedRef,
47
- id: idFromProps = null,
48
- maxSize = null,
49
- minSize,
50
- onCollapse = null,
51
- onResize = null,
52
- order = null,
53
- style: styleFromProps = {},
50
+ id: idFromProps,
51
+ maxSizePercentage,
52
+ maxSizePixels,
53
+ minSizePercentage,
54
+ minSizePixels,
55
+ onCollapse,
56
+ onExpand,
57
+ onResize,
58
+ order,
59
+ style: styleFromProps,
54
60
  tagName: Type = "div"
55
61
  }) {
56
62
  const context = useContext(PanelGroupContext);
57
63
  if (context === null) {
58
64
  throw Error(`Panel components must be rendered within a PanelGroup container`);
59
65
  }
60
- const panelId = useUniqueId(idFromProps);
61
66
  const {
62
67
  collapsePanel,
63
68
  expandPanel,
64
69
  getPanelSize,
65
70
  getPanelStyle,
71
+ isPanelCollapsed,
66
72
  registerPanel,
67
73
  resizePanel,
68
- units,
69
74
  unregisterPanel
70
75
  } = context;
71
- if (minSize == null) {
72
- if (units === "percentages") {
73
- // Mimics legacy default value for percentage based panel groups
74
- minSize = 10;
75
- } else {
76
- // There is no meaningful minimum pixel default we can provide
77
- minSize = 0;
78
- }
79
- }
80
-
81
- // Use a ref to guard against users passing inline props
82
- const callbacksRef = useRef({
83
- onCollapse,
84
- onResize
85
- });
86
- useEffect(() => {
87
- callbacksRef.current.onCollapse = onCollapse;
88
- callbacksRef.current.onResize = onResize;
89
- });
90
- const style = getPanelStyle(panelId, defaultSize);
91
- const committedValuesRef = useRef({
92
- size: parseSizeFromStyle(style)
93
- });
76
+ const panelId = useUniqueId(idFromProps);
94
77
  const panelDataRef = useRef({
95
- callbacksRef,
96
- collapsedSize,
97
- collapsible,
98
- defaultSize,
78
+ callbacks: {
79
+ onCollapse,
80
+ onExpand,
81
+ onResize
82
+ },
83
+ constraints: {
84
+ collapsedSizePercentage,
85
+ collapsedSizePixels,
86
+ collapsible,
87
+ defaultSizePercentage,
88
+ defaultSizePixels,
89
+ maxSizePercentage,
90
+ maxSizePixels,
91
+ minSizePercentage,
92
+ minSizePixels
93
+ },
99
94
  id: panelId,
100
- idWasAutoGenerated: idFromProps == null,
101
- maxSize,
102
- minSize,
95
+ idIsFromProps: idFromProps !== undefined,
103
96
  order
104
97
  });
98
+ const devWarningsRef = useRef({
99
+ didLogMissingDefaultSizeWarning: false
100
+ });
101
+
102
+ // Normally we wouldn't log a warning during render,
103
+ // but effects don't run on the server, so we can't do it there
104
+ {
105
+ if (!devWarningsRef.current.didLogMissingDefaultSizeWarning) ;
106
+ }
105
107
  useIsomorphicLayoutEffect(() => {
106
- committedValuesRef.current.size = parseSizeFromStyle(style);
107
- panelDataRef.current.callbacksRef = callbacksRef;
108
- panelDataRef.current.collapsedSize = collapsedSize;
109
- panelDataRef.current.collapsible = collapsible;
110
- panelDataRef.current.defaultSize = defaultSize;
108
+ const {
109
+ callbacks,
110
+ constraints
111
+ } = panelDataRef.current;
111
112
  panelDataRef.current.id = panelId;
112
- panelDataRef.current.idWasAutoGenerated = idFromProps == null;
113
- panelDataRef.current.maxSize = maxSize;
114
- panelDataRef.current.minSize = minSize;
113
+ panelDataRef.current.idIsFromProps = idFromProps !== undefined;
115
114
  panelDataRef.current.order = order;
115
+ callbacks.onCollapse = onCollapse;
116
+ callbacks.onExpand = onExpand;
117
+ callbacks.onResize = onResize;
118
+ constraints.collapsedSizePercentage = collapsedSizePercentage;
119
+ constraints.collapsedSizePixels = collapsedSizePixels;
120
+ constraints.collapsible = collapsible;
121
+ constraints.defaultSizePercentage = defaultSizePercentage;
122
+ constraints.defaultSizePixels = defaultSizePixels;
123
+ constraints.maxSizePercentage = maxSizePercentage;
124
+ constraints.maxSizePixels = maxSizePixels;
125
+ constraints.minSizePercentage = minSizePercentage;
126
+ constraints.minSizePixels = minSizePixels;
116
127
  });
117
128
  useIsomorphicLayoutEffect(() => {
118
- registerPanel(panelId, panelDataRef);
129
+ const panelData = panelDataRef.current;
130
+ registerPanel(panelData);
119
131
  return () => {
120
- unregisterPanel(panelId);
132
+ unregisterPanel(panelData);
121
133
  };
122
134
  }, [order, panelId, registerPanel, unregisterPanel]);
123
135
  useImperativeHandle(forwardedRef, () => ({
124
- collapse: () => collapsePanel(panelId),
125
- expand: () => expandPanel(panelId),
126
- getCollapsed() {
127
- return committedValuesRef.current.size === 0;
136
+ collapse: () => {
137
+ collapsePanel(panelDataRef.current);
138
+ },
139
+ expand: () => {
140
+ expandPanel(panelDataRef.current);
128
141
  },
129
142
  getId() {
130
143
  return panelId;
131
144
  },
132
- getSize(units) {
133
- return getPanelSize(panelId, units);
145
+ getSize() {
146
+ return getPanelSize(panelDataRef.current);
134
147
  },
135
- resize: (percentage, units) => resizePanel(panelId, percentage, units)
136
- }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
148
+ isCollapsed() {
149
+ return isPanelCollapsed(panelDataRef.current);
150
+ },
151
+ isExpanded() {
152
+ return !isPanelCollapsed(panelDataRef.current);
153
+ },
154
+ resize: mixedSizes => {
155
+ resizePanel(panelDataRef.current, mixedSizes);
156
+ }
157
+ }), [collapsePanel, expandPanel, getPanelSize, isPanelCollapsed, panelId, resizePanel]);
158
+ const style = getPanelStyle(panelDataRef.current);
137
159
  return createElement(Type, {
138
160
  children,
139
161
  className: classNameFromProps,
140
- "data-panel": "",
141
- "data-panel-collapsible": collapsible || undefined,
142
- "data-panel-id": panelId,
143
- "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1),
144
- id: `data-panel-id-${panelId}`,
145
162
  style: {
146
163
  ...style,
147
164
  ...styleFromProps
148
- }
165
+ },
166
+ // CSS selectors
167
+ "data-panel": "",
168
+ // e2e test attributes
169
+ "data-panel-collapsible": collapsible || undefined ,
170
+ "data-panel-id": panelId ,
171
+ "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
149
172
  });
150
173
  }
151
174
  const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
@@ -155,36 +178,133 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
155
178
  PanelWithForwardedRef.displayName = "Panel";
156
179
  Panel.displayName = "forwardRef(Panel)";
157
180
 
158
- // HACK
159
- function parseSizeFromStyle(style) {
181
+ const PRECISION = 10;
182
+
183
+ function convertPixelsToPercentage(pixels, groupSizePixels) {
184
+ return pixels / groupSizePixels * 100;
185
+ }
186
+
187
+ function convertPixelConstraintsToPercentages(panelConstraints, groupSizePixels) {
188
+ let {
189
+ collapsedSizePercentage = 0,
190
+ collapsedSizePixels,
191
+ defaultSizePercentage,
192
+ defaultSizePixels,
193
+ maxSizePercentage = 100,
194
+ maxSizePixels,
195
+ minSizePercentage = 0,
196
+ minSizePixels
197
+ } = panelConstraints;
198
+ if (collapsedSizePixels != null) {
199
+ collapsedSizePercentage = convertPixelsToPercentage(collapsedSizePixels, groupSizePixels);
200
+ }
201
+ if (defaultSizePixels != null) {
202
+ defaultSizePercentage = convertPixelsToPercentage(defaultSizePixels, groupSizePixels);
203
+ }
204
+ if (minSizePixels != null) {
205
+ minSizePercentage = convertPixelsToPercentage(minSizePixels, groupSizePixels);
206
+ }
207
+ if (maxSizePixels != null) {
208
+ maxSizePercentage = convertPixelsToPercentage(maxSizePixels, groupSizePixels);
209
+ }
210
+ return {
211
+ collapsedSizePercentage,
212
+ defaultSizePercentage,
213
+ maxSizePercentage,
214
+ minSizePercentage
215
+ };
216
+ }
217
+
218
+ function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, groupSizePixels) {
219
+ // All panel constraints, excluding the current one
220
+ let totalMinConstraints = 0;
221
+ let totalMaxConstraints = 0;
222
+ for (let index = 0; index < panelConstraintsArray.length; index++) {
223
+ if (index !== panelIndex) {
224
+ const {
225
+ collapsible
226
+ } = panelConstraintsArray[index];
227
+ const {
228
+ collapsedSizePercentage,
229
+ maxSizePercentage,
230
+ minSizePercentage
231
+ } = convertPixelConstraintsToPercentages(panelConstraintsArray[index], groupSizePixels);
232
+ totalMaxConstraints += maxSizePercentage;
233
+ totalMinConstraints += collapsible ? collapsedSizePercentage : minSizePercentage;
234
+ }
235
+ }
160
236
  const {
161
- flexGrow
162
- } = style;
163
- if (typeof flexGrow === "string") {
164
- return parseFloat(flexGrow);
237
+ collapsedSizePercentage,
238
+ defaultSizePercentage,
239
+ maxSizePercentage,
240
+ minSizePercentage
241
+ } = convertPixelConstraintsToPercentages(panelConstraintsArray[panelIndex], groupSizePixels);
242
+ return {
243
+ collapsedSizePercentage,
244
+ defaultSizePercentage,
245
+ maxSizePercentage: panelConstraintsArray.length > 1 ? Math.min(maxSizePercentage, 100 - totalMinConstraints) : maxSizePercentage,
246
+ minSizePercentage: panelConstraintsArray.length > 1 ? Math.max(minSizePercentage, 100 - totalMaxConstraints) : minSizePercentage
247
+ };
248
+ }
249
+
250
+ function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
251
+ actual = parseFloat(actual.toFixed(fractionDigits));
252
+ expected = parseFloat(expected.toFixed(fractionDigits));
253
+ const delta = actual - expected;
254
+ if (delta === 0) {
255
+ return 0;
165
256
  } else {
166
- return flexGrow;
257
+ return delta > 0 ? 1 : -1;
167
258
  }
168
259
  }
169
260
 
170
- const PRECISION = 10;
261
+ function fuzzyNumbersEqual(actual, expected, fractionDigits) {
262
+ return fuzzyCompareNumbers(actual, expected, fractionDigits) === 0;
263
+ }
171
264
 
172
- function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, prevSizes, panelSizeBeforeCollapse, initialDragState) {
173
- const {
174
- id: groupId,
175
- panels,
176
- units
177
- } = committedValues;
178
- const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
265
+ // Panel size must be in percentages; pixel values should be pre-converted
266
+ function resizePanel({
267
+ groupSizePixels,
268
+ panelConstraints,
269
+ panelIndex,
270
+ size
271
+ }) {
272
+ let {
273
+ collapsible
274
+ } = panelConstraints[panelIndex];
179
275
  const {
180
- sizes: initialSizes
181
- } = initialDragState || {};
182
-
183
- // If we're resizing by mouse or touch, use the initial sizes as a base.
184
- // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
185
- const baseSizes = initialSizes || prevSizes;
186
- const panelsArray = panelsMapToSortedArray(panels);
187
- const nextSizes = baseSizes.concat();
276
+ collapsedSizePercentage,
277
+ maxSizePercentage,
278
+ minSizePercentage
279
+ } = computePercentagePanelConstraints(panelConstraints, panelIndex, groupSizePixels);
280
+ if (minSizePercentage != null) {
281
+ if (fuzzyCompareNumbers(size, minSizePercentage) < 0) {
282
+ if (collapsible) {
283
+ size = collapsedSizePercentage;
284
+ } else {
285
+ size = minSizePercentage;
286
+ }
287
+ }
288
+ }
289
+ if (maxSizePercentage != null) {
290
+ size = Math.min(maxSizePercentage, size);
291
+ }
292
+ return size;
293
+ }
294
+
295
+ // All units must be in percentages; pixel values should be pre-converted
296
+ function adjustLayoutByDelta({
297
+ delta,
298
+ groupSizePixels,
299
+ layout: prevLayout,
300
+ panelConstraints,
301
+ pivotIndices,
302
+ trigger
303
+ }) {
304
+ if (fuzzyNumbersEqual(delta, 0)) {
305
+ return prevLayout;
306
+ }
307
+ const nextLayout = [...prevLayout];
188
308
  let deltaApplied = 0;
189
309
 
190
310
  // A resizing panel affects the panels before or after it.
@@ -195,206 +315,243 @@ function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, p
195
315
  // A positive delta means the panel immediately before the resizer should "expand".
196
316
  // This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the resizer.
197
317
 
198
- // Max-bounds check the panel being expanded first.
318
+ // First, check the panel we're pivoting around;
319
+ // We should only expand or contract by as much as its constraints allow
199
320
  {
200
- const pivotId = deltaPixels < 0 ? idAfter : idBefore;
201
- const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
202
- const panel = panelsArray[index];
203
- const baseSize = baseSizes[index];
204
- const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
205
- if (baseSize === nextSize) {
206
- // If there's no room for the pivot panel to grow, we can ignore this drag update.
207
- return baseSizes;
208
- } else {
209
- if (nextSize === 0 && baseSize > 0) {
210
- panelSizeBeforeCollapse.set(pivotId, baseSize);
321
+ const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
322
+ const initialSize = nextLayout[pivotIndex];
323
+ const {
324
+ collapsible
325
+ } = panelConstraints[pivotIndex];
326
+ const {
327
+ collapsedSizePercentage,
328
+ maxSizePercentage,
329
+ minSizePercentage
330
+ } = computePercentagePanelConstraints(panelConstraints, pivotIndex, groupSizePixels);
331
+ const isCollapsed = collapsible && fuzzyNumbersEqual(initialSize, collapsedSizePercentage);
332
+ let unsafeSize = initialSize + Math.abs(delta);
333
+ if (isCollapsed) {
334
+ switch (trigger) {
335
+ case "keyboard":
336
+ if (minSizePercentage > unsafeSize) {
337
+ unsafeSize = minSizePercentage;
338
+ }
211
339
  }
212
- deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
213
340
  }
214
- }
215
- let pivotId = deltaPixels < 0 ? idBefore : idAfter;
216
- let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
217
- while (true) {
218
- const panel = panelsArray[index];
219
- const baseSize = baseSizes[index];
220
- const deltaRemaining = Math.abs(deltaPixels) - Math.abs(deltaApplied);
221
- const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize - deltaRemaining, event);
222
- if (baseSize !== nextSize) {
223
- if (nextSize === 0 && baseSize > 0) {
224
- panelSizeBeforeCollapse.set(panel.current.id, baseSize);
225
- }
226
- deltaApplied += baseSize - nextSize;
227
- nextSizes[index] = nextSize;
228
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
229
- numeric: true
230
- }) >= 0) {
231
- break;
232
- }
341
+ const safeSize = resizePanel({
342
+ groupSizePixels,
343
+ panelConstraints,
344
+ panelIndex: pivotIndex,
345
+ size: unsafeSize
346
+ });
347
+ if (fuzzyNumbersEqual(initialSize, safeSize)) {
348
+ // If there's no room for the pivot panel to grow, we should ignore this change
349
+ return nextLayout;
350
+ } else {
351
+ delta = delta < 0 ? initialSize - safeSize : safeSize - initialSize;
233
352
  }
234
- if (deltaPixels < 0) {
235
- if (--index < 0) {
236
- break;
353
+ }
354
+
355
+ // Delta added to a panel needs to be subtracted from other panels
356
+ // within the constraints that those panels allow
357
+ {
358
+ const pivotIndex = delta < 0 ? pivotIndices[0] : pivotIndices[1];
359
+ let index = pivotIndex;
360
+ while (index >= 0 && index < panelConstraints.length) {
361
+ const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
362
+ const prevSize = prevLayout[index];
363
+ const unsafeSize = prevSize - deltaRemaining;
364
+ let safeSize = resizePanel({
365
+ groupSizePixels,
366
+ panelConstraints,
367
+ panelIndex: index,
368
+ size: unsafeSize
369
+ });
370
+ if (!fuzzyNumbersEqual(prevSize, safeSize)) {
371
+ deltaApplied += prevSize - safeSize;
372
+ nextLayout[index] = safeSize;
373
+ if (deltaApplied.toPrecision(3).localeCompare(Math.abs(delta).toPrecision(3), undefined, {
374
+ numeric: true
375
+ }) >= 0) {
376
+ break;
377
+ }
237
378
  }
238
- } else {
239
- if (++index >= panelsArray.length) {
240
- break;
379
+ if (delta < 0) {
380
+ index--;
381
+ } else {
382
+ index++;
241
383
  }
242
384
  }
243
385
  }
244
386
 
245
387
  // If we were unable to resize any of the panels panels, return the previous state.
246
- // This will essentially bailout and ignore the "mousemove" event.
247
- if (deltaApplied === 0) {
248
- return baseSizes;
388
+ // This will essentially bailout and ignore e.g. drags past a panel's boundaries
389
+ if (fuzzyNumbersEqual(deltaApplied, 0)) {
390
+ return prevLayout;
249
391
  }
392
+ {
393
+ const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
394
+ const unsafeSize = prevLayout[pivotIndex] + deltaApplied;
395
+ const safeSize = resizePanel({
396
+ groupSizePixels,
397
+ panelConstraints,
398
+ panelIndex: pivotIndex,
399
+ size: unsafeSize
400
+ });
250
401
 
251
- // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
252
- pivotId = deltaPixels < 0 ? idAfter : idBefore;
253
- index = panelsArray.findIndex(panel => panel.current.id === pivotId);
254
- nextSizes[index] = baseSizes[index] + deltaApplied;
255
- return nextSizes;
256
- }
257
- function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
258
- sizes.forEach((size, index) => {
259
- const panelRef = panelsArray[index];
260
- if (!panelRef) {
261
- // Handle initial mount (when panels are registered too late to be in the panels array)
262
- // The subsequent render+effects will handle the resize notification
263
- return;
264
- }
265
- const {
266
- callbacksRef,
267
- collapsedSize,
268
- collapsible,
269
- id
270
- } = panelRef.current;
271
- const lastNotifiedSize = panelIdToLastNotifiedSizeMap[id];
272
- if (lastNotifiedSize !== size) {
273
- panelIdToLastNotifiedSizeMap[id] = size;
274
- const {
275
- onCollapse,
276
- onResize
277
- } = callbacksRef.current;
278
- if (onResize) {
279
- onResize(size, lastNotifiedSize);
402
+ // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
403
+ nextLayout[pivotIndex] = safeSize;
404
+
405
+ // Edge case where expanding or contracting one panel caused another one to change collapsed state
406
+ if (!fuzzyNumbersEqual(safeSize, unsafeSize)) {
407
+ let deltaRemaining = unsafeSize - safeSize;
408
+ const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
409
+ let index = pivotIndex;
410
+ while (index >= 0 && index < panelConstraints.length) {
411
+ const prevSize = nextLayout[index];
412
+ const unsafeSize = prevSize + deltaRemaining;
413
+ const safeSize = resizePanel({
414
+ groupSizePixels,
415
+ panelConstraints,
416
+ panelIndex: index,
417
+ size: unsafeSize
418
+ });
419
+ if (!fuzzyNumbersEqual(prevSize, safeSize)) {
420
+ deltaRemaining -= safeSize - prevSize;
421
+ nextLayout[index] = safeSize;
422
+ }
423
+ if (fuzzyNumbersEqual(deltaRemaining, 0)) {
424
+ break;
425
+ }
426
+ if (delta > 0) {
427
+ index--;
428
+ } else {
429
+ index++;
430
+ }
280
431
  }
281
- if (collapsible && onCollapse) {
282
- if ((lastNotifiedSize == null || lastNotifiedSize === collapsedSize) && size !== collapsedSize) {
283
- onCollapse(false);
284
- } else if (lastNotifiedSize !== collapsedSize && size === collapsedSize) {
285
- onCollapse(true);
432
+
433
+ // If we can't redistribute, this layout is invalid;
434
+ // There may be an incremental layout that is valid though
435
+ if (!fuzzyNumbersEqual(deltaRemaining, 0)) {
436
+ try {
437
+ return adjustLayoutByDelta({
438
+ delta: delta < 0 ? delta + 1 : delta - 1,
439
+ groupSizePixels,
440
+ layout: prevLayout,
441
+ panelConstraints,
442
+ pivotIndices,
443
+ trigger
444
+ });
445
+ } catch (error) {
446
+ if (error instanceof RangeError) {
447
+ console.error(`Could not apply delta ${delta} to layout`);
448
+ return prevLayout;
449
+ }
450
+ } finally {
286
451
  }
287
452
  }
288
453
  }
289
- });
454
+ }
455
+ return nextLayout;
290
456
  }
291
- function calculateDefaultLayout({
292
- groupId,
293
- panels,
294
- units
295
- }) {
296
- const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
297
- const panelsArray = panelsMapToSortedArray(panels);
298
- const sizes = Array(panelsArray.length);
299
- let numPanelsWithSizes = 0;
300
- let remainingSize = 100;
301
457
 
302
- // Assigning default sizes requires a couple of passes:
303
- // First, all panels with defaultSize should be set as-is
304
- for (let index = 0; index < panelsArray.length; index++) {
305
- const panel = panelsArray[index];
306
- const {
307
- defaultSize
308
- } = panel.current;
309
- if (defaultSize != null) {
310
- numPanelsWithSizes++;
311
- sizes[index] = units === "pixels" ? defaultSize / groupSizePixels * 100 : defaultSize;
312
- remainingSize -= sizes[index];
313
- }
458
+ function assert(expectedCondition, message = "Assertion failed!") {
459
+ if (!expectedCondition) {
460
+ console.error(message);
461
+ throw Error(message);
314
462
  }
463
+ }
315
464
 
316
- // Remaining total size should be distributed evenly between panels
317
- // This may require two passes, depending on min/max constraints
318
- for (let index = 0; index < panelsArray.length; index++) {
319
- const panel = panelsArray[index];
320
- let {
321
- defaultSize,
322
- id,
323
- maxSize,
324
- minSize
325
- } = panel.current;
326
- if (defaultSize != null) {
327
- continue;
328
- }
329
- if (units === "pixels") {
330
- minSize = minSize / groupSizePixels * 100;
331
- if (maxSize != null) {
332
- maxSize = maxSize / groupSizePixels * 100;
333
- }
334
- }
335
- const remainingPanels = panelsArray.length - numPanelsWithSizes;
336
- const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, remainingSize / remainingPanels));
337
- sizes[index] = size;
338
- numPanelsWithSizes++;
339
- remainingSize -= size;
465
+ function getPercentageSizeFromMixedSizes({
466
+ sizePercentage,
467
+ sizePixels
468
+ }, groupSizePixels) {
469
+ if (sizePercentage != null) {
470
+ return sizePercentage;
471
+ } else if (sizePixels != null) {
472
+ return convertPixelsToPercentage(sizePixels, groupSizePixels);
340
473
  }
474
+ return undefined;
475
+ }
341
476
 
342
- // If there is additional, left over space, assign it to any panel(s) that permits it
343
- // (It's not worth taking multiple additional passes to evenly distribute)
344
- if (remainingSize !== 0) {
345
- for (let index = 0; index < panelsArray.length; index++) {
346
- const panel = panelsArray[index];
347
- let {
348
- maxSize,
349
- minSize
350
- } = panel.current;
351
- if (units === "pixels") {
352
- minSize = minSize / groupSizePixels * 100;
353
- if (maxSize != null) {
354
- maxSize = maxSize / groupSizePixels * 100;
355
- }
356
- }
357
- const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, sizes[index] + remainingSize));
358
- if (size !== sizes[index]) {
359
- remainingSize -= size - sizes[index];
360
- sizes[index] = size;
477
+ function calculateAriaValues({
478
+ groupSizePixels,
479
+ layout,
480
+ panelsArray,
481
+ pivotIndices
482
+ }) {
483
+ let currentMinSize = 0;
484
+ let currentMaxSize = 100;
485
+ let totalMinSize = 0;
486
+ let totalMaxSize = 0;
361
487
 
362
- // Fuzzy comparison to account for imprecise floating point math
363
- if (Math.abs(remainingSize).toFixed(3) === "0.000") {
364
- break;
365
- }
366
- }
488
+ // A panel's effective min/max sizes also need to account for other panel's sizes.
489
+ panelsArray.forEach((panelData, index) => {
490
+ const {
491
+ constraints
492
+ } = panelData;
493
+ const {
494
+ maxSizePercentage,
495
+ maxSizePixels,
496
+ minSizePercentage,
497
+ minSizePixels
498
+ } = constraints;
499
+ const minSize = getPercentageSizeFromMixedSizes({
500
+ sizePercentage: minSizePercentage,
501
+ sizePixels: minSizePixels
502
+ }, groupSizePixels) ?? 0;
503
+ const maxSize = getPercentageSizeFromMixedSizes({
504
+ sizePercentage: maxSizePercentage,
505
+ sizePixels: maxSizePixels
506
+ }, groupSizePixels) ?? 100;
507
+ if (index === pivotIndices[0]) {
508
+ currentMinSize = minSize;
509
+ currentMaxSize = maxSize;
510
+ } else {
511
+ totalMinSize += minSize;
512
+ totalMaxSize += maxSize;
367
513
  }
368
- }
514
+ });
515
+ const valueMax = Math.min(currentMaxSize, 100 - totalMinSize);
516
+ const valueMin = Math.max(currentMinSize, 100 - totalMaxSize);
517
+ const valueNow = layout[pivotIndices[0]];
518
+ return {
519
+ valueMax,
520
+ valueMin,
521
+ valueNow
522
+ };
523
+ }
369
524
 
370
- // Finally, if there is still left-over size, log an error
371
- if (Math.abs(remainingSize).toFixed(3) !== "0.000") {
372
- {
373
- console.error(`Invalid panel group configuration; default panel sizes should total 100% but was ${(100 - remainingSize).toFixed(1)}%. This can cause the cursor to become misaligned while dragging.`);
374
- }
375
- }
376
- return sizes;
525
+ function getResizeHandleElementsForGroup(groupId) {
526
+ return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`));
377
527
  }
378
- function getBeforeAndAfterIds(id, panelsArray) {
379
- if (panelsArray.length < 2) {
380
- return [null, null];
381
- }
382
- const index = panelsArray.findIndex(panel => panel.current.id === id);
383
- if (index < 0) {
384
- return [null, null];
528
+
529
+ function getResizeHandleElementIndex(groupId, id) {
530
+ const handles = getResizeHandleElementsForGroup(groupId);
531
+ const index = handles.findIndex(handle => handle.getAttribute("data-panel-resize-handle-id") === id);
532
+ return index ?? null;
533
+ }
534
+
535
+ function determinePivotIndices(groupId, dragHandleId) {
536
+ const index = getResizeHandleElementIndex(groupId, dragHandleId);
537
+ return index != null ? [index, index + 1] : [-1, -1];
538
+ }
539
+
540
+ function getPanelGroupElement(id) {
541
+ const element = document.querySelector(`[data-panel-group-id="${id}"]`);
542
+ if (element) {
543
+ return element;
385
544
  }
386
- const isLastPanel = index === panelsArray.length - 1;
387
- const idBefore = isLastPanel ? panelsArray[index - 1].current.id : id;
388
- const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
389
- return [idBefore, idAfter];
545
+ return null;
390
546
  }
391
- function getAvailableGroupSizePixels(groupId) {
392
- const panelGroupElement = getPanelGroup(groupId);
547
+
548
+ function calculateAvailablePanelSizeInPixels(groupId) {
549
+ const panelGroupElement = getPanelGroupElement(groupId);
393
550
  if (panelGroupElement == null) {
394
551
  return NaN;
395
552
  }
396
553
  const direction = panelGroupElement.getAttribute("data-panel-group-direction");
397
- const resizeHandles = getResizeHandlesForGroup(groupId);
554
+ const resizeHandles = getResizeHandleElementsForGroup(groupId);
398
555
  if (direction === "horizontal") {
399
556
  return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
400
557
  return accumulated + handle.offsetWidth;
@@ -406,278 +563,112 @@ function getAvailableGroupSizePixels(groupId) {
406
563
  }
407
564
  }
408
565
 
409
- // This method returns a number between 1 and 100 representing
410
- // the % of the group's overall space this panel should occupy.
411
- function getFlexGrow(panels, id, sizes) {
412
- if (panels.size === 1) {
413
- return "100";
414
- }
415
- const panelsArray = panelsMapToSortedArray(panels);
416
- const index = panelsArray.findIndex(panel => panel.current.id === id);
417
- const size = sizes[index];
418
- if (size == null) {
419
- return "0";
420
- }
421
- return size.toPrecision(PRECISION);
422
- }
423
- function getPanel(id) {
424
- const element = document.querySelector(`[data-panel-id="${id}"]`);
425
- if (element) {
426
- return element;
566
+ function getAvailableGroupSizePixels(groupId) {
567
+ const panelGroupElement = getPanelGroupElement(groupId);
568
+ if (panelGroupElement == null) {
569
+ return NaN;
427
570
  }
428
- return null;
429
- }
430
- function getPanelGroup(id) {
431
- const element = document.querySelector(`[data-panel-group-id="${id}"]`);
432
- if (element) {
433
- return element;
571
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
572
+ const resizeHandles = getResizeHandleElementsForGroup(groupId);
573
+ if (direction === "horizontal") {
574
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
575
+ return accumulated + handle.offsetWidth;
576
+ }, 0);
577
+ } else {
578
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
579
+ return accumulated + handle.offsetHeight;
580
+ }, 0);
434
581
  }
435
- return null;
436
582
  }
437
- function getResizeHandle(id) {
583
+
584
+ function getResizeHandleElement(id) {
438
585
  const element = document.querySelector(`[data-panel-resize-handle-id="${id}"]`);
439
586
  if (element) {
440
587
  return element;
441
588
  }
442
589
  return null;
443
590
  }
444
- function getResizeHandleIndex(id) {
445
- const handles = getResizeHandles();
446
- const index = handles.findIndex(handle => handle.getAttribute("data-panel-resize-handle-id") === id);
447
- return index ?? null;
448
- }
449
- function getResizeHandles() {
450
- return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id]`));
451
- }
452
- function getResizeHandlesForGroup(groupId) {
453
- return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`));
454
- }
591
+
455
592
  function getResizeHandlePanelIds(groupId, handleId, panelsArray) {
456
- const handle = getResizeHandle(handleId);
457
- const handles = getResizeHandlesForGroup(groupId);
593
+ const handle = getResizeHandleElement(handleId);
594
+ const handles = getResizeHandleElementsForGroup(groupId);
458
595
  const index = handle ? handles.indexOf(handle) : -1;
459
- const idBefore = panelsArray[index]?.current?.id ?? null;
460
- const idAfter = panelsArray[index + 1]?.current?.id ?? null;
596
+ const idBefore = panelsArray[index]?.id ?? null;
597
+ const idAfter = panelsArray[index + 1]?.id ?? null;
461
598
  return [idBefore, idAfter];
462
599
  }
463
- function panelsMapToSortedArray(panels) {
464
- return Array.from(panels.values()).sort((panelA, panelB) => {
465
- const orderA = panelA.current.order;
466
- const orderB = panelB.current.order;
467
- if (orderA == null && orderB == null) {
468
- return 0;
469
- } else if (orderA == null) {
470
- return -1;
471
- } else if (orderB == null) {
472
- return 1;
473
- } else {
474
- return orderA - orderB;
475
- }
476
- });
477
- }
478
- function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
479
- let {
480
- collapsedSize,
481
- collapsible,
482
- maxSize,
483
- minSize
484
- } = panel.current;
485
- if (units === "pixels") {
486
- collapsedSize = collapsedSize / groupSizePixels * 100;
487
- if (maxSize != null) {
488
- maxSize = maxSize / groupSizePixels * 100;
489
- }
490
- minSize = minSize / groupSizePixels * 100;
491
- }
492
- if (collapsible) {
493
- if (prevSize > collapsedSize) {
494
- // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
495
- if (nextSize <= minSize / 2 + collapsedSize) {
496
- return collapsedSize;
497
- }
498
- } else {
499
- const isKeyboardEvent = event?.type?.startsWith("key");
500
- if (!isKeyboardEvent) {
501
- // Keyboard events should expand a collapsed panel to the min size,
502
- // but mouse events should wait until the panel has reached its min size
503
- // to avoid a visual flickering when dragging between collapsed and min size.
504
- if (nextSize < minSize) {
505
- return collapsedSize;
506
- }
507
- }
508
- }
509
- }
510
- return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
511
- }
512
- function validatePanelProps(units, panelData) {
513
- const {
514
- collapsible,
515
- defaultSize,
516
- maxSize,
517
- minSize
518
- } = panelData.current;
519
-
520
- // Basic props validation
521
- if (minSize < 0 || units === "percentages" && minSize > 100) {
522
- {
523
- console.error(`Invalid Panel minSize provided, ${minSize}`);
524
- }
525
- panelData.current.minSize = 0;
526
- }
527
- if (maxSize != null) {
528
- if (maxSize < 0 || units === "percentages" && maxSize > 100) {
529
- {
530
- console.error(`Invalid Panel maxSize provided, ${maxSize}`);
531
- }
532
- panelData.current.maxSize = null;
533
- }
534
- }
535
- if (defaultSize !== null) {
536
- if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
537
- {
538
- console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
539
- }
540
- panelData.current.defaultSize = null;
541
- } else if (defaultSize < minSize && !collapsible) {
542
- {
543
- console.error(`Panel minSize (${minSize}) cannot be greater than defaultSize (${defaultSize})`);
544
- }
545
- panelData.current.defaultSize = minSize;
546
- } else if (maxSize != null && defaultSize > maxSize) {
547
- {
548
- console.error(`Panel maxSize (${maxSize}) cannot be less than defaultSize (${defaultSize})`);
549
- }
550
- panelData.current.defaultSize = maxSize;
551
- }
552
- }
553
- }
554
- function validatePanelGroupLayout({
555
- groupId,
556
- panels,
557
- nextSizes,
558
- prevSizes,
559
- units
560
- }) {
561
- // Clone because this method modifies
562
- nextSizes = [...nextSizes];
563
- const panelsArray = panelsMapToSortedArray(panels);
564
- const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
565
- let remainingSize = 0;
566
-
567
- // First, check all of the proposed sizes against the min/max constraints
568
- for (let index = 0; index < panelsArray.length; index++) {
569
- const panel = panelsArray[index];
570
- const prevSize = prevSizes[index];
571
- const nextSize = nextSizes[index];
572
- const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
573
- if (nextSize != safeNextSize) {
574
- remainingSize += nextSize - safeNextSize;
575
- nextSizes[index] = safeNextSize;
576
- {
577
- console.error(`Invalid size (${nextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
578
- }
579
- }
580
- }
581
-
582
- // If there is additional, left over space, assign it to any panel(s) that permits it
583
- // (It's not worth taking multiple additional passes to evenly distribute)
584
- if (remainingSize.toFixed(3) !== "0.000") {
585
- for (let index = 0; index < panelsArray.length; index++) {
586
- const panel = panelsArray[index];
587
- let {
588
- maxSize,
589
- minSize
590
- } = panel.current;
591
- if (units === "pixels") {
592
- minSize = minSize / groupSizePixels * 100;
593
- if (maxSize != null) {
594
- maxSize = maxSize / groupSizePixels * 100;
595
- }
596
- }
597
- const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
598
- if (size !== nextSizes[index]) {
599
- remainingSize -= size - nextSizes[index];
600
- nextSizes[index] = size;
601
-
602
- // Fuzzy comparison to account for imprecise floating point math
603
- if (Math.abs(remainingSize).toFixed(3) === "0.000") {
604
- break;
605
- }
606
- }
607
- }
608
- }
609
-
610
- // If we still have remainder, the requested layout wasn't valid and we should warn about it
611
- if (remainingSize.toFixed(3) !== "0.000") {
612
- {
613
- console.error(`"Invalid panel group configuration; default panel sizes should total 100% but was ${100 - remainingSize}%`);
614
- }
615
- }
616
- return nextSizes;
617
- }
618
-
619
- function assert(expectedCondition, message = "Assertion failed!") {
620
- if (!expectedCondition) {
621
- console.error(message);
622
- throw Error(message);
623
- }
624
- }
625
600
 
626
601
  // https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
627
602
 
628
603
  function useWindowSplitterPanelGroupBehavior({
629
604
  committedValuesRef,
630
605
  groupId,
631
- panels,
632
- setSizes,
633
- sizes,
634
- panelSizeBeforeCollapse
606
+ layout,
607
+ panelDataArray,
608
+ setLayout
635
609
  }) {
610
+ const devWarningsRef = useRef({
611
+ didWarnAboutMissingResizeHandle: false
612
+ });
613
+ useIsomorphicLayoutEffect(() => {
614
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
615
+ const resizeHandleElements = getResizeHandleElementsForGroup(groupId);
616
+ for (let index = 0; index < panelDataArray.length - 1; index++) {
617
+ const {
618
+ valueMax,
619
+ valueMin,
620
+ valueNow
621
+ } = calculateAriaValues({
622
+ groupSizePixels,
623
+ layout,
624
+ panelsArray: panelDataArray,
625
+ pivotIndices: [index, index + 1]
626
+ });
627
+ const resizeHandleElement = resizeHandleElements[index];
628
+ if (resizeHandleElement == null) {
629
+ {
630
+ const {
631
+ didWarnAboutMissingResizeHandle
632
+ } = devWarningsRef.current;
633
+ if (!didWarnAboutMissingResizeHandle) {
634
+ devWarningsRef.current.didWarnAboutMissingResizeHandle = true;
635
+ console.warn(`WARNING: Missing resize handle for PanelGroup "${groupId}"`);
636
+ }
637
+ }
638
+ } else {
639
+ resizeHandleElement.setAttribute("aria-controls", panelDataArray[index].id);
640
+ resizeHandleElement.setAttribute("aria-valuemax", "" + Math.round(valueMax));
641
+ resizeHandleElement.setAttribute("aria-valuemin", "" + Math.round(valueMin));
642
+ resizeHandleElement.setAttribute("aria-valuenow", "" + Math.round(valueNow));
643
+ }
644
+ }
645
+ return () => {
646
+ resizeHandleElements.forEach((resizeHandleElement, index) => {
647
+ resizeHandleElement.removeAttribute("aria-controls");
648
+ resizeHandleElement.removeAttribute("aria-valuemax");
649
+ resizeHandleElement.removeAttribute("aria-valuemin");
650
+ resizeHandleElement.removeAttribute("aria-valuenow");
651
+ });
652
+ };
653
+ }, [groupId, layout, panelDataArray]);
636
654
  useEffect(() => {
637
655
  const {
638
656
  direction,
639
- panels
657
+ panelDataArray
640
658
  } = committedValuesRef.current;
641
- const groupElement = getPanelGroup(groupId);
659
+ const groupElement = getPanelGroupElement(groupId);
642
660
  assert(groupElement != null, `No group found for id "${groupId}"`);
643
661
  const {
644
662
  height,
645
663
  width
646
664
  } = groupElement.getBoundingClientRect();
647
- const handles = getResizeHandlesForGroup(groupId);
665
+ const handles = getResizeHandleElementsForGroup(groupId);
648
666
  const cleanupFunctions = handles.map(handle => {
649
667
  const handleId = handle.getAttribute("data-panel-resize-handle-id");
650
- const panelsArray = panelsMapToSortedArray(panels);
651
- const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
668
+ const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelDataArray);
652
669
  if (idBefore == null || idAfter == null) {
653
670
  return () => {};
654
671
  }
655
- let currentMinSize = 0;
656
- let currentMaxSize = 100;
657
- let totalMinSize = 0;
658
- let totalMaxSize = 0;
659
-
660
- // A panel's effective min/max sizes also need to account for other panel's sizes.
661
- panelsArray.forEach(panelData => {
662
- const {
663
- id,
664
- maxSize,
665
- minSize
666
- } = panelData.current;
667
- if (id === idBefore) {
668
- currentMinSize = minSize;
669
- currentMaxSize = maxSize != null ? maxSize : 100;
670
- } else {
671
- totalMinSize += minSize;
672
- totalMaxSize += maxSize != null ? maxSize : 100;
673
- }
674
- });
675
- const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
676
- const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
677
- const flexGrow = getFlexGrow(panels, idBefore, sizes);
678
- handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
679
- handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
680
- handle.setAttribute("aria-valuenow", "" + Math.round(parseInt(flexGrow)));
681
672
  const onKeyDown = event => {
682
673
  if (event.defaultPrevented) {
683
674
  return;
@@ -686,20 +677,32 @@ function useWindowSplitterPanelGroupBehavior({
686
677
  case "Enter":
687
678
  {
688
679
  event.preventDefault();
689
- const index = panelsArray.findIndex(panel => panel.current.id === idBefore);
680
+ const index = panelDataArray.findIndex(panelData => panelData.id === idBefore);
690
681
  if (index >= 0) {
691
- const panelData = panelsArray[index];
692
- const size = sizes[index];
682
+ const panelData = panelDataArray[index];
683
+ const size = layout[index];
693
684
  if (size != null) {
685
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
686
+ const minSize = getPercentageSizeFromMixedSizes({
687
+ sizePercentage: panelData.constraints.minSizePercentage,
688
+ sizePixels: panelData.constraints.minSizePixels
689
+ }, groupSizePixels) ?? 0;
694
690
  let delta = 0;
695
- if (size.toPrecision(PRECISION) <= panelData.current.minSize.toPrecision(PRECISION)) {
691
+ if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
696
692
  delta = direction === "horizontal" ? width : height;
697
693
  } else {
698
694
  delta = -(direction === "horizontal" ? width : height);
699
695
  }
700
- const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
701
- if (sizes !== nextSizes) {
702
- setSizes(nextSizes);
696
+ const nextLayout = adjustLayoutByDelta({
697
+ delta,
698
+ groupSizePixels,
699
+ layout,
700
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints),
701
+ pivotIndices: determinePivotIndices(groupId, handleId),
702
+ trigger: "keyboard"
703
+ });
704
+ if (layout !== nextLayout) {
705
+ setLayout(nextLayout);
703
706
  }
704
707
  }
705
708
  }
@@ -708,72 +711,14 @@ function useWindowSplitterPanelGroupBehavior({
708
711
  }
709
712
  };
710
713
  handle.addEventListener("keydown", onKeyDown);
711
- const panelBefore = getPanel(idBefore);
712
- if (panelBefore != null) {
713
- handle.setAttribute("aria-controls", panelBefore.id);
714
- }
715
714
  return () => {
716
- handle.removeAttribute("aria-valuemax");
717
- handle.removeAttribute("aria-valuemin");
718
- handle.removeAttribute("aria-valuenow");
719
715
  handle.removeEventListener("keydown", onKeyDown);
720
- if (panelBefore != null) {
721
- handle.removeAttribute("aria-controls");
722
- }
723
716
  };
724
717
  });
725
718
  return () => {
726
719
  cleanupFunctions.forEach(cleanupFunction => cleanupFunction());
727
720
  };
728
- }, [committedValuesRef, groupId, panels, panelSizeBeforeCollapse, setSizes, sizes]);
729
- }
730
- function useWindowSplitterResizeHandlerBehavior({
731
- disabled,
732
- handleId,
733
- resizeHandler
734
- }) {
735
- useEffect(() => {
736
- if (disabled || resizeHandler == null) {
737
- return;
738
- }
739
- const handleElement = getResizeHandle(handleId);
740
- if (handleElement == null) {
741
- return;
742
- }
743
- const onKeyDown = event => {
744
- if (event.defaultPrevented) {
745
- return;
746
- }
747
- switch (event.key) {
748
- case "ArrowDown":
749
- case "ArrowLeft":
750
- case "ArrowRight":
751
- case "ArrowUp":
752
- case "End":
753
- case "Home":
754
- {
755
- event.preventDefault();
756
- resizeHandler(event);
757
- break;
758
- }
759
- case "F6":
760
- {
761
- event.preventDefault();
762
- const handles = getResizeHandles();
763
- const index = getResizeHandleIndex(handleId);
764
- assert(index !== null);
765
- const nextIndex = event.shiftKey ? index > 0 ? index - 1 : handles.length - 1 : index + 1 < handles.length ? index + 1 : 0;
766
- const nextHandle = handles[nextIndex];
767
- nextHandle.focus();
768
- break;
769
- }
770
- }
771
- };
772
- handleElement.addEventListener("keydown", onKeyDown);
773
- return () => {
774
- handleElement.removeEventListener("keydown", onKeyDown);
775
- };
776
- }, [disabled, handleId, resizeHandler]);
721
+ }, [committedValuesRef, groupId, layout, panelDataArray, setLayout]);
777
722
  }
778
723
 
779
724
  function areEqual(arrayA, arrayB) {
@@ -788,41 +733,61 @@ function areEqual(arrayA, arrayB) {
788
733
  return true;
789
734
  }
790
735
 
791
- function getDragOffset(event, handleId, direction, initialOffset = 0, initialHandleElementRect = null) {
736
+ function isKeyDown(event) {
737
+ return event.type === "keydown";
738
+ }
739
+ function isMouseEvent(event) {
740
+ return event.type.startsWith("mouse");
741
+ }
742
+ function isTouchEvent(event) {
743
+ return event.type.startsWith("touch");
744
+ }
745
+
746
+ function getResizeEventCursorPosition(direction, event) {
792
747
  const isHorizontal = direction === "horizontal";
793
- let pointerOffset = 0;
794
748
  if (isMouseEvent(event)) {
795
- pointerOffset = isHorizontal ? event.clientX : event.clientY;
749
+ return isHorizontal ? event.clientX : event.clientY;
796
750
  } else if (isTouchEvent(event)) {
797
751
  const firstTouch = event.touches[0];
798
- pointerOffset = isHorizontal ? firstTouch.screenX : firstTouch.screenY;
752
+ return isHorizontal ? firstTouch.screenX : firstTouch.screenY;
799
753
  } else {
800
- return 0;
754
+ throw Error(`Unsupported event type "${event.type}"`);
801
755
  }
802
- const handleElement = getResizeHandle(handleId);
803
- const rect = initialHandleElementRect || handleElement.getBoundingClientRect();
804
- const elementOffset = isHorizontal ? rect.left : rect.top;
805
- return pointerOffset - elementOffset - initialOffset;
756
+ }
757
+
758
+ function calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState) {
759
+ const isHorizontal = direction === "horizontal";
760
+ const handleElement = getResizeHandleElement(dragHandleId);
761
+ const groupId = handleElement.getAttribute("data-panel-group-id");
762
+ let {
763
+ initialCursorPosition
764
+ } = initialDragState;
765
+ const cursorPosition = getResizeEventCursorPosition(direction, event);
766
+ const groupElement = getPanelGroupElement(groupId);
767
+ const groupRect = groupElement.getBoundingClientRect();
768
+ const groupSizeInPixels = isHorizontal ? groupRect.width : groupRect.height;
769
+ const offsetPixels = cursorPosition - initialCursorPosition;
770
+ const offsetPercentage = offsetPixels / groupSizeInPixels * 100;
771
+ return offsetPercentage;
806
772
  }
807
773
 
808
774
  // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
809
- function getMovement(event, groupId, handleId, panelsArray, direction, prevSizes, initialDragState) {
810
- const {
811
- dragOffset = 0,
812
- dragHandleRect,
813
- sizes: initialSizes
814
- } = initialDragState || {};
815
-
816
- // If we're resizing by mouse or touch, use the initial sizes as a base.
817
- // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
818
- const baseSizes = initialSizes || prevSizes;
775
+ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initialDragState, keyboardResizeByOptions) {
819
776
  if (isKeyDown(event)) {
820
777
  const isHorizontal = direction === "horizontal";
821
- const groupElement = getPanelGroup(groupId);
778
+ const groupElement = getPanelGroupElement(groupId);
822
779
  const rect = groupElement.getBoundingClientRect();
823
780
  const groupSizeInPixels = isHorizontal ? rect.width : rect.height;
824
- const denominator = event.shiftKey ? 10 : 100;
825
- const delta = groupSizeInPixels / denominator;
781
+ let delta = 0;
782
+ if (event.shiftKey) {
783
+ delta = 100;
784
+ } else if (keyboardResizeByOptions.percentage != null) {
785
+ delta = keyboardResizeByOptions.percentage;
786
+ } else if (keyboardResizeByOptions.pixels != null) {
787
+ delta = keyboardResizeByOptions.pixels / groupSizeInPixels;
788
+ } else {
789
+ delta = 10;
790
+ }
826
791
  let movement = 0;
827
792
  switch (event.key) {
828
793
  case "ArrowDown":
@@ -838,40 +803,152 @@ function getMovement(event, groupId, handleId, panelsArray, direction, prevSizes
838
803
  movement = isHorizontal ? 0 : -delta;
839
804
  break;
840
805
  case "End":
841
- movement = groupSizeInPixels;
806
+ movement = 100;
842
807
  break;
843
808
  case "Home":
844
- movement = -groupSizeInPixels;
809
+ movement = -100;
845
810
  break;
846
811
  }
847
-
848
- // If the Panel being resized is collapsible,
849
- // we need to special case resizing around the minSize boundary.
850
- // If contracting, Panels should shrink to their minSize and then snap to fully collapsed.
851
- // If expanding from collapsed, they should snap back to their minSize.
852
- const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
853
- const targetPanelId = movement < 0 ? idBefore : idAfter;
854
- const targetPanelIndex = panelsArray.findIndex(panel => panel.current.id === targetPanelId);
855
- const targetPanel = panelsArray[targetPanelIndex];
856
- if (targetPanel.current.collapsible) {
857
- const baseSize = baseSizes[targetPanelIndex];
858
- if (baseSize === 0 || baseSize.toPrecision(PRECISION) === targetPanel.current.minSize.toPrecision(PRECISION)) {
859
- movement = movement < 0 ? -targetPanel.current.minSize * groupSizeInPixels : targetPanel.current.minSize * groupSizeInPixels;
860
- }
861
- }
862
812
  return movement;
863
813
  } else {
864
- return getDragOffset(event, handleId, direction, dragOffset, dragHandleRect);
814
+ return calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState);
865
815
  }
866
816
  }
867
- function isKeyDown(event) {
868
- return event.type === "keydown";
817
+
818
+ function calculateUnsafeDefaultLayout({
819
+ groupSizePixels,
820
+ panelDataArray
821
+ }) {
822
+ const layout = Array(panelDataArray.length);
823
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
824
+ let numPanelsWithSizes = 0;
825
+ let remainingSize = 100;
826
+
827
+ // Distribute default sizes first
828
+ for (let index = 0; index < panelDataArray.length; index++) {
829
+ const {
830
+ defaultSizePercentage
831
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
832
+ if (defaultSizePercentage != null) {
833
+ numPanelsWithSizes++;
834
+ layout[index] = defaultSizePercentage;
835
+ remainingSize -= defaultSizePercentage;
836
+ }
837
+ }
838
+
839
+ // Remaining size should be distributed evenly between panels without default sizes
840
+ for (let index = 0; index < panelDataArray.length; index++) {
841
+ const {
842
+ defaultSizePercentage
843
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
844
+ if (defaultSizePercentage != null) {
845
+ continue;
846
+ }
847
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
848
+ const size = remainingSize / numRemainingPanels;
849
+ numPanelsWithSizes++;
850
+ layout[index] = size;
851
+ remainingSize -= size;
852
+ }
853
+ return layout;
869
854
  }
870
- function isMouseEvent(event) {
871
- return event.type.startsWith("mouse");
855
+
856
+ function convertPercentageToPixels(percentage, groupSizePixels) {
857
+ return percentage / 100 * groupSizePixels;
872
858
  }
873
- function isTouchEvent(event) {
874
- return event.type.startsWith("touch");
859
+
860
+ // Layout should be pre-converted into percentages
861
+ function callPanelCallbacks(groupId, panelsArray, layout, panelIdToLastNotifiedMixedSizesMap) {
862
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
863
+ layout.forEach((sizePercentage, index) => {
864
+ const panelData = panelsArray[index];
865
+ if (!panelData) {
866
+ // Handle initial mount (when panels are registered too late to be in the panels array)
867
+ // The subsequent render+effects will handle the resize notification
868
+ return;
869
+ }
870
+ const {
871
+ callbacks,
872
+ constraints,
873
+ id: panelId
874
+ } = panelData;
875
+ const {
876
+ collapsible
877
+ } = constraints;
878
+ const mixedSizes = {
879
+ sizePercentage,
880
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
881
+ };
882
+ const lastNotifiedMixedSizes = panelIdToLastNotifiedMixedSizesMap[panelId];
883
+ if (lastNotifiedMixedSizes == null || mixedSizes.sizePercentage !== lastNotifiedMixedSizes.sizePercentage || mixedSizes.sizePixels !== lastNotifiedMixedSizes.sizePixels) {
884
+ panelIdToLastNotifiedMixedSizesMap[panelId] = mixedSizes;
885
+ const {
886
+ onCollapse,
887
+ onExpand,
888
+ onResize
889
+ } = callbacks;
890
+ if (onResize) {
891
+ onResize(mixedSizes, lastNotifiedMixedSizes);
892
+ }
893
+ if (collapsible && (onCollapse || onExpand)) {
894
+ const collapsedSize = getPercentageSizeFromMixedSizes({
895
+ sizePercentage: constraints.collapsedSizePercentage,
896
+ sizePixels: constraints.collapsedSizePixels
897
+ }, groupSizePixels) ?? 0;
898
+ const size = getPercentageSizeFromMixedSizes(mixedSizes, groupSizePixels);
899
+ if (onExpand && (lastNotifiedMixedSizes == null || lastNotifiedMixedSizes.sizePercentage === collapsedSize) && size !== collapsedSize) {
900
+ onExpand();
901
+ }
902
+ if (onCollapse && (lastNotifiedMixedSizes == null || lastNotifiedMixedSizes.sizePercentage !== collapsedSize) && size === collapsedSize) {
903
+ onCollapse();
904
+ }
905
+ }
906
+ }
907
+ });
908
+ }
909
+
910
+ function compareLayouts(a, b) {
911
+ if (a.length !== b.length) {
912
+ return false;
913
+ } else {
914
+ for (let index = 0; index < a.length; index++) {
915
+ if (a[index] != b[index]) {
916
+ return false;
917
+ }
918
+ }
919
+ }
920
+ return true;
921
+ }
922
+
923
+ // This method returns a number between 1 and 100 representing
924
+
925
+ // the % of the group's overall space this panel should occupy.
926
+ function computePanelFlexBoxStyle({
927
+ dragState,
928
+ layout,
929
+ panelData,
930
+ panelIndex,
931
+ precision = 3
932
+ }) {
933
+ const size = layout[panelIndex];
934
+ let flexGrow;
935
+ if (panelData.length === 1) {
936
+ flexGrow = "100";
937
+ } else if (size == null) {
938
+ flexGrow = "0";
939
+ } else {
940
+ flexGrow = size.toPrecision(precision);
941
+ }
942
+ return {
943
+ flexBasis: 0,
944
+ flexGrow,
945
+ flexShrink: 1,
946
+ // Without this, Panel sizes may be unintentionally overridden by their content
947
+ overflow: "hidden",
948
+ // Disable pointer events inside of a panel during resize
949
+ // This avoid edge cases like nested iframes
950
+ pointerEvents: dragState !== null ? "none" : undefined
951
+ };
875
952
  }
876
953
 
877
954
  let currentState = null;
@@ -925,17 +1002,47 @@ function debounce(callback, durationMs = 10) {
925
1002
  return callable;
926
1003
  }
927
1004
 
1005
+ // PanelGroup might be rendering in a server-side environment where localStorage is not available
1006
+ // or on a browser with cookies/storage disabled.
1007
+ // In either case, this function avoids accessing localStorage until needed,
1008
+ // and avoids throwing user-visible errors.
1009
+ function initializeDefaultStorage(storageObject) {
1010
+ try {
1011
+ if (typeof localStorage !== "undefined") {
1012
+ // Bypass this check for future calls
1013
+ storageObject.getItem = name => {
1014
+ return localStorage.getItem(name);
1015
+ };
1016
+ storageObject.setItem = (name, value) => {
1017
+ localStorage.setItem(name, value);
1018
+ };
1019
+ } else {
1020
+ throw new Error("localStorage not supported in this environment");
1021
+ }
1022
+ } catch (error) {
1023
+ console.error(error);
1024
+ storageObject.getItem = () => null;
1025
+ storageObject.setItem = () => {};
1026
+ }
1027
+ }
1028
+
928
1029
  // Note that Panel ids might be user-provided (stable) or useId generated (non-deterministic)
929
1030
  // so they should not be used as part of the serialization key.
930
- // Using an attribute like minSize instead should work well enough.
1031
+ // Using the min/max size attributes should work well enough as a backup.
931
1032
  // Pre-sorting by minSize allows remembering layouts even if panels are re-ordered/dragged.
932
1033
  function getSerializationKey(panels) {
933
1034
  return panels.map(panel => {
934
1035
  const {
935
- minSize,
1036
+ constraints,
1037
+ id,
1038
+ idIsFromProps,
936
1039
  order
937
- } = panel.current;
938
- return order ? `${order}:${minSize}` : `${minSize}`;
1040
+ } = panel;
1041
+ if (idIsFromProps) {
1042
+ return id;
1043
+ } else {
1044
+ return `${order}:${JSON.stringify(constraints)}`;
1045
+ }
939
1046
  }).sort((a, b) => a.localeCompare(b)).join(",");
940
1047
  }
941
1048
  function loadSerializedPanelGroupState(autoSaveId, storage) {
@@ -969,31 +1076,145 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
969
1076
  }
970
1077
  }
971
1078
 
972
- const debounceMap = {};
1079
+ function shouldMonitorPixelBasedConstraints(constraints) {
1080
+ return constraints.some(constraints => {
1081
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1082
+ });
1083
+ }
973
1084
 
974
- // PanelGroup might be rendering in a server-side environment where localStorage is not available
975
- // or on a browser with cookies/storage disabled.
976
- // In either case, this function avoids accessing localStorage until needed,
977
- // and avoids throwing user-visible errors.
978
- function initializeDefaultStorage(storageObject) {
979
- try {
980
- if (typeof localStorage !== "undefined") {
981
- // Bypass this check for future calls
982
- storageObject.getItem = name => {
983
- return localStorage.getItem(name);
984
- };
985
- storageObject.setItem = (name, value) => {
986
- localStorage.setItem(name, value);
987
- };
988
- } else {
989
- throw new Error("localStorage not supported in this environment");
1085
+ function validatePanelConstraints({
1086
+ groupSizePixels,
1087
+ panelConstraints,
1088
+ panelId,
1089
+ panelIndex
1090
+ }) {
1091
+ {
1092
+ const warnings = [];
1093
+ {
1094
+ const {
1095
+ collapsedSizePercentage,
1096
+ collapsedSizePixels,
1097
+ defaultSizePercentage,
1098
+ defaultSizePixels,
1099
+ maxSizePercentage,
1100
+ maxSizePixels,
1101
+ minSizePercentage,
1102
+ minSizePixels
1103
+ } = panelConstraints[panelIndex];
1104
+ const conflictingUnits = [];
1105
+ if (collapsedSizePercentage != null && collapsedSizePixels != null) {
1106
+ conflictingUnits.push("collapsed size");
1107
+ }
1108
+ if (defaultSizePercentage != null && defaultSizePixels != null) {
1109
+ conflictingUnits.push("default size");
1110
+ }
1111
+ if (maxSizePercentage != null && maxSizePixels != null) {
1112
+ conflictingUnits.push("max size");
1113
+ }
1114
+ if (minSizePercentage != null && minSizePixels != null) {
1115
+ conflictingUnits.push("min size");
1116
+ }
1117
+ if (conflictingUnits.length > 0) {
1118
+ warnings.push(`should not specify both percentage and pixel units for: ${conflictingUnits.join(", ")}`);
1119
+ }
1120
+ }
1121
+ {
1122
+ const {
1123
+ collapsedSizePercentage,
1124
+ defaultSizePercentage,
1125
+ maxSizePercentage,
1126
+ minSizePercentage
1127
+ } = computePercentagePanelConstraints(panelConstraints, panelIndex, groupSizePixels);
1128
+ if (minSizePercentage > maxSizePercentage) {
1129
+ warnings.push(`min size (${minSizePercentage}%) should not be greater than max size (${maxSizePercentage}%)`);
1130
+ }
1131
+ if (defaultSizePercentage != null) {
1132
+ if (defaultSizePercentage < 0) {
1133
+ warnings.push("default size should not be less than 0");
1134
+ } else if (defaultSizePercentage < minSizePercentage) {
1135
+ warnings.push("default size should not be less than min size");
1136
+ }
1137
+ if (defaultSizePercentage > 100) {
1138
+ warnings.push("default size should not be greater than 100");
1139
+ } else if (defaultSizePercentage > maxSizePercentage) {
1140
+ warnings.push("default size should not be greater than max size");
1141
+ }
1142
+ }
1143
+ if (collapsedSizePercentage > minSizePercentage) {
1144
+ warnings.push("collapsed size should not be greater than min size");
1145
+ }
1146
+ }
1147
+ if (warnings.length > 0) {
1148
+ const name = panelId != null ? `Panel "${panelId}"` : "Panel";
1149
+ console.warn(`${name} has an invalid configuration:\n\n${warnings.join("\n")}`);
1150
+ return false;
1151
+ }
1152
+ }
1153
+ return true;
1154
+ }
1155
+
1156
+ // All units must be in percentages; pixel values should be pre-converted
1157
+ function validatePanelGroupLayout({
1158
+ groupSizePixels,
1159
+ layout: prevLayout,
1160
+ panelConstraints
1161
+ }) {
1162
+ const nextLayout = [...prevLayout];
1163
+
1164
+ // Validate layout expectations
1165
+ if (nextLayout.length !== panelConstraints.length) {
1166
+ throw Error(`Invalid ${panelConstraints.length} panel layout: ${nextLayout.map(size => `${size}%`).join(", ")}`);
1167
+ } else if (!fuzzyNumbersEqual(nextLayout.reduce((accumulated, current) => accumulated + current, 0), 100)) {
1168
+ // This is not ideal so we should warn about it, but it may be recoverable in some cases
1169
+ // (especially if the amount is small)
1170
+ {
1171
+ console.warn(`WARNING: Invalid layout total size: ${nextLayout.map(size => `${size}%`).join(", ")}`);
1172
+ }
1173
+ }
1174
+ let remainingSize = 0;
1175
+
1176
+ // First pass: Validate the proposed layout given each panel's constraints
1177
+ for (let index = 0; index < panelConstraints.length; index++) {
1178
+ const unsafeSize = nextLayout[index];
1179
+ const safeSize = resizePanel({
1180
+ groupSizePixels,
1181
+ panelConstraints,
1182
+ panelIndex: index,
1183
+ size: unsafeSize
1184
+ });
1185
+ if (unsafeSize != safeSize) {
1186
+ remainingSize += unsafeSize - safeSize;
1187
+ nextLayout[index] = safeSize;
1188
+ }
1189
+ }
1190
+
1191
+ // If there is additional, left over space, assign it to any panel(s) that permits it
1192
+ // (It's not worth taking multiple additional passes to evenly distribute)
1193
+ if (!fuzzyNumbersEqual(remainingSize, 0)) {
1194
+ for (let index = 0; index < panelConstraints.length; index++) {
1195
+ const prevSize = nextLayout[index];
1196
+ const unsafeSize = prevSize + remainingSize;
1197
+ const safeSize = resizePanel({
1198
+ groupSizePixels,
1199
+ panelConstraints,
1200
+ panelIndex: index,
1201
+ size: unsafeSize
1202
+ });
1203
+ if (prevSize !== safeSize) {
1204
+ remainingSize -= safeSize - prevSize;
1205
+ nextLayout[index] = safeSize;
1206
+
1207
+ // Once we've used up the remainder, bail
1208
+ if (fuzzyNumbersEqual(remainingSize, 0)) {
1209
+ break;
1210
+ }
1211
+ }
990
1212
  }
991
- } catch (error) {
992
- console.error(error);
993
- storageObject.getItem = () => null;
994
- storageObject.setItem = () => {};
995
1213
  }
1214
+ return nextLayout;
996
1215
  }
1216
+
1217
+ const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100;
997
1218
  const defaultStorage = {
998
1219
  getItem: name => {
999
1220
  initializeDefaultStorage(defaultStorage);
@@ -1004,150 +1225,113 @@ const defaultStorage = {
1004
1225
  defaultStorage.setItem(name, value);
1005
1226
  }
1006
1227
  };
1007
-
1008
- // Initial drag state serves a few purposes:
1009
- // * dragOffset:
1010
- // Resize is calculated by the distance between the current pointer event and the resize handle being "dragged"
1011
- // This value accounts for the initial offset when the touch/click starts, so the handle doesn't appear to "jump"
1012
- // * dragHandleRect, sizes:
1013
- // When resizing is done via mouse/touch event– some initial state is stored
1014
- // so that any panels that contract will also expand if drag direction is reversed.
1228
+ const debounceMap = {};
1015
1229
  function PanelGroupWithForwardedRef({
1016
1230
  autoSaveId,
1017
- children = null,
1231
+ children,
1018
1232
  className: classNameFromProps = "",
1019
1233
  direction,
1020
- disablePointerEventsDuringResize = false,
1021
1234
  forwardedRef,
1022
- id: idFromProps = null,
1023
- onLayout,
1235
+ id: idFromProps,
1236
+ onLayout = null,
1237
+ keyboardResizeByPercentage = null,
1238
+ keyboardResizeByPixels = null,
1024
1239
  storage = defaultStorage,
1025
- style: styleFromProps = {},
1026
- tagName: Type = "div",
1027
- units = "percentages"
1240
+ style: styleFromProps,
1241
+ tagName: Type = "div"
1028
1242
  }) {
1029
1243
  const groupId = useUniqueId(idFromProps);
1030
- const [activeHandleId, setActiveHandleId] = useState(null);
1031
- const [panels, setPanels] = useState(new Map());
1032
-
1033
- // When resizing is done via mouse/touch event–
1034
- // We store the initial Panel sizes in this ref, and apply move deltas to them instead of to the current sizes.
1035
- // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
1036
- const initialDragStateRef = useRef(null);
1037
- const devWarningsRef = useRef({
1038
- didLogDefaultSizeWarning: false,
1039
- didLogIdAndOrderWarning: false,
1040
- didLogInvalidLayoutWarning: false,
1041
- prevPanelIds: []
1042
- });
1043
-
1044
- // Use a ref to guard against users passing inline props
1045
- const callbacksRef = useRef({
1046
- onLayout
1047
- });
1048
- useEffect(() => {
1049
- callbacksRef.current.onLayout = onLayout;
1050
- });
1051
- const panelIdToLastNotifiedSizeMapRef = useRef({});
1052
-
1053
- // 0-1 values representing the relative size of each panel.
1054
- const [sizes, setSizes] = useState([]);
1055
-
1056
- // Used to support imperative collapse/expand API.
1057
- const panelSizeBeforeCollapse = useRef(new Map());
1244
+ const [dragState, setDragState] = useState(null);
1245
+ const [layout, setLayout] = useState([]);
1246
+ const [panelDataArray, setPanelDataArray] = useState([]);
1247
+ const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1248
+ const panelSizeBeforeCollapseRef = useRef(new Map());
1058
1249
  const prevDeltaRef = useRef(0);
1059
-
1060
- // Store committed values to avoid unnecessarily re-running memoization/effects functions.
1061
1250
  const committedValuesRef = useRef({
1062
1251
  direction,
1252
+ dragState,
1063
1253
  id: groupId,
1064
- panels,
1065
- sizes,
1066
- units
1254
+ keyboardResizeByPercentage,
1255
+ keyboardResizeByPixels,
1256
+ layout,
1257
+ onLayout,
1258
+ panelDataArray
1259
+ });
1260
+ const devWarningsRef = useRef({
1261
+ didLogIdAndOrderWarning: false,
1262
+ didLogPanelConstraintsWarning: false,
1263
+ prevPanelIds: []
1067
1264
  });
1068
1265
  useImperativeHandle(forwardedRef, () => ({
1069
- getId: () => groupId,
1070
- getLayout: unitsFromParams => {
1266
+ getId: () => committedValuesRef.current.id,
1267
+ getLayout: () => {
1071
1268
  const {
1072
- sizes,
1073
- units: unitsFromProps
1269
+ id: groupId,
1270
+ layout
1074
1271
  } = committedValuesRef.current;
1075
- const units = unitsFromParams ?? unitsFromProps;
1076
- if (units === "pixels") {
1077
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
1078
- return sizes.map(size => size / 100 * groupSizePixels);
1079
- } else {
1080
- return sizes;
1081
- }
1272
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1273
+ return layout.map(sizePercentage => {
1274
+ return {
1275
+ sizePercentage,
1276
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1277
+ };
1278
+ });
1082
1279
  },
1083
- setLayout: (sizes, unitsFromParams) => {
1280
+ setLayout: mixedSizes => {
1084
1281
  const {
1085
1282
  id: groupId,
1086
- panels,
1087
- sizes: prevSizes,
1088
- units
1283
+ layout: prevLayout,
1284
+ onLayout,
1285
+ panelDataArray
1089
1286
  } = committedValuesRef.current;
1090
- if ((unitsFromParams || units) === "pixels") {
1091
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
1092
- sizes = sizes.map(size => size / groupSizePixels * 100);
1093
- }
1094
- const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1095
- const panelsArray = panelsMapToSortedArray(panels);
1096
- const nextSizes = validatePanelGroupLayout({
1097
- groupId,
1098
- panels,
1099
- nextSizes: sizes,
1100
- prevSizes,
1101
- units
1287
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1288
+ const unsafeLayout = mixedSizes.map(mixedSize => getPercentageSizeFromMixedSizes(mixedSize, groupSizePixels));
1289
+ const safeLayout = validatePanelGroupLayout({
1290
+ groupSizePixels,
1291
+ layout: unsafeLayout,
1292
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1102
1293
  });
1103
- if (!areEqual(prevSizes, nextSizes)) {
1104
- setSizes(nextSizes);
1105
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1294
+ if (!areEqual(prevLayout, safeLayout)) {
1295
+ setLayout(safeLayout);
1296
+ if (onLayout) {
1297
+ onLayout(safeLayout.map(sizePercentage => ({
1298
+ sizePercentage,
1299
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1300
+ })));
1301
+ }
1302
+ callPanelCallbacks(groupId, panelDataArray, safeLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1106
1303
  }
1107
1304
  }
1108
- }), [groupId]);
1305
+ }), []);
1109
1306
  useIsomorphicLayoutEffect(() => {
1110
1307
  committedValuesRef.current.direction = direction;
1308
+ committedValuesRef.current.dragState = dragState;
1111
1309
  committedValuesRef.current.id = groupId;
1112
- committedValuesRef.current.panels = panels;
1113
- committedValuesRef.current.sizes = sizes;
1114
- committedValuesRef.current.units = units;
1310
+ committedValuesRef.current.layout = layout;
1311
+ committedValuesRef.current.onLayout = onLayout;
1312
+ committedValuesRef.current.panelDataArray = panelDataArray;
1115
1313
  });
1116
1314
  useWindowSplitterPanelGroupBehavior({
1117
1315
  committedValuesRef,
1118
1316
  groupId,
1119
- panels,
1120
- setSizes,
1121
- sizes,
1122
- panelSizeBeforeCollapse
1317
+ layout,
1318
+ panelDataArray,
1319
+ setLayout
1123
1320
  });
1124
-
1125
- // Notify external code when sizes have changed.
1126
1321
  useEffect(() => {
1127
- const {
1128
- onLayout
1129
- } = callbacksRef.current;
1130
- const {
1131
- panels,
1132
- sizes
1133
- } = committedValuesRef.current;
1322
+ // If this panel has been configured to persist sizing information, save sizes to local storage.
1323
+ if (autoSaveId) {
1324
+ if (layout.length === 0 || layout.length !== panelDataArray.length) {
1325
+ return;
1326
+ }
1134
1327
 
1135
- // Don't commit layout until all panels have registered and re-rendered with their actual sizes.
1136
- if (sizes.length > 0) {
1137
- if (onLayout) {
1138
- onLayout(sizes);
1328
+ // Limit the frequency of localStorage updates.
1329
+ if (!debounceMap[autoSaveId]) {
1330
+ debounceMap[autoSaveId] = debounce(savePanelGroupLayout, LOCAL_STORAGE_DEBOUNCE_INTERVAL);
1139
1331
  }
1140
- const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1141
-
1142
- // When possible, we notify before the next render so that rendering work can be batched together.
1143
- // Some cases are difficult to detect though,
1144
- // for example– panels that are conditionally rendered can affect the size of neighboring panels.
1145
- // In this case, the best we can do is notify on commit.
1146
- // The callPanelCallbacks() uses its own memoization to avoid notifying panels twice in these cases.
1147
- const panelsArray = panelsMapToSortedArray(panels);
1148
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1332
+ debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1149
1333
  }
1150
- }, [sizes]);
1334
+ }, [autoSaveId, layout, panelDataArray, storage]);
1151
1335
 
1152
1336
  // Once all panels have registered themselves,
1153
1337
  // Compute the initial sizes based on default weights.
@@ -1155,212 +1339,350 @@ function PanelGroupWithForwardedRef({
1155
1339
  useIsomorphicLayoutEffect(() => {
1156
1340
  const {
1157
1341
  id: groupId,
1158
- sizes,
1159
- units
1342
+ layout,
1343
+ onLayout
1160
1344
  } = committedValuesRef.current;
1161
- if (sizes.length === panels.size) {
1162
- // Only compute (or restore) default sizes once per panel configuration.
1345
+ if (layout.length === panelDataArray.length) {
1346
+ // Only compute (or restore) default layout once per panel configuration.
1163
1347
  return;
1164
1348
  }
1165
1349
 
1166
1350
  // If this panel has been configured to persist sizing information,
1167
1351
  // default size should be restored from local storage if possible.
1168
- let defaultSizes = null;
1352
+ let unsafeLayout = null;
1169
1353
  if (autoSaveId) {
1170
- const panelsArray = panelsMapToSortedArray(panels);
1171
- defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
1354
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1172
1355
  }
1173
- if (defaultSizes != null) {
1174
- // Validate saved sizes in case something has changed since last render
1175
- // e.g. for pixel groups, this could be the size of the window
1176
- const validatedSizes = validatePanelGroupLayout({
1177
- groupId,
1178
- panels,
1179
- nextSizes: defaultSizes,
1180
- prevSizes: defaultSizes,
1181
- units
1356
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1357
+ if (unsafeLayout == null) {
1358
+ unsafeLayout = calculateUnsafeDefaultLayout({
1359
+ groupSizePixels,
1360
+ panelDataArray
1182
1361
  });
1183
- setSizes(validatedSizes);
1362
+ }
1363
+
1364
+ // Validate even saved layouts in case something has changed since last render
1365
+ // e.g. for pixel groups, this could be the size of the window
1366
+ const validatedLayout = validatePanelGroupLayout({
1367
+ groupSizePixels,
1368
+ layout: unsafeLayout,
1369
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1370
+ });
1371
+ if (!areEqual(layout, validatedLayout)) {
1372
+ setLayout(validatedLayout);
1373
+ }
1374
+ if (onLayout) {
1375
+ onLayout(validatedLayout.map(sizePercentage => ({
1376
+ sizePercentage,
1377
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1378
+ })));
1379
+ }
1380
+ callPanelCallbacks(groupId, panelDataArray, validatedLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1381
+ }, [autoSaveId, layout, panelDataArray, storage]);
1382
+ useIsomorphicLayoutEffect(() => {
1383
+ const constraints = panelDataArray.map(({
1384
+ constraints
1385
+ }) => constraints);
1386
+ if (!shouldMonitorPixelBasedConstraints(constraints)) {
1387
+ // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
1388
+ return;
1389
+ }
1390
+ if (typeof ResizeObserver === "undefined") {
1391
+ console.warn(`WARNING: Pixel based constraints require ResizeObserver but it is not supported by the current browser.`);
1184
1392
  } else {
1185
- const sizes = calculateDefaultLayout({
1186
- groupId,
1187
- panels,
1188
- units
1393
+ const resizeObserver = new ResizeObserver(() => {
1394
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1395
+ const {
1396
+ layout: prevLayout,
1397
+ onLayout
1398
+ } = committedValuesRef.current;
1399
+ const nextLayout = validatePanelGroupLayout({
1400
+ groupSizePixels,
1401
+ layout: prevLayout,
1402
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1403
+ });
1404
+ if (!areEqual(prevLayout, nextLayout)) {
1405
+ setLayout(nextLayout);
1406
+ if (onLayout) {
1407
+ onLayout(nextLayout.map(sizePercentage => ({
1408
+ sizePercentage,
1409
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1410
+ })));
1411
+ }
1412
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1413
+ }
1189
1414
  });
1190
- setSizes(sizes);
1415
+ resizeObserver.observe(getPanelGroupElement(groupId));
1416
+ return () => {
1417
+ resizeObserver.disconnect();
1418
+ };
1191
1419
  }
1192
- }, [autoSaveId, panels, storage]);
1193
- useEffect(() => {
1194
- // If this panel has been configured to persist sizing information, save sizes to local storage.
1195
- if (autoSaveId) {
1196
- if (sizes.length === 0 || sizes.length !== panels.size) {
1197
- return;
1198
- }
1199
- const panelsArray = panelsMapToSortedArray(panels);
1420
+ }, [groupId, panelDataArray]);
1200
1421
 
1201
- // Limit the frequency of localStorage updates.
1202
- if (!debounceMap[autoSaveId]) {
1203
- debounceMap[autoSaveId] = debounce(savePanelGroupLayout, 100);
1204
- }
1205
- debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
1206
- }
1422
+ // DEV warnings
1423
+ useEffect(() => {
1207
1424
  {
1208
1425
  const {
1209
1426
  didLogIdAndOrderWarning,
1427
+ didLogPanelConstraintsWarning,
1210
1428
  prevPanelIds
1211
1429
  } = devWarningsRef.current;
1212
1430
  if (!didLogIdAndOrderWarning) {
1213
1431
  const {
1214
- panels
1432
+ panelDataArray
1215
1433
  } = committedValuesRef.current;
1216
- const panelIds = Array.from(panels.keys());
1434
+ const panelIds = panelDataArray.map(({
1435
+ id
1436
+ }) => id);
1217
1437
  devWarningsRef.current.prevPanelIds = panelIds;
1218
1438
  const panelsHaveChanged = prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
1219
1439
  if (panelsHaveChanged) {
1220
- if (Array.from(panels.values()).find(panel => panel.current.idWasAutoGenerated || panel.current.order == null)) {
1440
+ if (panelDataArray.find(({
1441
+ idIsFromProps,
1442
+ order
1443
+ }) => !idIsFromProps || order == null)) {
1221
1444
  devWarningsRef.current.didLogIdAndOrderWarning = true;
1222
1445
  console.warn(`WARNING: Panel id and order props recommended when panels are dynamically rendered`);
1223
1446
  }
1224
1447
  }
1225
1448
  }
1226
- }
1227
- }, [autoSaveId, panels, sizes, storage]);
1228
- useIsomorphicLayoutEffect(() => {
1229
- // Pixel panel constraints need to be reassessed after a group resize
1230
- // We can avoid the ResizeObserver overhead for relative layouts
1231
- if (units === "pixels") {
1232
- const resizeObserver = new ResizeObserver(() => {
1233
- const {
1234
- panels,
1235
- sizes: prevSizes
1236
- } = committedValuesRef.current;
1237
- const nextSizes = validatePanelGroupLayout({
1238
- groupId,
1239
- panels,
1240
- nextSizes: prevSizes,
1241
- prevSizes,
1242
- units
1243
- });
1244
- if (!areEqual(prevSizes, nextSizes)) {
1245
- setSizes(nextSizes);
1449
+ if (!didLogPanelConstraintsWarning) {
1450
+ const panelConstraints = panelDataArray.map(panelData => panelData.constraints);
1451
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1452
+ for (let panelIndex = 0; panelIndex < panelConstraints.length; panelIndex++) {
1453
+ const isValid = validatePanelConstraints({
1454
+ groupSizePixels,
1455
+ panelConstraints,
1456
+ panelId: panelDataArray[panelIndex].id,
1457
+ panelIndex
1458
+ });
1459
+ if (!isValid) {
1460
+ devWarningsRef.current.didLogPanelConstraintsWarning = true;
1461
+ break;
1462
+ }
1246
1463
  }
1247
- });
1248
- resizeObserver.observe(getPanelGroup(groupId));
1249
- return () => {
1250
- resizeObserver.disconnect();
1251
- };
1464
+ }
1252
1465
  }
1253
- }, [groupId, units]);
1254
- const getPanelSize = useCallback((id, unitsFromParams) => {
1466
+ });
1467
+
1468
+ // External APIs are safe to memoize via committed values ref
1469
+ const collapsePanel = useCallback(panelData => {
1255
1470
  const {
1256
- panels,
1257
- units: unitsFromProps
1471
+ layout: prevLayout,
1472
+ onLayout,
1473
+ panelDataArray
1258
1474
  } = committedValuesRef.current;
1259
- const panelsArray = panelsMapToSortedArray(panels);
1260
- const index = panelsArray.findIndex(panel => panel.current.id === id);
1261
- const size = sizes[index];
1262
- const units = unitsFromParams ?? unitsFromProps;
1263
- if (units === "pixels") {
1264
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
1265
- return size / 100 * groupSizePixels;
1266
- } else {
1267
- return size;
1475
+ if (panelData.constraints.collapsible) {
1476
+ const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1477
+ const {
1478
+ collapsedSizePercentage,
1479
+ panelSizePercentage,
1480
+ pivotIndices,
1481
+ groupSizePixels
1482
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
1483
+ if (panelSizePercentage !== collapsedSizePercentage) {
1484
+ // Store size before collapse;
1485
+ // This is the size that gets restored if the expand() API is used.
1486
+ panelSizeBeforeCollapseRef.current.set(panelData.id, panelSizePercentage);
1487
+ const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
1488
+ const delta = isLastPanel ? panelSizePercentage - collapsedSizePercentage : collapsedSizePercentage - panelSizePercentage;
1489
+ const nextLayout = adjustLayoutByDelta({
1490
+ delta,
1491
+ groupSizePixels,
1492
+ layout: prevLayout,
1493
+ panelConstraints: panelConstraintsArray,
1494
+ pivotIndices,
1495
+ trigger: "imperative-api"
1496
+ });
1497
+ if (!compareLayouts(prevLayout, nextLayout)) {
1498
+ setLayout(nextLayout);
1499
+ if (onLayout) {
1500
+ onLayout(nextLayout.map(sizePercentage => ({
1501
+ sizePercentage,
1502
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1503
+ })));
1504
+ }
1505
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1506
+ }
1507
+ }
1268
1508
  }
1269
- }, [groupId, sizes]);
1270
- const getPanelStyle = useCallback((id, defaultSize) => {
1509
+ }, [groupId]);
1510
+
1511
+ // External APIs are safe to memoize via committed values ref
1512
+ const expandPanel = useCallback(panelData => {
1271
1513
  const {
1272
- panels
1514
+ layout: prevLayout,
1515
+ onLayout,
1516
+ panelDataArray
1273
1517
  } = committedValuesRef.current;
1274
-
1275
- // Before mounting, Panels will not yet have registered themselves.
1276
- // This includes server rendering.
1277
- // At this point the best we can do is render everything with the same size.
1278
- if (panels.size === 0) {
1279
- {
1280
- if (!devWarningsRef.current.didLogDefaultSizeWarning) ;
1518
+ if (panelData.constraints.collapsible) {
1519
+ const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1520
+ const {
1521
+ collapsedSizePercentage,
1522
+ panelSizePercentage,
1523
+ minSizePercentage,
1524
+ pivotIndices,
1525
+ groupSizePixels
1526
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
1527
+ if (panelSizePercentage === collapsedSizePercentage) {
1528
+ // Restore this panel to the size it was before it was collapsed, if possible.
1529
+ const prevPanelSizePercentage = panelSizeBeforeCollapseRef.current.get(panelData.id);
1530
+ const baseSizePercentage = prevPanelSizePercentage != null ? prevPanelSizePercentage : minSizePercentage;
1531
+ const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
1532
+ const delta = isLastPanel ? panelSizePercentage - baseSizePercentage : baseSizePercentage - panelSizePercentage;
1533
+ const nextLayout = adjustLayoutByDelta({
1534
+ delta,
1535
+ groupSizePixels,
1536
+ layout: prevLayout,
1537
+ panelConstraints: panelConstraintsArray,
1538
+ pivotIndices,
1539
+ trigger: "imperative-api"
1540
+ });
1541
+ if (!compareLayouts(prevLayout, nextLayout)) {
1542
+ setLayout(nextLayout);
1543
+ if (onLayout) {
1544
+ onLayout(nextLayout.map(sizePercentage => ({
1545
+ sizePercentage,
1546
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1547
+ })));
1548
+ }
1549
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1550
+ }
1281
1551
  }
1282
- return {
1283
- flexBasis: 0,
1284
- flexGrow: defaultSize != null ? defaultSize : undefined,
1285
- flexShrink: 1,
1286
- // Without this, Panel sizes may be unintentionally overridden by their content.
1287
- overflow: "hidden"
1288
- };
1289
1552
  }
1290
- const flexGrow = getFlexGrow(panels, id, sizes);
1553
+ }, [groupId]);
1554
+
1555
+ // External APIs are safe to memoize via committed values ref
1556
+ const getPanelSize = useCallback(panelData => {
1557
+ const {
1558
+ layout,
1559
+ panelDataArray
1560
+ } = committedValuesRef.current;
1561
+ const {
1562
+ panelSizePercentage,
1563
+ panelSizePixels
1564
+ } = panelDataHelper(groupId, panelDataArray, panelData, layout);
1291
1565
  return {
1292
- flexBasis: 0,
1293
- flexGrow,
1294
- flexShrink: 1,
1295
- // Without this, Panel sizes may be unintentionally overridden by their content.
1296
- overflow: "hidden",
1297
- // Disable pointer events inside of a panel during resize.
1298
- // This avoid edge cases like nested iframes.
1299
- pointerEvents: disablePointerEventsDuringResize && activeHandleId !== null ? "none" : undefined
1566
+ sizePercentage: panelSizePercentage,
1567
+ sizePixels: panelSizePixels
1300
1568
  };
1301
- }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1302
- const registerPanel = useCallback((id, panelRef) => {
1569
+ }, [groupId]);
1570
+
1571
+ // This API should never read from committedValuesRef
1572
+ const getPanelStyle = useCallback(panelData => {
1573
+ const panelIndex = panelDataArray.indexOf(panelData);
1574
+ return computePanelFlexBoxStyle({
1575
+ dragState,
1576
+ layout,
1577
+ panelData: panelDataArray,
1578
+ panelIndex
1579
+ });
1580
+ }, [dragState, layout, panelDataArray]);
1581
+
1582
+ // External APIs are safe to memoize via committed values ref
1583
+ const isPanelCollapsed = useCallback(panelData => {
1303
1584
  const {
1304
- units
1585
+ layout,
1586
+ panelDataArray
1305
1587
  } = committedValuesRef.current;
1306
- validatePanelProps(units, panelRef);
1307
- setPanels(prevPanels => {
1308
- if (prevPanels.has(id)) {
1309
- return prevPanels;
1310
- }
1311
- const nextPanels = new Map(prevPanels);
1312
- nextPanels.set(id, panelRef);
1313
- return nextPanels;
1588
+ const {
1589
+ collapsedSizePercentage,
1590
+ collapsible,
1591
+ panelSizePercentage
1592
+ } = panelDataHelper(groupId, panelDataArray, panelData, layout);
1593
+ return collapsible === true && panelSizePercentage === collapsedSizePercentage;
1594
+ }, [groupId]);
1595
+
1596
+ // External APIs are safe to memoize via committed values ref
1597
+ const isPanelExpanded = useCallback(panelData => {
1598
+ const {
1599
+ layout,
1600
+ panelDataArray
1601
+ } = committedValuesRef.current;
1602
+ const {
1603
+ collapsedSizePercentage,
1604
+ collapsible,
1605
+ panelSizePercentage
1606
+ } = panelDataHelper(groupId, panelDataArray, panelData, layout);
1607
+ return !collapsible || panelSizePercentage > collapsedSizePercentage;
1608
+ }, [groupId]);
1609
+ const registerPanel = useCallback(panelData => {
1610
+ setPanelDataArray(prevPanelDataArray => {
1611
+ const nextPanelDataArray = [...prevPanelDataArray, panelData];
1612
+ return nextPanelDataArray.sort((panelA, panelB) => {
1613
+ const orderA = panelA.order;
1614
+ const orderB = panelB.order;
1615
+ if (orderA == null && orderB == null) {
1616
+ return 0;
1617
+ } else if (orderA == null) {
1618
+ return -1;
1619
+ } else if (orderB == null) {
1620
+ return 1;
1621
+ } else {
1622
+ return orderA - orderB;
1623
+ }
1624
+ });
1314
1625
  });
1315
1626
  }, []);
1316
- const registerResizeHandle = useCallback(handleId => {
1317
- const resizeHandler = event => {
1627
+ const registerResizeHandle = useCallback(dragHandleId => {
1628
+ return function resizeHandler(event) {
1318
1629
  event.preventDefault();
1319
1630
  const {
1320
1631
  direction,
1321
- panels,
1322
- sizes: prevSizes
1632
+ dragState,
1633
+ id: groupId,
1634
+ keyboardResizeByPercentage,
1635
+ keyboardResizeByPixels,
1636
+ onLayout,
1637
+ panelDataArray,
1638
+ layout: prevLayout
1323
1639
  } = committedValuesRef.current;
1324
- const panelsArray = panelsMapToSortedArray(panels);
1325
- const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
1326
- if (idBefore == null || idAfter == null) {
1327
- return;
1328
- }
1329
- let movement = getMovement(event, groupId, handleId, panelsArray, direction, prevSizes, initialDragStateRef.current);
1330
- if (movement === 0) {
1640
+ const {
1641
+ initialLayout
1642
+ } = dragState ?? {};
1643
+ const pivotIndices = determinePivotIndices(groupId, dragHandleId);
1644
+ let delta = calculateDeltaPercentage(event, groupId, dragHandleId, direction, dragState, {
1645
+ percentage: keyboardResizeByPercentage,
1646
+ pixels: keyboardResizeByPixels
1647
+ });
1648
+ if (delta === 0) {
1331
1649
  return;
1332
1650
  }
1333
- const groupElement = getPanelGroup(groupId);
1334
- const rect = groupElement.getBoundingClientRect();
1335
- const isHorizontal = direction === "horizontal";
1336
1651
 
1337
1652
  // Support RTL layouts
1653
+ const isHorizontal = direction === "horizontal";
1338
1654
  if (document.dir === "rtl" && isHorizontal) {
1339
- movement = -movement;
1655
+ delta = -delta;
1340
1656
  }
1341
- const size = isHorizontal ? rect.width : rect.height;
1342
- const delta = movement / size * 100;
1343
-
1344
- // If a validateLayout method has been provided
1345
- // it's important to use it before updating the mouse cursor
1346
- const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1347
- const sizesChanged = !areEqual(prevSizes, nextSizes);
1657
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1658
+ const panelConstraints = panelDataArray.map(panelData => panelData.constraints);
1659
+ const nextLayout = adjustLayoutByDelta({
1660
+ delta,
1661
+ groupSizePixels,
1662
+ layout: initialLayout ?? prevLayout,
1663
+ panelConstraints,
1664
+ pivotIndices,
1665
+ trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch"
1666
+ });
1667
+ const layoutChanged = !compareLayouts(prevLayout, nextLayout);
1348
1668
 
1349
- // Don't update cursor for resizes triggered by keyboard interactions.
1669
+ // Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
1670
+ // Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
1350
1671
  if (isMouseEvent(event) || isTouchEvent(event)) {
1351
1672
  // Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
1352
1673
  // In this case, Panel sizes might not change–
1353
1674
  // but updating cursor in this scenario would cause a flicker.
1354
1675
  if (prevDeltaRef.current != delta) {
1355
- if (!sizesChanged) {
1676
+ prevDeltaRef.current = delta;
1677
+ if (!layoutChanged) {
1356
1678
  // If the pointer has moved too far to resize the panel any further,
1357
1679
  // update the cursor style for a visual clue.
1358
1680
  // This mimics VS Code behavior.
1359
1681
 
1360
1682
  if (isHorizontal) {
1361
- setGlobalCursorStyle(movement < 0 ? "horizontal-min" : "horizontal-max");
1683
+ setGlobalCursorStyle(delta < 0 ? "horizontal-min" : "horizontal-max");
1362
1684
  } else {
1363
- setGlobalCursorStyle(movement < 0 ? "vertical-min" : "vertical-max");
1685
+ setGlobalCursorStyle(delta < 0 ? "vertical-min" : "vertical-max");
1364
1686
  }
1365
1687
  } else {
1366
1688
  // Reset the cursor style to the the normal resize cursor.
@@ -1368,208 +1690,100 @@ function PanelGroupWithForwardedRef({
1368
1690
  }
1369
1691
  }
1370
1692
  }
1371
- if (sizesChanged) {
1372
- const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1373
-
1374
- // It's okay to bypass in this case because we already validated above
1375
- setSizes(nextSizes);
1376
-
1377
- // If resize change handlers have been declared, this is the time to call them.
1378
- // Trigger user callbacks after updating state, so that user code can override the sizes.
1379
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1693
+ if (layoutChanged) {
1694
+ setLayout(nextLayout);
1695
+ if (onLayout) {
1696
+ onLayout(nextLayout.map(sizePercentage => ({
1697
+ sizePercentage,
1698
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1699
+ })));
1700
+ }
1701
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1380
1702
  }
1381
- prevDeltaRef.current = delta;
1382
1703
  };
1383
- return resizeHandler;
1384
- }, [groupId]);
1385
- const unregisterPanel = useCallback(id => {
1386
- setPanels(prevPanels => {
1387
- if (!prevPanels.has(id)) {
1388
- return prevPanels;
1389
- }
1390
- const nextPanels = new Map(prevPanels);
1391
- nextPanels.delete(id);
1392
- return nextPanels;
1393
- });
1394
1704
  }, []);
1395
- const collapsePanel = useCallback(id => {
1705
+
1706
+ // External APIs are safe to memoize via committed values ref
1707
+ const resizePanel = useCallback((panelData, mixedSizes) => {
1396
1708
  const {
1397
- panels,
1398
- sizes: prevSizes
1709
+ layout: prevLayout,
1710
+ onLayout,
1711
+ panelDataArray
1399
1712
  } = committedValuesRef.current;
1400
- const panel = panels.get(id);
1401
- if (panel == null) {
1402
- return;
1403
- }
1713
+ const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1404
1714
  const {
1405
- collapsedSize,
1406
- collapsible
1407
- } = panel.current;
1408
- if (!collapsible) {
1409
- return;
1410
- }
1411
- const panelsArray = panelsMapToSortedArray(panels);
1412
- const index = panelsArray.indexOf(panel);
1413
- if (index < 0) {
1414
- return;
1415
- }
1416
- const currentSize = prevSizes[index];
1417
- if (currentSize === collapsedSize) {
1418
- // Panel is already collapsed.
1419
- return;
1420
- }
1421
- panelSizeBeforeCollapse.current.set(id, currentSize);
1422
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1423
- if (idBefore == null || idAfter == null) {
1424
- return;
1425
- }
1426
- const isLastPanel = index === panelsArray.length - 1;
1427
- const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1428
- const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1429
- if (prevSizes !== nextSizes) {
1430
- const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1431
- setSizes(nextSizes);
1432
-
1433
- // If resize change handlers have been declared, this is the time to call them.
1434
- // Trigger user callbacks after updating state, so that user code can override the sizes.
1435
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1715
+ groupSizePixels,
1716
+ panelSizePercentage,
1717
+ pivotIndices
1718
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
1719
+ const sizePercentage = getPercentageSizeFromMixedSizes(mixedSizes, groupSizePixels);
1720
+ const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
1721
+ const delta = isLastPanel ? panelSizePercentage - sizePercentage : sizePercentage - panelSizePercentage;
1722
+ const nextLayout = adjustLayoutByDelta({
1723
+ delta,
1724
+ groupSizePixels,
1725
+ layout: prevLayout,
1726
+ panelConstraints: panelConstraintsArray,
1727
+ pivotIndices,
1728
+ trigger: "imperative-api"
1729
+ });
1730
+ if (!compareLayouts(prevLayout, nextLayout)) {
1731
+ setLayout(nextLayout);
1732
+ if (onLayout) {
1733
+ onLayout(nextLayout.map(sizePercentage => ({
1734
+ sizePercentage,
1735
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1736
+ })));
1737
+ }
1738
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1436
1739
  }
1437
- }, []);
1438
- const expandPanel = useCallback(id => {
1740
+ }, [groupId]);
1741
+ const startDragging = useCallback((dragHandleId, event) => {
1439
1742
  const {
1440
- panels,
1441
- sizes: prevSizes
1743
+ direction,
1744
+ layout
1442
1745
  } = committedValuesRef.current;
1443
- const panel = panels.get(id);
1444
- if (panel == null) {
1445
- return;
1446
- }
1447
- const {
1448
- collapsedSize,
1449
- minSize
1450
- } = panel.current;
1451
- const sizeBeforeCollapse = panelSizeBeforeCollapse.current.get(id) || minSize;
1452
- if (!sizeBeforeCollapse) {
1453
- return;
1454
- }
1455
- const panelsArray = panelsMapToSortedArray(panels);
1456
- const index = panelsArray.indexOf(panel);
1457
- if (index < 0) {
1458
- return;
1459
- }
1460
- const currentSize = prevSizes[index];
1461
- if (currentSize !== collapsedSize) {
1462
- // Panel is already expanded.
1463
- return;
1464
- }
1465
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1466
- if (idBefore == null || idAfter == null) {
1467
- return;
1468
- }
1469
- const isLastPanel = index === panelsArray.length - 1;
1470
- const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1471
- const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1472
- if (prevSizes !== nextSizes) {
1473
- const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1474
- setSizes(nextSizes);
1475
-
1476
- // If resize change handlers have been declared, this is the time to call them.
1477
- // Trigger user callbacks after updating state, so that user code can override the sizes.
1478
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1479
- }
1746
+ const handleElement = getResizeHandleElement(dragHandleId);
1747
+ const initialCursorPosition = getResizeEventCursorPosition(direction, event);
1748
+ setDragState({
1749
+ dragHandleId,
1750
+ dragHandleRect: handleElement.getBoundingClientRect(),
1751
+ initialCursorPosition,
1752
+ initialLayout: layout
1753
+ });
1480
1754
  }, []);
1481
- const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1482
- const {
1483
- id: groupId,
1484
- panels,
1485
- sizes: prevSizes,
1486
- units
1487
- } = committedValuesRef.current;
1488
- if ((unitsFromParams || units) === "pixels") {
1489
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
1490
- nextSize = nextSize / groupSizePixels * 100;
1491
- }
1492
- const panel = panels.get(id);
1493
- if (panel == null) {
1494
- return;
1495
- }
1496
- let {
1497
- collapsedSize,
1498
- collapsible,
1499
- maxSize,
1500
- minSize
1501
- } = panel.current;
1502
- if (units === "pixels") {
1503
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
1504
- minSize = minSize / groupSizePixels * 100;
1505
- if (maxSize != null) {
1506
- maxSize = maxSize / groupSizePixels * 100;
1507
- }
1508
- }
1509
- const panelsArray = panelsMapToSortedArray(panels);
1510
- const index = panelsArray.indexOf(panel);
1511
- if (index < 0) {
1512
- return;
1513
- }
1514
- const currentSize = prevSizes[index];
1515
- if (currentSize === nextSize) {
1516
- return;
1517
- }
1518
- if (collapsible && nextSize === collapsedSize) ; else {
1519
- const unsafeNextSize = nextSize;
1520
- nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1521
- {
1522
- if (unsafeNextSize !== nextSize) {
1523
- console.error(`Invalid size (${unsafeNextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
1524
- }
1755
+ const stopDragging = useCallback(() => {
1756
+ resetGlobalCursorStyle();
1757
+ setDragState(null);
1758
+ }, []);
1759
+ const unregisterPanel = useCallback(panelData => {
1760
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1761
+ setPanelDataArray(panelDataArray => {
1762
+ const index = panelDataArray.indexOf(panelData);
1763
+ if (index >= 0) {
1764
+ panelDataArray = [...panelDataArray];
1765
+ panelDataArray.splice(index, 1);
1525
1766
  }
1526
- }
1527
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1528
- if (idBefore == null || idAfter == null) {
1529
- return;
1530
- }
1531
- const isLastPanel = index === panelsArray.length - 1;
1532
- const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1533
- const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1534
- if (prevSizes !== nextSizes) {
1535
- const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1536
- setSizes(nextSizes);
1537
-
1538
- // If resize change handlers have been declared, this is the time to call them.
1539
- // Trigger user callbacks after updating state, so that user code can override the sizes.
1540
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1541
- }
1767
+ return panelDataArray;
1768
+ });
1542
1769
  }, []);
1543
1770
  const context = useMemo(() => ({
1544
- activeHandleId,
1545
1771
  collapsePanel,
1546
1772
  direction,
1773
+ dragState,
1547
1774
  expandPanel,
1548
1775
  getPanelSize,
1549
1776
  getPanelStyle,
1550
1777
  groupId,
1778
+ isPanelCollapsed,
1779
+ isPanelExpanded,
1551
1780
  registerPanel,
1552
1781
  registerResizeHandle,
1553
1782
  resizePanel,
1554
- startDragging: (id, event) => {
1555
- setActiveHandleId(id);
1556
- if (isMouseEvent(event) || isTouchEvent(event)) {
1557
- const handleElement = getResizeHandle(id);
1558
- initialDragStateRef.current = {
1559
- dragHandleRect: handleElement.getBoundingClientRect(),
1560
- dragOffset: getDragOffset(event, id, direction),
1561
- sizes: committedValuesRef.current.sizes
1562
- };
1563
- }
1564
- },
1565
- stopDragging: () => {
1566
- resetGlobalCursorStyle();
1567
- setActiveHandleId(null);
1568
- initialDragStateRef.current = null;
1569
- },
1570
- units,
1783
+ startDragging,
1784
+ stopDragging,
1571
1785
  unregisterPanel
1572
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1786
+ }), [collapsePanel, dragState, direction, expandPanel, getPanelSize, getPanelStyle, groupId, isPanelCollapsed, isPanelExpanded, registerPanel, registerResizeHandle, resizePanel, startDragging, stopDragging, unregisterPanel]);
1573
1787
  const style = {
1574
1788
  display: "flex",
1575
1789
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1578,20 +1792,20 @@ function PanelGroupWithForwardedRef({
1578
1792
  width: "100%"
1579
1793
  };
1580
1794
  return createElement(PanelGroupContext.Provider, {
1581
- children: createElement(Type, {
1582
- children,
1583
- className: classNameFromProps,
1584
- "data-panel-group": "",
1585
- "data-panel-group-direction": direction,
1586
- "data-panel-group-id": groupId,
1587
- "data-panel-group-units": units,
1588
- style: {
1589
- ...style,
1590
- ...styleFromProps
1591
- }
1592
- }),
1593
1795
  value: context
1594
- });
1796
+ }, createElement(Type, {
1797
+ children,
1798
+ className: classNameFromProps,
1799
+ style: {
1800
+ ...style,
1801
+ ...styleFromProps
1802
+ },
1803
+ // CSS selectors
1804
+ "data-panel-group": "",
1805
+ // e2e test attributes
1806
+ "data-panel-group-direction": direction ,
1807
+ "data-panel-group-id": groupId
1808
+ }));
1595
1809
  }
1596
1810
  const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwardedRef, {
1597
1811
  ...props,
@@ -1599,6 +1813,77 @@ const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwar
1599
1813
  }));
1600
1814
  PanelGroupWithForwardedRef.displayName = "PanelGroup";
1601
1815
  PanelGroup.displayName = "forwardRef(PanelGroup)";
1816
+ function panelDataHelper(groupId, panelDataArray, panelData, layout) {
1817
+ const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1818
+ const panelIndex = panelDataArray.indexOf(panelData);
1819
+ const panelConstraints = panelConstraintsArray[panelIndex];
1820
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1821
+ const percentagePanelConstraints = computePercentagePanelConstraints(panelConstraintsArray, panelIndex, groupSizePixels);
1822
+ const isLastPanel = panelIndex === panelDataArray.length - 1;
1823
+ const pivotIndices = isLastPanel ? [panelIndex - 1, panelIndex] : [panelIndex, panelIndex + 1];
1824
+ const panelSizePercentage = layout[panelIndex];
1825
+ const panelSizePixels = convertPercentageToPixels(panelSizePercentage, groupSizePixels);
1826
+ return {
1827
+ ...percentagePanelConstraints,
1828
+ collapsible: panelConstraints.collapsible,
1829
+ panelSizePercentage,
1830
+ panelSizePixels,
1831
+ groupSizePixels,
1832
+ pivotIndices
1833
+ };
1834
+ }
1835
+
1836
+ // https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
1837
+
1838
+ function useWindowSplitterResizeHandlerBehavior({
1839
+ disabled,
1840
+ handleId,
1841
+ resizeHandler
1842
+ }) {
1843
+ useEffect(() => {
1844
+ if (disabled || resizeHandler == null) {
1845
+ return;
1846
+ }
1847
+ const handleElement = getResizeHandleElement(handleId);
1848
+ if (handleElement == null) {
1849
+ return;
1850
+ }
1851
+ const onKeyDown = event => {
1852
+ if (event.defaultPrevented) {
1853
+ return;
1854
+ }
1855
+ switch (event.key) {
1856
+ case "ArrowDown":
1857
+ case "ArrowLeft":
1858
+ case "ArrowRight":
1859
+ case "ArrowUp":
1860
+ case "End":
1861
+ case "Home":
1862
+ {
1863
+ event.preventDefault();
1864
+ resizeHandler(event);
1865
+ break;
1866
+ }
1867
+ case "F6":
1868
+ {
1869
+ event.preventDefault();
1870
+ const groupId = handleElement.getAttribute("data-panel-group-id");
1871
+ const handles = getResizeHandleElementsForGroup(groupId);
1872
+ const index = getResizeHandleElementIndex(groupId, handleId);
1873
+ assert(index !== null);
1874
+ const nextIndex = event.shiftKey ? index > 0 ? index - 1 : handles.length - 1 : index + 1 < handles.length ? index + 1 : 0;
1875
+ const nextHandle = handles[nextIndex];
1876
+ nextHandle.focus();
1877
+ break;
1878
+ }
1879
+ }
1880
+ };
1881
+ handleElement.addEventListener("keydown", onKeyDown);
1882
+ return () => {
1883
+ handleElement.removeEventListener("keydown", onKeyDown);
1884
+ };
1885
+ }, [disabled, handleId, resizeHandler]);
1886
+ }
1602
1887
 
1603
1888
  function PanelResizeHandle({
1604
1889
  children = null,
@@ -1623,15 +1908,15 @@ function PanelResizeHandle({
1623
1908
  throw Error(`PanelResizeHandle components must be rendered within a PanelGroup container`);
1624
1909
  }
1625
1910
  const {
1626
- activeHandleId,
1627
1911
  direction,
1912
+ dragState,
1628
1913
  groupId,
1629
1914
  registerResizeHandle,
1630
1915
  startDragging,
1631
1916
  stopDragging
1632
1917
  } = panelGroupContext;
1633
1918
  const resizeHandleId = useUniqueId(idFromProps);
1634
- const isDragging = activeHandleId === resizeHandleId;
1919
+ const isDragging = dragState?.dragHandleId === resizeHandleId;
1635
1920
  const [isFocused, setIsFocused] = useState(false);
1636
1921
  const [resizeHandler, setResizeHandler] = useState(null);
1637
1922
  const stopDraggingAndBlur = useCallback(() => {
@@ -1695,6 +1980,8 @@ function PanelResizeHandle({
1695
1980
  return createElement(Type, {
1696
1981
  children,
1697
1982
  className: classNameFromProps,
1983
+ // CSS selectors
1984
+ "data-resize-handle": "",
1698
1985
  "data-resize-handle-active": isDragging ? "pointer" : isFocused ? "keyboard" : undefined,
1699
1986
  "data-panel-group-direction": direction,
1700
1987
  "data-panel-group-id": groupId,
@@ -1734,4 +2021,4 @@ function PanelResizeHandle({
1734
2021
  }
1735
2022
  PanelResizeHandle.displayName = "PanelResizeHandle";
1736
2023
 
1737
- export { Panel, PanelGroup, PanelResizeHandle, getAvailableGroupSizePixels };
2024
+ export { Panel, PanelGroup, PanelResizeHandle };