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
package/src/PanelGroup.ts CHANGED
@@ -1,12 +1,40 @@
1
- import { isBrowser } from "#is-browser";
2
1
  import { isDevelopment } from "#is-development";
2
+ import { PanelData } from "./Panel";
3
+ import { DragState, PanelGroupContext, ResizeEvent } from "./PanelGroupContext";
4
+ import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
5
+ import useUniqueId from "./hooks/useUniqueId";
6
+ import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterPanelGroupBehavior";
7
+ import { Direction, MixedSizes } from "./types";
8
+ import { adjustLayoutByDelta } from "./utils/adjustLayoutByDelta";
9
+ import { areEqual } from "./utils/arrays";
10
+ import { calculateDeltaPercentage } from "./utils/calculateDeltaPercentage";
11
+ import { calculateUnsafeDefaultLayout } from "./utils/calculateUnsafeDefaultLayout";
12
+ import { callPanelCallbacks } from "./utils/callPanelCallbacks";
13
+ import { compareLayouts } from "./utils/compareLayouts";
14
+ import { computePanelFlexBoxStyle } from "./utils/computePanelFlexBoxStyle";
15
+ import { computePercentagePanelConstraints } from "./utils/computePercentagePanelConstraints";
16
+ import { convertPercentageToPixels } from "./utils/convertPercentageToPixels";
17
+ import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
18
+ import debounce from "./utils/debounce";
19
+ import { determinePivotIndices } from "./utils/determinePivotIndices";
20
+ import { calculateAvailablePanelSizeInPixels } from "./utils/dom/calculateAvailablePanelSizeInPixels";
21
+ import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement";
22
+ import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
23
+ import { isKeyDown, isMouseEvent, isTouchEvent } from "./utils/events";
24
+ import { getPercentageSizeFromMixedSizes } from "./utils/getPercentageSizeFromMixedSizes";
25
+ import { getResizeEventCursorPosition } from "./utils/getResizeEventCursorPosition";
26
+ import { initializeDefaultStorage } from "./utils/initializeDefaultStorage";
27
+ import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization";
28
+ import { shouldMonitorPixelBasedConstraints } from "./utils/shouldMonitorPixelBasedConstraints";
29
+ import { validatePanelConstraints } from "./utils/validatePanelConstraints";
30
+ import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout";
3
31
  import {
4
- createElement,
5
32
  CSSProperties,
6
33
  ElementType,
7
34
  ForwardedRef,
35
+ PropsWithChildren,
36
+ createElement,
8
37
  forwardRef,
9
- ReactNode,
10
38
  useCallback,
11
39
  useEffect,
12
40
  useImperativeHandle,
@@ -15,76 +43,20 @@ import {
15
43
  useState,
16
44
  } from "./vendor/react";
17
45
 
18
- import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
19
- import useUniqueId from "./hooks/useUniqueId";
20
- import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterBehavior";
21
- import { PanelGroupContext } from "./PanelContexts";
22
- import {
23
- Direction,
24
- PanelData,
25
- PanelGroupOnLayout,
26
- PanelGroupStorage,
27
- ResizeEvent,
28
- Units,
29
- } from "./types";
30
- import { areEqual } from "./utils/arrays";
31
- import {
32
- getDragOffset,
33
- getMovement,
34
- isMouseEvent,
35
- isTouchEvent,
36
- } from "./utils/coordinates";
37
- import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
38
- import debounce from "./utils/debounce";
39
- import {
40
- adjustByDelta,
41
- calculateDefaultLayout,
42
- callPanelCallbacks,
43
- getAvailableGroupSizePixels,
44
- getBeforeAndAfterIds,
45
- getFlexGrow,
46
- getPanelGroup,
47
- getResizeHandle,
48
- getResizeHandlePanelIds,
49
- panelsMapToSortedArray,
50
- validatePanelGroupLayout,
51
- validatePanelProps,
52
- } from "./utils/group";
53
- import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization";
46
+ const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100;
54
47
 
55
- const debounceMap: {
56
- [key: string]: (
57
- autoSaveId: string,
58
- panels: PanelData[],
59
- sizes: number[],
60
- storage: PanelGroupStorage
61
- ) => void;
62
- } = {};
48
+ export type ImperativePanelGroupHandle = {
49
+ getId: () => string;
50
+ getLayout: () => MixedSizes[];
51
+ setLayout: (layout: Partial<MixedSizes>[]) => void;
52
+ };
63
53
 
64
- // PanelGroup might be rendering in a server-side environment where localStorage is not available
65
- // or on a browser with cookies/storage disabled.
66
- // In either case, this function avoids accessing localStorage until needed,
67
- // and avoids throwing user-visible errors.
68
- function initializeDefaultStorage(storageObject: PanelGroupStorage) {
69
- try {
70
- if (typeof localStorage !== "undefined") {
71
- // Bypass this check for future calls
72
- storageObject.getItem = (name: string) => {
73
- return localStorage.getItem(name);
74
- };
75
- storageObject.setItem = (name: string, value: string) => {
76
- localStorage.setItem(name, value);
77
- };
78
- } else {
79
- throw new Error("localStorage not supported in this environment");
80
- }
81
- } catch (error) {
82
- console.error(error);
54
+ export type PanelGroupStorage = {
55
+ getItem(name: string): string | null;
56
+ setItem(name: string, value: string): void;
57
+ };
83
58
 
84
- storageObject.getItem = () => null;
85
- storageObject.setItem = () => {};
86
- }
87
- }
59
+ export type PanelGroupOnLayout = (layout: MixedSizes[]) => void;
88
60
 
89
61
  const defaultStorage: PanelGroupStorage = {
90
62
  getItem: (name: string) => {
@@ -97,271 +69,315 @@ const defaultStorage: PanelGroupStorage = {
97
69
  },
98
70
  };
99
71
 
100
- export type CommittedValues = {
101
- direction: Direction;
102
- id: string;
103
- panels: Map<string, PanelData>;
104
- sizes: number[];
105
- units: Units;
106
- };
107
-
108
- export type PanelDataMap = Map<string, PanelData>;
109
-
110
- // Initial drag state serves a few purposes:
111
- // * dragOffset:
112
- // Resize is calculated by the distance between the current pointer event and the resize handle being "dragged"
113
- // This value accounts for the initial offset when the touch/click starts, so the handle doesn't appear to "jump"
114
- // * dragHandleRect, sizes:
115
- // When resizing is done via mouse/touch event– some initial state is stored
116
- // so that any panels that contract will also expand if drag direction is reversed.
117
- export type InitialDragState = {
118
- dragHandleRect: DOMRect;
119
- dragOffset: number;
120
- sizes: number[];
121
- };
122
-
123
- export type PanelGroupProps = {
72
+ export type PanelGroupProps = PropsWithChildren<{
124
73
  autoSaveId?: string;
125
- children?: ReactNode;
126
74
  className?: string;
127
75
  direction: Direction;
128
- disablePointerEventsDuringResize?: boolean;
129
76
  id?: string | null;
130
- onLayout?: PanelGroupOnLayout;
77
+ keyboardResizeByPercentage?: number | null;
78
+ keyboardResizeByPixels?: number | null;
79
+ onLayout?: PanelGroupOnLayout | null;
131
80
  storage?: PanelGroupStorage;
132
81
  style?: CSSProperties;
133
82
  tagName?: ElementType;
134
- units?: Units;
135
- };
83
+ }>;
136
84
 
137
- export type ImperativePanelGroupHandle = {
138
- getId: () => string;
139
- getLayout: (units?: Units) => number[];
140
- setLayout: (panelSizes: number[], units?: Units) => void;
141
- };
85
+ const debounceMap: {
86
+ [key: string]: typeof savePanelGroupLayout;
87
+ } = {};
142
88
 
143
89
  function PanelGroupWithForwardedRef({
144
90
  autoSaveId,
145
- children = null,
91
+ children,
146
92
  className: classNameFromProps = "",
147
93
  direction,
148
- disablePointerEventsDuringResize = false,
149
94
  forwardedRef,
150
- id: idFromProps = null,
151
- onLayout,
95
+ id: idFromProps,
96
+ onLayout = null,
97
+ keyboardResizeByPercentage = null,
98
+ keyboardResizeByPixels = null,
152
99
  storage = defaultStorage,
153
- style: styleFromProps = {},
100
+ style: styleFromProps,
154
101
  tagName: Type = "div",
155
- units = "percentages",
156
102
  }: PanelGroupProps & {
157
103
  forwardedRef: ForwardedRef<ImperativePanelGroupHandle>;
158
104
  }) {
159
105
  const groupId = useUniqueId(idFromProps);
160
106
 
161
- const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
162
- const [panels, setPanels] = useState<PanelDataMap>(new Map());
107
+ const [dragState, setDragState] = useState<DragState | null>(null);
108
+ const [layout, setLayout] = useState<number[]>([]);
109
+ const [panelDataArray, setPanelDataArray] = useState<PanelData[]>([]);
110
+
111
+ const panelIdToLastNotifiedMixedSizesMapRef = useRef<
112
+ Record<string, MixedSizes>
113
+ >({});
114
+ const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
115
+ const prevDeltaRef = useRef<number>(0);
163
116
 
164
- // When resizing is done via mouse/touch event–
165
- // We store the initial Panel sizes in this ref, and apply move deltas to them instead of to the current sizes.
166
- // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
167
- const initialDragStateRef = useRef<InitialDragState | null>(null);
117
+ const committedValuesRef = useRef<{
118
+ direction: Direction;
119
+ dragState: DragState | null;
120
+ id: string;
121
+ keyboardResizeByPercentage: number | null;
122
+ keyboardResizeByPixels: number | null;
123
+ layout: number[];
124
+ onLayout: PanelGroupOnLayout | null;
125
+ panelDataArray: PanelData[];
126
+ }>({
127
+ direction,
128
+ dragState,
129
+ id: groupId,
130
+ keyboardResizeByPercentage,
131
+ keyboardResizeByPixels,
132
+ layout,
133
+ onLayout,
134
+ panelDataArray,
135
+ });
168
136
 
169
137
  const devWarningsRef = useRef<{
170
- didLogDefaultSizeWarning: boolean;
171
138
  didLogIdAndOrderWarning: boolean;
172
- didLogInvalidLayoutWarning: boolean;
139
+ didLogPanelConstraintsWarning: boolean;
173
140
  prevPanelIds: string[];
174
141
  }>({
175
- didLogDefaultSizeWarning: false,
176
142
  didLogIdAndOrderWarning: false,
177
- didLogInvalidLayoutWarning: false,
143
+ didLogPanelConstraintsWarning: false,
178
144
  prevPanelIds: [],
179
145
  });
180
146
 
181
- // Use a ref to guard against users passing inline props
182
- const callbacksRef = useRef<{
183
- onLayout: PanelGroupOnLayout | undefined;
184
- }>({ onLayout });
185
- useEffect(() => {
186
- callbacksRef.current.onLayout = onLayout;
187
- });
188
-
189
- const panelIdToLastNotifiedSizeMapRef = useRef<Record<string, number>>({});
190
-
191
- // 0-1 values representing the relative size of each panel.
192
- const [sizes, setSizes] = useState<number[]>([]);
193
-
194
- // Used to support imperative collapse/expand API.
195
- const panelSizeBeforeCollapse = useRef<Map<string, number>>(new Map());
196
-
197
- const prevDeltaRef = useRef<number>(0);
198
-
199
- // Store committed values to avoid unnecessarily re-running memoization/effects functions.
200
- const committedValuesRef = useRef<CommittedValues>({
201
- direction,
202
- id: groupId,
203
- panels,
204
- sizes,
205
- units,
206
- });
207
-
208
147
  useImperativeHandle(
209
148
  forwardedRef,
210
149
  () => ({
211
- getId: () => groupId,
212
- getLayout: (unitsFromParams?: Units) => {
213
- const { sizes, units: unitsFromProps } = committedValuesRef.current;
214
-
215
- const units = unitsFromParams ?? unitsFromProps;
216
- if (units === "pixels") {
217
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
218
- return sizes.map((size) => (size / 100) * groupSizePixels);
219
- } else {
220
- return sizes;
221
- }
150
+ getId: () => committedValuesRef.current.id,
151
+ getLayout: () => {
152
+ const { id: groupId, layout } = committedValuesRef.current;
153
+
154
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
155
+
156
+ return layout.map((sizePercentage) => {
157
+ return {
158
+ sizePercentage,
159
+ sizePixels: convertPercentageToPixels(
160
+ sizePercentage,
161
+ groupSizePixels
162
+ ),
163
+ };
164
+ });
222
165
  },
223
- setLayout: (sizes: number[], unitsFromParams?: Units) => {
166
+ setLayout: (mixedSizes: Partial<MixedSizes>[]) => {
224
167
  const {
225
168
  id: groupId,
226
- panels,
227
- sizes: prevSizes,
228
- units,
169
+ layout: prevLayout,
170
+ onLayout,
171
+ panelDataArray,
229
172
  } = committedValuesRef.current;
230
173
 
231
- if ((unitsFromParams || units) === "pixels") {
232
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
233
- sizes = sizes.map((size) => (size / groupSizePixels) * 100);
234
- }
174
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
235
175
 
236
- const panelIdToLastNotifiedSizeMap =
237
- panelIdToLastNotifiedSizeMapRef.current;
238
- const panelsArray = panelsMapToSortedArray(panels);
176
+ const unsafeLayout = mixedSizes.map(
177
+ (mixedSize) =>
178
+ getPercentageSizeFromMixedSizes(mixedSize, groupSizePixels)!
179
+ );
239
180
 
240
- const nextSizes = validatePanelGroupLayout({
241
- groupId,
242
- panels,
243
- nextSizes: sizes,
244
- prevSizes,
245
- units,
181
+ const safeLayout = validatePanelGroupLayout({
182
+ groupSizePixels,
183
+ layout: unsafeLayout,
184
+ panelConstraints: panelDataArray.map(
185
+ (panelData) => panelData.constraints
186
+ ),
246
187
  });
247
- if (!areEqual(prevSizes, nextSizes)) {
248
- setSizes(nextSizes);
188
+
189
+ if (!areEqual(prevLayout, safeLayout)) {
190
+ setLayout(safeLayout);
191
+
192
+ if (onLayout) {
193
+ onLayout(
194
+ safeLayout.map((sizePercentage) => ({
195
+ sizePercentage,
196
+ sizePixels: convertPercentageToPixels(
197
+ sizePercentage,
198
+ groupSizePixels
199
+ ),
200
+ }))
201
+ );
202
+ }
249
203
 
250
204
  callPanelCallbacks(
251
- panelsArray,
252
- nextSizes,
253
- panelIdToLastNotifiedSizeMap
205
+ groupId,
206
+ panelDataArray,
207
+ safeLayout,
208
+ panelIdToLastNotifiedMixedSizesMapRef.current
254
209
  );
255
210
  }
256
211
  },
257
212
  }),
258
- [groupId]
213
+ []
259
214
  );
260
215
 
261
216
  useIsomorphicLayoutEffect(() => {
262
217
  committedValuesRef.current.direction = direction;
218
+ committedValuesRef.current.dragState = dragState;
263
219
  committedValuesRef.current.id = groupId;
264
- committedValuesRef.current.panels = panels;
265
- committedValuesRef.current.sizes = sizes;
266
- committedValuesRef.current.units = units;
220
+ committedValuesRef.current.layout = layout;
221
+ committedValuesRef.current.onLayout = onLayout;
222
+ committedValuesRef.current.panelDataArray = panelDataArray;
267
223
  });
268
224
 
269
225
  useWindowSplitterPanelGroupBehavior({
270
226
  committedValuesRef,
271
227
  groupId,
272
- panels,
273
- setSizes,
274
- sizes,
275
- panelSizeBeforeCollapse,
228
+ layout,
229
+ panelDataArray,
230
+ setLayout,
276
231
  });
277
232
 
278
- // Notify external code when sizes have changed.
279
233
  useEffect(() => {
280
- const { onLayout } = callbacksRef.current!;
281
- const { panels, sizes } = committedValuesRef.current;
282
-
283
- // Don't commit layout until all panels have registered and re-rendered with their actual sizes.
284
- if (sizes.length > 0) {
285
- if (onLayout) {
286
- onLayout(sizes);
234
+ // If this panel has been configured to persist sizing information, save sizes to local storage.
235
+ if (autoSaveId) {
236
+ if (layout.length === 0 || layout.length !== panelDataArray.length) {
237
+ return;
287
238
  }
288
239
 
289
- const panelIdToLastNotifiedSizeMap =
290
- panelIdToLastNotifiedSizeMapRef.current;
291
-
292
- // When possible, we notify before the next render so that rendering work can be batched together.
293
- // Some cases are difficult to detect though,
294
- // for example– panels that are conditionally rendered can affect the size of neighboring panels.
295
- // In this case, the best we can do is notify on commit.
296
- // The callPanelCallbacks() uses its own memoization to avoid notifying panels twice in these cases.
297
- const panelsArray = panelsMapToSortedArray(panels);
298
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
240
+ // Limit the frequency of localStorage updates.
241
+ if (!debounceMap[autoSaveId]) {
242
+ debounceMap[autoSaveId] = debounce(
243
+ savePanelGroupLayout,
244
+ LOCAL_STORAGE_DEBOUNCE_INTERVAL
245
+ );
246
+ }
247
+ debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
299
248
  }
300
- }, [sizes]);
249
+ }, [autoSaveId, layout, panelDataArray, storage]);
301
250
 
302
251
  // Once all panels have registered themselves,
303
252
  // Compute the initial sizes based on default weights.
304
253
  // This assumes that panels register during initial mount (no conditional rendering)!
305
254
  useIsomorphicLayoutEffect(() => {
306
- const { id: groupId, sizes, units } = committedValuesRef.current;
307
- if (sizes.length === panels.size) {
308
- // Only compute (or restore) default sizes once per panel configuration.
255
+ const { id: groupId, layout, onLayout } = committedValuesRef.current;
256
+ if (layout.length === panelDataArray.length) {
257
+ // Only compute (or restore) default layout once per panel configuration.
309
258
  return;
310
259
  }
311
260
 
312
261
  // If this panel has been configured to persist sizing information,
313
262
  // default size should be restored from local storage if possible.
314
- let defaultSizes: number[] | null = null;
263
+ let unsafeLayout: number[] | null = null;
315
264
  if (autoSaveId) {
316
- const panelsArray = panelsMapToSortedArray(panels);
317
- defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
265
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
318
266
  }
319
267
 
320
- if (defaultSizes != null) {
321
- // Validate saved sizes in case something has changed since last render
322
- // e.g. for pixel groups, this could be the size of the window
323
- const validatedSizes = validatePanelGroupLayout({
324
- groupId,
325
- panels,
326
- nextSizes: defaultSizes,
327
- prevSizes: defaultSizes,
328
- units,
329
- });
268
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
330
269
 
331
- setSizes(validatedSizes);
332
- } else {
333
- const sizes = calculateDefaultLayout({
334
- groupId,
335
- panels,
336
- units,
270
+ if (unsafeLayout == null) {
271
+ unsafeLayout = calculateUnsafeDefaultLayout({
272
+ groupSizePixels,
273
+ panelDataArray,
337
274
  });
275
+ }
338
276
 
339
- setSizes(sizes);
277
+ // Validate even saved layouts in case something has changed since last render
278
+ // e.g. for pixel groups, this could be the size of the window
279
+ const validatedLayout = validatePanelGroupLayout({
280
+ groupSizePixels,
281
+ layout: unsafeLayout,
282
+ panelConstraints: panelDataArray.map(
283
+ (panelData) => panelData.constraints
284
+ ),
285
+ });
286
+
287
+ if (!areEqual(layout, validatedLayout)) {
288
+ setLayout(validatedLayout);
340
289
  }
341
- }, [autoSaveId, panels, storage]);
342
290
 
343
- useEffect(() => {
344
- // If this panel has been configured to persist sizing information, save sizes to local storage.
345
- if (autoSaveId) {
346
- if (sizes.length === 0 || sizes.length !== panels.size) {
347
- return;
348
- }
291
+ if (onLayout) {
292
+ onLayout(
293
+ validatedLayout.map((sizePercentage) => ({
294
+ sizePercentage,
295
+ sizePixels: convertPercentageToPixels(
296
+ sizePercentage,
297
+ groupSizePixels
298
+ ),
299
+ }))
300
+ );
301
+ }
349
302
 
350
- const panelsArray = panelsMapToSortedArray(panels);
303
+ callPanelCallbacks(
304
+ groupId,
305
+ panelDataArray,
306
+ validatedLayout,
307
+ panelIdToLastNotifiedMixedSizesMapRef.current
308
+ );
309
+ }, [autoSaveId, layout, panelDataArray, storage]);
351
310
 
352
- // Limit the frequency of localStorage updates.
353
- if (!debounceMap[autoSaveId]) {
354
- debounceMap[autoSaveId] = debounce(savePanelGroupLayout, 100);
355
- }
356
- debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
311
+ useIsomorphicLayoutEffect(() => {
312
+ const constraints = panelDataArray.map(({ constraints }) => constraints);
313
+ if (!shouldMonitorPixelBasedConstraints(constraints)) {
314
+ // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
315
+ return;
357
316
  }
358
317
 
318
+ if (typeof ResizeObserver === "undefined") {
319
+ console.warn(
320
+ `WARNING: Pixel based constraints require ResizeObserver but it is not supported by the current browser.`
321
+ );
322
+ } else {
323
+ const resizeObserver = new ResizeObserver(() => {
324
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
325
+
326
+ const { layout: prevLayout, onLayout } = committedValuesRef.current;
327
+
328
+ const nextLayout = validatePanelGroupLayout({
329
+ groupSizePixels,
330
+ layout: prevLayout,
331
+ panelConstraints: panelDataArray.map(
332
+ (panelData) => panelData.constraints
333
+ ),
334
+ });
335
+
336
+ if (!areEqual(prevLayout, nextLayout)) {
337
+ setLayout(nextLayout);
338
+
339
+ if (onLayout) {
340
+ onLayout(
341
+ nextLayout.map((sizePercentage) => ({
342
+ sizePercentage,
343
+ sizePixels: convertPercentageToPixels(
344
+ sizePercentage,
345
+ groupSizePixels
346
+ ),
347
+ }))
348
+ );
349
+ }
350
+
351
+ callPanelCallbacks(
352
+ groupId,
353
+ panelDataArray,
354
+ nextLayout,
355
+ panelIdToLastNotifiedMixedSizesMapRef.current
356
+ );
357
+ }
358
+ });
359
+
360
+ resizeObserver.observe(getPanelGroupElement(groupId)!);
361
+
362
+ return () => {
363
+ resizeObserver.disconnect();
364
+ };
365
+ }
366
+ }, [groupId, panelDataArray]);
367
+
368
+ // DEV warnings
369
+ useEffect(() => {
359
370
  if (isDevelopment) {
360
- const { didLogIdAndOrderWarning, prevPanelIds } = devWarningsRef.current;
371
+ const {
372
+ didLogIdAndOrderWarning,
373
+ didLogPanelConstraintsWarning,
374
+ prevPanelIds,
375
+ } = devWarningsRef.current;
376
+
361
377
  if (!didLogIdAndOrderWarning) {
362
- const { panels } = committedValuesRef.current;
378
+ const { panelDataArray } = committedValuesRef.current;
363
379
 
364
- const panelIds = Array.from(panels.keys());
380
+ const panelIds = panelDataArray.map(({ id }) => id);
365
381
 
366
382
  devWarningsRef.current.prevPanelIds = panelIds;
367
383
 
@@ -369,9 +385,8 @@ function PanelGroupWithForwardedRef({
369
385
  prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
370
386
  if (panelsHaveChanged) {
371
387
  if (
372
- Array.from(panels.values()).find(
373
- (panel) =>
374
- panel.current.idWasAutoGenerated || panel.current.order == null
388
+ panelDataArray.find(
389
+ ({ idIsFromProps, order }) => !idIsFromProps || order == null
375
390
  )
376
391
  ) {
377
392
  devWarningsRef.current.didLogIdAndOrderWarning = true;
@@ -382,512 +397,509 @@ function PanelGroupWithForwardedRef({
382
397
  }
383
398
  }
384
399
  }
385
- }
386
- }, [autoSaveId, panels, sizes, storage]);
387
400
 
388
- useIsomorphicLayoutEffect(() => {
389
- // Pixel panel constraints need to be reassessed after a group resize
390
- // We can avoid the ResizeObserver overhead for relative layouts
391
- if (units === "pixels") {
392
- const resizeObserver = new ResizeObserver(() => {
393
- const { panels, sizes: prevSizes } = committedValuesRef.current;
401
+ if (!didLogPanelConstraintsWarning) {
402
+ const panelConstraints = panelDataArray.map(
403
+ (panelData) => panelData.constraints
404
+ );
394
405
 
395
- const nextSizes = validatePanelGroupLayout({
396
- groupId,
397
- panels,
398
- nextSizes: prevSizes,
399
- prevSizes,
400
- units,
401
- });
402
- if (!areEqual(prevSizes, nextSizes)) {
403
- setSizes(nextSizes);
404
- }
405
- });
406
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
406
407
 
407
- resizeObserver.observe(getPanelGroup(groupId)!);
408
+ for (
409
+ let panelIndex = 0;
410
+ panelIndex < panelConstraints.length;
411
+ panelIndex++
412
+ ) {
413
+ const isValid = validatePanelConstraints({
414
+ groupSizePixels,
415
+ panelConstraints,
416
+ panelId: panelDataArray[panelIndex].id,
417
+ panelIndex,
418
+ });
408
419
 
409
- return () => {
410
- resizeObserver.disconnect();
411
- };
412
- }
413
- }, [groupId, units]);
420
+ if (!isValid) {
421
+ devWarningsRef.current.didLogPanelConstraintsWarning = true;
414
422
 
415
- const getPanelSize = useCallback(
416
- (id: string, unitsFromParams?: Units) => {
417
- const { panels, units: unitsFromProps } = committedValuesRef.current;
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ }
428
+ });
418
429
 
419
- const panelsArray = panelsMapToSortedArray(panels);
430
+ // External APIs are safe to memoize via committed values ref
431
+ const collapsePanel = useCallback(
432
+ (panelData: PanelData) => {
433
+ const {
434
+ layout: prevLayout,
435
+ onLayout,
436
+ panelDataArray,
437
+ } = committedValuesRef.current;
420
438
 
421
- const index = panelsArray.findIndex((panel) => panel.current.id === id);
422
- const size = sizes[index];
439
+ if (panelData.constraints.collapsible) {
440
+ const panelConstraintsArray = panelDataArray.map(
441
+ (panelData) => panelData.constraints
442
+ );
423
443
 
424
- const units = unitsFromParams ?? unitsFromProps;
425
- if (units === "pixels") {
426
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
427
- return (size / 100) * groupSizePixels;
428
- } else {
429
- return size;
430
- }
431
- },
432
- [groupId, sizes]
433
- );
444
+ const {
445
+ collapsedSizePercentage,
446
+ panelSizePercentage,
447
+ pivotIndices,
448
+ groupSizePixels,
449
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
450
+
451
+ if (panelSizePercentage !== collapsedSizePercentage) {
452
+ // Store size before collapse;
453
+ // This is the size that gets restored if the expand() API is used.
454
+ panelSizeBeforeCollapseRef.current.set(
455
+ panelData.id,
456
+ panelSizePercentage
457
+ );
434
458
 
435
- const getPanelStyle = useCallback(
436
- (id: string, defaultSize: number | null): CSSProperties => {
437
- const { panels } = committedValuesRef.current;
438
-
439
- // Before mounting, Panels will not yet have registered themselves.
440
- // This includes server rendering.
441
- // At this point the best we can do is render everything with the same size.
442
- if (panels.size === 0) {
443
- if (isDevelopment) {
444
- if (!devWarningsRef.current.didLogDefaultSizeWarning) {
445
- if (!isBrowser && defaultSize == null) {
446
- devWarningsRef.current.didLogDefaultSizeWarning = true;
447
- console.warn(
448
- `WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering`
459
+ const isLastPanel =
460
+ panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
461
+ const delta = isLastPanel
462
+ ? panelSizePercentage - collapsedSizePercentage
463
+ : collapsedSizePercentage - panelSizePercentage;
464
+
465
+ const nextLayout = adjustLayoutByDelta({
466
+ delta,
467
+ groupSizePixels,
468
+ layout: prevLayout,
469
+ panelConstraints: panelConstraintsArray,
470
+ pivotIndices,
471
+ trigger: "imperative-api",
472
+ });
473
+
474
+ if (!compareLayouts(prevLayout, nextLayout)) {
475
+ setLayout(nextLayout);
476
+
477
+ if (onLayout) {
478
+ onLayout(
479
+ nextLayout.map((sizePercentage) => ({
480
+ sizePercentage,
481
+ sizePixels: convertPercentageToPixels(
482
+ sizePercentage,
483
+ groupSizePixels
484
+ ),
485
+ }))
449
486
  );
450
487
  }
488
+
489
+ callPanelCallbacks(
490
+ groupId,
491
+ panelDataArray,
492
+ nextLayout,
493
+ panelIdToLastNotifiedMixedSizesMapRef.current
494
+ );
451
495
  }
452
496
  }
453
-
454
- return {
455
- flexBasis: 0,
456
- flexGrow: defaultSize != null ? defaultSize : undefined,
457
- flexShrink: 1,
458
-
459
- // Without this, Panel sizes may be unintentionally overridden by their content.
460
- overflow: "hidden",
461
- };
462
497
  }
463
-
464
- const flexGrow = getFlexGrow(panels, id, sizes);
465
-
466
- return {
467
- flexBasis: 0,
468
- flexGrow,
469
- flexShrink: 1,
470
-
471
- // Without this, Panel sizes may be unintentionally overridden by their content.
472
- overflow: "hidden",
473
-
474
- // Disable pointer events inside of a panel during resize.
475
- // This avoid edge cases like nested iframes.
476
- pointerEvents:
477
- disablePointerEventsDuringResize && activeHandleId !== null
478
- ? "none"
479
- : undefined,
480
- };
481
498
  },
482
- [activeHandleId, disablePointerEventsDuringResize, sizes]
499
+ [groupId]
483
500
  );
484
501
 
485
- const registerPanel = useCallback((id: string, panelRef: PanelData) => {
486
- const { units } = committedValuesRef.current;
487
-
488
- validatePanelProps(units, panelRef);
489
-
490
- setPanels((prevPanels) => {
491
- if (prevPanels.has(id)) {
492
- return prevPanels;
493
- }
494
-
495
- const nextPanels = new Map(prevPanels);
496
- nextPanels.set(id, panelRef);
497
-
498
- return nextPanels;
499
- });
500
- }, []);
501
-
502
- const registerResizeHandle = useCallback(
503
- (handleId: string) => {
504
- const resizeHandler = (event: ResizeEvent) => {
505
- event.preventDefault();
506
-
507
- const {
508
- direction,
509
- panels,
510
- sizes: prevSizes,
511
- } = committedValuesRef.current;
512
-
513
- const panelsArray = panelsMapToSortedArray(panels);
514
-
515
- const [idBefore, idAfter] = getResizeHandlePanelIds(
516
- groupId,
517
- handleId,
518
- panelsArray
519
- );
520
- if (idBefore == null || idAfter == null) {
521
- return;
522
- }
523
-
524
- let movement = getMovement(
525
- event,
526
- groupId,
527
- handleId,
528
- panelsArray,
529
- direction,
530
- prevSizes,
531
- initialDragStateRef.current
532
- );
533
- if (movement === 0) {
534
- return;
535
- }
536
-
537
- const groupElement = getPanelGroup(groupId)!;
538
- const rect = groupElement.getBoundingClientRect();
539
- const isHorizontal = direction === "horizontal";
540
-
541
- // Support RTL layouts
542
- if (document.dir === "rtl" && isHorizontal) {
543
- movement = -movement;
544
- }
502
+ // External APIs are safe to memoize via committed values ref
503
+ const expandPanel = useCallback(
504
+ (panelData: PanelData) => {
505
+ const {
506
+ layout: prevLayout,
507
+ onLayout,
508
+ panelDataArray,
509
+ } = committedValuesRef.current;
545
510
 
546
- const size = isHorizontal ? rect.width : rect.height;
547
- const delta = (movement / size) * 100;
548
-
549
- // If a validateLayout method has been provided
550
- // it's important to use it before updating the mouse cursor
551
- const nextSizes = adjustByDelta(
552
- event,
553
- committedValuesRef.current,
554
- idBefore,
555
- idAfter,
556
- delta,
557
- prevSizes,
558
- panelSizeBeforeCollapse.current,
559
- initialDragStateRef.current
511
+ if (panelData.constraints.collapsible) {
512
+ const panelConstraintsArray = panelDataArray.map(
513
+ (panelData) => panelData.constraints
560
514
  );
561
515
 
562
- const sizesChanged = !areEqual(prevSizes, nextSizes);
563
-
564
- // Don't update cursor for resizes triggered by keyboard interactions.
565
- if (isMouseEvent(event) || isTouchEvent(event)) {
566
- // Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
567
- // In this case, Panel sizes might not change–
568
- // but updating cursor in this scenario would cause a flicker.
569
- if (prevDeltaRef.current != delta) {
570
- if (!sizesChanged) {
571
- // If the pointer has moved too far to resize the panel any further,
572
- // update the cursor style for a visual clue.
573
- // This mimics VS Code behavior.
574
-
575
- if (isHorizontal) {
576
- setGlobalCursorStyle(
577
- movement < 0 ? "horizontal-min" : "horizontal-max"
578
- );
579
- } else {
580
- setGlobalCursorStyle(
581
- movement < 0 ? "vertical-min" : "vertical-max"
582
- );
583
- }
584
- } else {
585
- // Reset the cursor style to the the normal resize cursor.
586
- setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical");
516
+ const {
517
+ collapsedSizePercentage,
518
+ panelSizePercentage,
519
+ minSizePercentage,
520
+ pivotIndices,
521
+ groupSizePixels,
522
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
523
+
524
+ if (panelSizePercentage === collapsedSizePercentage) {
525
+ // Restore this panel to the size it was before it was collapsed, if possible.
526
+ const prevPanelSizePercentage =
527
+ panelSizeBeforeCollapseRef.current.get(panelData.id);
528
+
529
+ const baseSizePercentage =
530
+ prevPanelSizePercentage != null
531
+ ? prevPanelSizePercentage
532
+ : minSizePercentage;
533
+
534
+ const isLastPanel =
535
+ panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
536
+ const delta = isLastPanel
537
+ ? panelSizePercentage - baseSizePercentage
538
+ : baseSizePercentage - panelSizePercentage;
539
+
540
+ const nextLayout = adjustLayoutByDelta({
541
+ delta,
542
+ groupSizePixels,
543
+ layout: prevLayout,
544
+ panelConstraints: panelConstraintsArray,
545
+ pivotIndices,
546
+ trigger: "imperative-api",
547
+ });
548
+
549
+ if (!compareLayouts(prevLayout, nextLayout)) {
550
+ setLayout(nextLayout);
551
+
552
+ if (onLayout) {
553
+ onLayout(
554
+ nextLayout.map((sizePercentage) => ({
555
+ sizePercentage,
556
+ sizePixels: convertPercentageToPixels(
557
+ sizePercentage,
558
+ groupSizePixels
559
+ ),
560
+ }))
561
+ );
587
562
  }
563
+
564
+ callPanelCallbacks(
565
+ groupId,
566
+ panelDataArray,
567
+ nextLayout,
568
+ panelIdToLastNotifiedMixedSizesMapRef.current
569
+ );
588
570
  }
589
571
  }
572
+ }
573
+ },
574
+ [groupId]
575
+ );
590
576
 
591
- if (sizesChanged) {
592
- const panelIdToLastNotifiedSizeMap =
593
- panelIdToLastNotifiedSizeMapRef.current;
594
-
595
- // It's okay to bypass in this case because we already validated above
596
- setSizes(nextSizes);
577
+ // External APIs are safe to memoize via committed values ref
578
+ const getPanelSize = useCallback(
579
+ (panelData: PanelData) => {
580
+ const { layout, panelDataArray } = committedValuesRef.current;
597
581
 
598
- // If resize change handlers have been declared, this is the time to call them.
599
- // Trigger user callbacks after updating state, so that user code can override the sizes.
600
- callPanelCallbacks(
601
- panelsArray,
602
- nextSizes,
603
- panelIdToLastNotifiedSizeMap
604
- );
605
- }
582
+ const { panelSizePercentage, panelSizePixels } = panelDataHelper(
583
+ groupId,
584
+ panelDataArray,
585
+ panelData,
586
+ layout
587
+ );
606
588
 
607
- prevDeltaRef.current = delta;
589
+ return {
590
+ sizePercentage: panelSizePercentage,
591
+ sizePixels: panelSizePixels,
608
592
  };
609
-
610
- return resizeHandler;
611
593
  },
612
594
  [groupId]
613
595
  );
614
596
 
615
- const unregisterPanel = useCallback((id: string) => {
616
- setPanels((prevPanels) => {
617
- if (!prevPanels.has(id)) {
618
- return prevPanels;
619
- }
620
-
621
- const nextPanels = new Map(prevPanels);
622
- nextPanels.delete(id);
623
-
624
- return nextPanels;
625
- });
626
- }, []);
627
-
628
- const collapsePanel = useCallback((id: string) => {
629
- const { panels, sizes: prevSizes } = committedValuesRef.current;
630
-
631
- const panel = panels.get(id);
632
- if (panel == null) {
633
- return;
634
- }
635
-
636
- const { collapsedSize, collapsible } = panel.current;
637
- if (!collapsible) {
638
- return;
639
- }
640
-
641
- const panelsArray = panelsMapToSortedArray(panels);
597
+ // This API should never read from committedValuesRef
598
+ const getPanelStyle = useCallback(
599
+ (panelData: PanelData) => {
600
+ const panelIndex = panelDataArray.indexOf(panelData);
601
+
602
+ return computePanelFlexBoxStyle({
603
+ dragState,
604
+ layout,
605
+ panelData: panelDataArray,
606
+ panelIndex,
607
+ });
608
+ },
609
+ [dragState, layout, panelDataArray]
610
+ );
642
611
 
643
- const index = panelsArray.indexOf(panel);
644
- if (index < 0) {
645
- return;
646
- }
612
+ // External APIs are safe to memoize via committed values ref
613
+ const isPanelCollapsed = useCallback(
614
+ (panelData: PanelData) => {
615
+ const { layout, panelDataArray } = committedValuesRef.current;
647
616
 
648
- const currentSize = prevSizes[index];
649
- if (currentSize === collapsedSize) {
650
- // Panel is already collapsed.
651
- return;
652
- }
617
+ const { collapsedSizePercentage, collapsible, panelSizePercentage } =
618
+ panelDataHelper(groupId, panelDataArray, panelData, layout);
653
619
 
654
- panelSizeBeforeCollapse.current.set(id, currentSize);
620
+ return (
621
+ collapsible === true && panelSizePercentage === collapsedSizePercentage
622
+ );
623
+ },
624
+ [groupId]
625
+ );
655
626
 
656
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
657
- if (idBefore == null || idAfter == null) {
658
- return;
659
- }
627
+ // External APIs are safe to memoize via committed values ref
628
+ const isPanelExpanded = useCallback(
629
+ (panelData: PanelData) => {
630
+ const { layout, panelDataArray } = committedValuesRef.current;
660
631
 
661
- const isLastPanel = index === panelsArray.length - 1;
662
- const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
663
-
664
- const nextSizes = adjustByDelta(
665
- null,
666
- committedValuesRef.current,
667
- idBefore,
668
- idAfter,
669
- delta,
670
- prevSizes,
671
- panelSizeBeforeCollapse.current,
672
- null
673
- );
674
- if (prevSizes !== nextSizes) {
675
- const panelIdToLastNotifiedSizeMap =
676
- panelIdToLastNotifiedSizeMapRef.current;
632
+ const { collapsedSizePercentage, collapsible, panelSizePercentage } =
633
+ panelDataHelper(groupId, panelDataArray, panelData, layout);
677
634
 
678
- setSizes(nextSizes);
635
+ return !collapsible || panelSizePercentage > collapsedSizePercentage;
636
+ },
637
+ [groupId]
638
+ );
679
639
 
680
- // If resize change handlers have been declared, this is the time to call them.
681
- // Trigger user callbacks after updating state, so that user code can override the sizes.
682
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
683
- }
640
+ const registerPanel = useCallback((panelData: PanelData) => {
641
+ setPanelDataArray((prevPanelDataArray) => {
642
+ const nextPanelDataArray = [...prevPanelDataArray, panelData];
643
+ return nextPanelDataArray.sort((panelA, panelB) => {
644
+ const orderA = panelA.order;
645
+ const orderB = panelB.order;
646
+ if (orderA == null && orderB == null) {
647
+ return 0;
648
+ } else if (orderA == null) {
649
+ return -1;
650
+ } else if (orderB == null) {
651
+ return 1;
652
+ } else {
653
+ return orderA - orderB;
654
+ }
655
+ });
656
+ });
684
657
  }, []);
685
658
 
686
- const expandPanel = useCallback((id: string) => {
687
- const { panels, sizes: prevSizes } = committedValuesRef.current;
688
-
689
- const panel = panels.get(id);
690
- if (panel == null) {
691
- return;
692
- }
693
-
694
- const { collapsedSize, minSize } = panel.current;
695
-
696
- const sizeBeforeCollapse =
697
- panelSizeBeforeCollapse.current.get(id) || minSize;
698
- if (!sizeBeforeCollapse) {
699
- return;
700
- }
701
-
702
- const panelsArray = panelsMapToSortedArray(panels);
703
-
704
- const index = panelsArray.indexOf(panel);
705
- if (index < 0) {
706
- return;
707
- }
708
-
709
- const currentSize = prevSizes[index];
710
- if (currentSize !== collapsedSize) {
711
- // Panel is already expanded.
712
- return;
713
- }
714
-
715
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
716
- if (idBefore == null || idAfter == null) {
717
- return;
718
- }
719
-
720
- const isLastPanel = index === panelsArray.length - 1;
721
- const delta = isLastPanel
722
- ? collapsedSize - sizeBeforeCollapse
723
- : sizeBeforeCollapse;
724
-
725
- const nextSizes = adjustByDelta(
726
- null,
727
- committedValuesRef.current,
728
- idBefore,
729
- idAfter,
730
- delta,
731
- prevSizes,
732
- panelSizeBeforeCollapse.current,
733
- null
734
- );
735
- if (prevSizes !== nextSizes) {
736
- const panelIdToLastNotifiedSizeMap =
737
- panelIdToLastNotifiedSizeMapRef.current;
738
-
739
- setSizes(nextSizes);
659
+ const registerResizeHandle = useCallback((dragHandleId: string) => {
660
+ return function resizeHandler(event: ResizeEvent) {
661
+ event.preventDefault();
740
662
 
741
- // If resize change handlers have been declared, this is the time to call them.
742
- // Trigger user callbacks after updating state, so that user code can override the sizes.
743
- callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
744
- }
745
- }, []);
746
-
747
- const resizePanel = useCallback(
748
- (id: string, nextSize: number, unitsFromParams?: Units) => {
749
663
  const {
664
+ direction,
665
+ dragState,
750
666
  id: groupId,
751
- panels,
752
- sizes: prevSizes,
753
- units,
667
+ keyboardResizeByPercentage,
668
+ keyboardResizeByPixels,
669
+ onLayout,
670
+ panelDataArray,
671
+ layout: prevLayout,
754
672
  } = committedValuesRef.current;
755
673
 
756
- if ((unitsFromParams || units) === "pixels") {
757
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
758
- nextSize = (nextSize / groupSizePixels) * 100;
759
- }
674
+ const { initialLayout } = dragState ?? {};
760
675
 
761
- const panel = panels.get(id);
762
- if (panel == null) {
763
- return;
764
- }
765
-
766
- let { collapsedSize, collapsible, maxSize, minSize } = panel.current;
676
+ const pivotIndices = determinePivotIndices(groupId, dragHandleId);
767
677
 
768
- if (units === "pixels") {
769
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
770
- minSize = (minSize / groupSizePixels) * 100;
771
- if (maxSize != null) {
772
- maxSize = (maxSize / groupSizePixels) * 100;
678
+ let delta = calculateDeltaPercentage(
679
+ event,
680
+ groupId,
681
+ dragHandleId,
682
+ direction,
683
+ dragState!,
684
+ {
685
+ percentage: keyboardResizeByPercentage,
686
+ pixels: keyboardResizeByPixels,
773
687
  }
774
- }
775
-
776
- const panelsArray = panelsMapToSortedArray(panels);
777
-
778
- const index = panelsArray.indexOf(panel);
779
- if (index < 0) {
688
+ );
689
+ if (delta === 0) {
780
690
  return;
781
691
  }
782
692
 
783
- const currentSize = prevSizes[index];
784
- if (currentSize === nextSize) {
785
- return;
693
+ // Support RTL layouts
694
+ const isHorizontal = direction === "horizontal";
695
+ if (document.dir === "rtl" && isHorizontal) {
696
+ delta = -delta;
786
697
  }
787
698
 
788
- if (collapsible && nextSize === collapsedSize) {
789
- // This is a valid resize state.
790
- } else {
791
- const unsafeNextSize = nextSize;
699
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
700
+ const panelConstraints = panelDataArray.map(
701
+ (panelData) => panelData.constraints
702
+ );
792
703
 
793
- nextSize = Math.min(
794
- maxSize != null ? maxSize : 100,
795
- Math.max(minSize, nextSize)
796
- );
704
+ const nextLayout = adjustLayoutByDelta({
705
+ delta,
706
+ groupSizePixels,
707
+ layout: initialLayout ?? prevLayout,
708
+ panelConstraints,
709
+ pivotIndices,
710
+ trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch",
711
+ });
797
712
 
798
- if (isDevelopment) {
799
- if (unsafeNextSize !== nextSize) {
800
- console.error(
801
- `Invalid size (${unsafeNextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`
802
- );
713
+ const layoutChanged = !compareLayouts(prevLayout, nextLayout);
714
+
715
+ // Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
716
+ // Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
717
+ if (isMouseEvent(event) || isTouchEvent(event)) {
718
+ // Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
719
+ // In this case, Panel sizes might not change–
720
+ // but updating cursor in this scenario would cause a flicker.
721
+ if (prevDeltaRef.current != delta) {
722
+ prevDeltaRef.current = delta;
723
+
724
+ if (!layoutChanged) {
725
+ // If the pointer has moved too far to resize the panel any further,
726
+ // update the cursor style for a visual clue.
727
+ // This mimics VS Code behavior.
728
+
729
+ if (isHorizontal) {
730
+ setGlobalCursorStyle(
731
+ delta < 0 ? "horizontal-min" : "horizontal-max"
732
+ );
733
+ } else {
734
+ setGlobalCursorStyle(delta < 0 ? "vertical-min" : "vertical-max");
735
+ }
736
+ } else {
737
+ // Reset the cursor style to the the normal resize cursor.
738
+ setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical");
803
739
  }
804
740
  }
805
741
  }
806
742
 
807
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
808
- if (idBefore == null || idAfter == null) {
809
- return;
743
+ if (layoutChanged) {
744
+ setLayout(nextLayout);
745
+
746
+ if (onLayout) {
747
+ onLayout(
748
+ nextLayout.map((sizePercentage) => ({
749
+ sizePercentage,
750
+ sizePixels: convertPercentageToPixels(
751
+ sizePercentage,
752
+ groupSizePixels
753
+ ),
754
+ }))
755
+ );
756
+ }
757
+
758
+ callPanelCallbacks(
759
+ groupId,
760
+ panelDataArray,
761
+ nextLayout,
762
+ panelIdToLastNotifiedMixedSizesMapRef.current
763
+ );
810
764
  }
765
+ };
766
+ }, []);
811
767
 
812
- const isLastPanel = index === panelsArray.length - 1;
768
+ // External APIs are safe to memoize via committed values ref
769
+ const resizePanel = useCallback(
770
+ (panelData: PanelData, mixedSizes: Partial<MixedSizes>) => {
771
+ const {
772
+ layout: prevLayout,
773
+ onLayout,
774
+ panelDataArray,
775
+ } = committedValuesRef.current;
776
+
777
+ const panelConstraintsArray = panelDataArray.map(
778
+ (panelData) => panelData.constraints
779
+ );
780
+
781
+ const { groupSizePixels, panelSizePercentage, pivotIndices } =
782
+ panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
783
+
784
+ const sizePercentage = getPercentageSizeFromMixedSizes(
785
+ mixedSizes,
786
+ groupSizePixels
787
+ )!;
788
+
789
+ const isLastPanel =
790
+ panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
813
791
  const delta = isLastPanel
814
- ? currentSize - nextSize
815
- : nextSize - currentSize;
816
-
817
- const nextSizes = adjustByDelta(
818
- null,
819
- committedValuesRef.current,
820
- idBefore,
821
- idAfter,
792
+ ? panelSizePercentage - sizePercentage
793
+ : sizePercentage - panelSizePercentage;
794
+
795
+ const nextLayout = adjustLayoutByDelta({
822
796
  delta,
823
- prevSizes,
824
- panelSizeBeforeCollapse.current,
825
- null
826
- );
827
- if (prevSizes !== nextSizes) {
828
- const panelIdToLastNotifiedSizeMap =
829
- panelIdToLastNotifiedSizeMapRef.current;
797
+ groupSizePixels,
798
+ layout: prevLayout,
799
+ panelConstraints: panelConstraintsArray,
800
+ pivotIndices,
801
+ trigger: "imperative-api",
802
+ });
830
803
 
831
- setSizes(nextSizes);
804
+ if (!compareLayouts(prevLayout, nextLayout)) {
805
+ setLayout(nextLayout);
806
+
807
+ if (onLayout) {
808
+ onLayout(
809
+ nextLayout.map((sizePercentage) => ({
810
+ sizePercentage,
811
+ sizePixels: convertPercentageToPixels(
812
+ sizePercentage,
813
+ groupSizePixels
814
+ ),
815
+ }))
816
+ );
817
+ }
832
818
 
833
- // If resize change handlers have been declared, this is the time to call them.
834
- // Trigger user callbacks after updating state, so that user code can override the sizes.
835
819
  callPanelCallbacks(
836
- panelsArray,
837
- nextSizes,
838
- panelIdToLastNotifiedSizeMap
820
+ groupId,
821
+ panelDataArray,
822
+ nextLayout,
823
+ panelIdToLastNotifiedMixedSizesMapRef.current
839
824
  );
840
825
  }
841
826
  },
827
+ [groupId]
828
+ );
829
+
830
+ const startDragging = useCallback(
831
+ (dragHandleId: string, event: ResizeEvent) => {
832
+ const { direction, layout } = committedValuesRef.current;
833
+
834
+ const handleElement = getResizeHandleElement(dragHandleId)!;
835
+
836
+ const initialCursorPosition = getResizeEventCursorPosition(
837
+ direction,
838
+ event
839
+ );
840
+
841
+ setDragState({
842
+ dragHandleId,
843
+ dragHandleRect: handleElement.getBoundingClientRect(),
844
+ initialCursorPosition,
845
+ initialLayout: layout,
846
+ });
847
+ },
842
848
  []
843
849
  );
844
850
 
851
+ const stopDragging = useCallback(() => {
852
+ resetGlobalCursorStyle();
853
+ setDragState(null);
854
+ }, []);
855
+
856
+ const unregisterPanel = useCallback((panelData: PanelData) => {
857
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
858
+
859
+ setPanelDataArray((panelDataArray) => {
860
+ const index = panelDataArray.indexOf(panelData);
861
+ if (index >= 0) {
862
+ panelDataArray = [...panelDataArray];
863
+ panelDataArray.splice(index, 1);
864
+ }
865
+
866
+ return panelDataArray;
867
+ });
868
+ }, []);
869
+
845
870
  const context = useMemo(
846
871
  () => ({
847
- activeHandleId,
848
872
  collapsePanel,
849
873
  direction,
874
+ dragState,
850
875
  expandPanel,
851
876
  getPanelSize,
852
877
  getPanelStyle,
853
878
  groupId,
879
+ isPanelCollapsed,
880
+ isPanelExpanded,
854
881
  registerPanel,
855
882
  registerResizeHandle,
856
883
  resizePanel,
857
- startDragging: (id: string, event: ResizeEvent) => {
858
- setActiveHandleId(id);
859
-
860
- if (isMouseEvent(event) || isTouchEvent(event)) {
861
- const handleElement = getResizeHandle(id)!;
862
-
863
- initialDragStateRef.current = {
864
- dragHandleRect: handleElement.getBoundingClientRect(),
865
- dragOffset: getDragOffset(event, id, direction),
866
- sizes: committedValuesRef.current.sizes,
867
- };
868
- }
869
- },
870
- stopDragging: () => {
871
- resetGlobalCursorStyle();
872
- setActiveHandleId(null);
873
-
874
- initialDragStateRef.current = null;
875
- },
876
- units,
884
+ startDragging,
885
+ stopDragging,
877
886
  unregisterPanel,
878
887
  }),
879
888
  [
880
- activeHandleId,
881
889
  collapsePanel,
890
+ dragState,
882
891
  direction,
883
892
  expandPanel,
884
893
  getPanelSize,
885
894
  getPanelStyle,
886
895
  groupId,
896
+ isPanelCollapsed,
897
+ isPanelExpanded,
887
898
  registerPanel,
888
899
  registerResizeHandle,
889
900
  resizePanel,
890
- units,
901
+ startDragging,
902
+ stopDragging,
891
903
  unregisterPanel,
892
904
  ]
893
905
  );
@@ -900,18 +912,25 @@ function PanelGroupWithForwardedRef({
900
912
  width: "100%",
901
913
  };
902
914
 
903
- return createElement(PanelGroupContext.Provider, {
904
- children: createElement(Type, {
915
+ return createElement(
916
+ PanelGroupContext.Provider,
917
+ { value: context },
918
+ createElement(Type, {
905
919
  children,
906
920
  className: classNameFromProps,
921
+ style: {
922
+ ...style,
923
+ ...styleFromProps,
924
+ },
925
+
926
+ // CSS selectors
907
927
  "data-panel-group": "",
908
- "data-panel-group-direction": direction,
909
- "data-panel-group-id": groupId,
910
- "data-panel-group-units": units,
911
- style: { ...style, ...styleFromProps },
912
- }),
913
- value: context,
914
- });
928
+
929
+ // e2e test attributes
930
+ "data-panel-group-direction": isDevelopment ? direction : undefined,
931
+ "data-panel-group-id": isDevelopment ? groupId : undefined,
932
+ })
933
+ );
915
934
  }
916
935
 
917
936
  export const PanelGroup = forwardRef<
@@ -923,3 +942,45 @@ export const PanelGroup = forwardRef<
923
942
 
924
943
  PanelGroupWithForwardedRef.displayName = "PanelGroup";
925
944
  PanelGroup.displayName = "forwardRef(PanelGroup)";
945
+
946
+ function panelDataHelper(
947
+ groupId: string,
948
+ panelDataArray: PanelData[],
949
+ panelData: PanelData,
950
+ layout: number[]
951
+ ) {
952
+ const panelConstraintsArray = panelDataArray.map(
953
+ (panelData) => panelData.constraints
954
+ );
955
+
956
+ const panelIndex = panelDataArray.indexOf(panelData);
957
+ const panelConstraints = panelConstraintsArray[panelIndex];
958
+
959
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
960
+
961
+ const percentagePanelConstraints = computePercentagePanelConstraints(
962
+ panelConstraintsArray,
963
+ panelIndex,
964
+ groupSizePixels
965
+ );
966
+
967
+ const isLastPanel = panelIndex === panelDataArray.length - 1;
968
+ const pivotIndices = isLastPanel
969
+ ? [panelIndex - 1, panelIndex]
970
+ : [panelIndex, panelIndex + 1];
971
+
972
+ const panelSizePercentage = layout[panelIndex];
973
+ const panelSizePixels = convertPercentageToPixels(
974
+ panelSizePercentage,
975
+ groupSizePixels
976
+ );
977
+
978
+ return {
979
+ ...percentagePanelConstraints,
980
+ collapsible: panelConstraints.collapsible,
981
+ panelSizePercentage,
982
+ panelSizePixels,
983
+ groupSizePixels,
984
+ pivotIndices,
985
+ };
986
+ }