react-resizable-panels 0.0.55 → 0.0.57

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 (95) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/CHANGELOG.md +238 -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 +1276 -1043
  11. package/dist/react-resizable-panels.browser.cjs.mjs +1 -2
  12. package/dist/react-resizable-panels.browser.development.cjs.js +1410 -1097
  13. package/dist/react-resizable-panels.browser.development.cjs.mjs +1 -2
  14. package/dist/react-resizable-panels.browser.development.esm.js +1411 -1097
  15. package/dist/react-resizable-panels.browser.esm.js +1277 -1043
  16. package/dist/react-resizable-panels.cjs.js +1276 -1043
  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 +1415 -1102
  20. package/dist/react-resizable-panels.development.cjs.mjs +1 -2
  21. package/dist/react-resizable-panels.development.esm.js +1416 -1102
  22. package/dist/react-resizable-panels.development.node.cjs.js +1179 -947
  23. package/dist/react-resizable-panels.development.node.cjs.mjs +1 -2
  24. package/dist/react-resizable-panels.development.node.esm.js +1180 -947
  25. package/dist/react-resizable-panels.esm.js +1277 -1043
  26. package/dist/react-resizable-panels.esm.js.map +1 -1
  27. package/dist/react-resizable-panels.node.cjs.js +1068 -910
  28. package/dist/react-resizable-panels.node.cjs.mjs +1 -2
  29. package/dist/react-resizable-panels.node.esm.js +1069 -910
  30. package/jest.config.js +10 -0
  31. package/package.json +5 -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 -667
  36. package/src/PanelGroupContext.ts +33 -0
  37. package/src/PanelResizeHandle.ts +21 -17
  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 +98 -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.test.ts +47 -0
  60. package/src/utils/convertPixelConstraintsToPercentages.ts +72 -0
  61. package/src/utils/convertPixelsToPercentage.test.ts +9 -0
  62. package/src/utils/convertPixelsToPercentage.ts +6 -0
  63. package/src/utils/determinePivotIndices.ts +10 -0
  64. package/src/utils/dom/calculateAvailablePanelSizeInPixels.ts +29 -0
  65. package/src/utils/dom/getAvailableGroupSizePixels.ts +29 -0
  66. package/src/utils/dom/getPanelElement.ts +7 -0
  67. package/src/utils/dom/getPanelGroupElement.ts +9 -0
  68. package/src/utils/dom/getResizeHandleElement.ts +9 -0
  69. package/src/utils/dom/getResizeHandleElementIndex.ts +12 -0
  70. package/src/utils/dom/getResizeHandleElementsForGroup.ts +9 -0
  71. package/src/utils/dom/getResizeHandlePanelIds.ts +18 -0
  72. package/src/utils/events.ts +13 -0
  73. package/src/utils/getPercentageSizeFromMixedSizes.test.ts +47 -0
  74. package/src/utils/getPercentageSizeFromMixedSizes.ts +15 -0
  75. package/src/utils/getResizeEventCursorPosition.ts +19 -0
  76. package/src/utils/initializeDefaultStorage.ts +26 -0
  77. package/src/utils/numbers/fuzzyCompareNumbers.test.ts +16 -0
  78. package/src/utils/numbers/fuzzyCompareNumbers.ts +17 -0
  79. package/src/utils/numbers/fuzzyNumbersEqual.ts +9 -0
  80. package/src/utils/resizePanel.test.ts +45 -0
  81. package/src/utils/resizePanel.ts +60 -0
  82. package/src/utils/serialization.ts +9 -4
  83. package/src/utils/shouldMonitorPixelBasedConstraints.test.ts +23 -0
  84. package/src/utils/shouldMonitorPixelBasedConstraints.ts +13 -0
  85. package/src/utils/test-utils.ts +136 -0
  86. package/src/utils/validatePanelConstraints.test.ts +151 -0
  87. package/src/utils/validatePanelConstraints.ts +103 -0
  88. package/src/utils/validatePanelGroupLayout.test.ts +233 -0
  89. package/src/utils/validatePanelGroupLayout.ts +88 -0
  90. package/src/vendor/react.ts +4 -0
  91. package/.eslintrc.json +0 -22
  92. package/dist/declarations/src/utils/group.d.ts +0 -29
  93. package/src/PanelContexts.ts +0 -22
  94. package/src/utils/coordinates.ts +0 -149
  95. 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,319 @@ 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[]>([]);
163
110
 
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);
111
+ const panelIdToLastNotifiedMixedSizesMapRef = useRef<
112
+ Record<string, MixedSizes>
113
+ >({});
114
+ const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
115
+ const prevDeltaRef = useRef<number>(0);
116
+
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);
269
+ if (groupSizePixels <= 0) {
270
+ // Wait until the group has rendered a non-zero size before computing layout.
271
+ return;
272
+ }
330
273
 
331
- setSizes(validatedSizes);
332
- } else {
333
- const sizes = calculateDefaultLayout({
334
- groupId,
335
- panels,
336
- units,
274
+ if (unsafeLayout == null) {
275
+ unsafeLayout = calculateUnsafeDefaultLayout({
276
+ groupSizePixels,
277
+ panelDataArray,
337
278
  });
279
+ }
280
+
281
+ // Validate even saved layouts in case something has changed since last render
282
+ // e.g. for pixel groups, this could be the size of the window
283
+ const validatedLayout = validatePanelGroupLayout({
284
+ groupSizePixels,
285
+ layout: unsafeLayout,
286
+ panelConstraints: panelDataArray.map(
287
+ (panelData) => panelData.constraints
288
+ ),
289
+ });
338
290
 
339
- setSizes(sizes);
291
+ if (!areEqual(layout, validatedLayout)) {
292
+ setLayout(validatedLayout);
340
293
  }
341
- }, [autoSaveId, panels, storage]);
342
294
 
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
- }
295
+ if (onLayout) {
296
+ onLayout(
297
+ validatedLayout.map((sizePercentage) => ({
298
+ sizePercentage,
299
+ sizePixels: convertPercentageToPixels(
300
+ sizePercentage,
301
+ groupSizePixels
302
+ ),
303
+ }))
304
+ );
305
+ }
349
306
 
350
- const panelsArray = panelsMapToSortedArray(panels);
307
+ callPanelCallbacks(
308
+ groupId,
309
+ panelDataArray,
310
+ validatedLayout,
311
+ panelIdToLastNotifiedMixedSizesMapRef.current
312
+ );
313
+ }, [autoSaveId, layout, panelDataArray, storage]);
351
314
 
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);
315
+ useIsomorphicLayoutEffect(() => {
316
+ const constraints = panelDataArray.map(({ constraints }) => constraints);
317
+ if (!shouldMonitorPixelBasedConstraints(constraints)) {
318
+ // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
319
+ return;
357
320
  }
358
321
 
322
+ if (typeof ResizeObserver === "undefined") {
323
+ console.warn(
324
+ `WARNING: Pixel based constraints require ResizeObserver but it is not supported by the current browser.`
325
+ );
326
+ } else {
327
+ const resizeObserver = new ResizeObserver(() => {
328
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
329
+
330
+ const { layout: prevLayout, onLayout } = committedValuesRef.current;
331
+
332
+ const nextLayout = validatePanelGroupLayout({
333
+ groupSizePixels,
334
+ layout: prevLayout,
335
+ panelConstraints: panelDataArray.map(
336
+ (panelData) => panelData.constraints
337
+ ),
338
+ });
339
+
340
+ if (!areEqual(prevLayout, nextLayout)) {
341
+ setLayout(nextLayout);
342
+
343
+ if (onLayout) {
344
+ onLayout(
345
+ nextLayout.map((sizePercentage) => ({
346
+ sizePercentage,
347
+ sizePixels: convertPercentageToPixels(
348
+ sizePercentage,
349
+ groupSizePixels
350
+ ),
351
+ }))
352
+ );
353
+ }
354
+
355
+ callPanelCallbacks(
356
+ groupId,
357
+ panelDataArray,
358
+ nextLayout,
359
+ panelIdToLastNotifiedMixedSizesMapRef.current
360
+ );
361
+ }
362
+ });
363
+
364
+ resizeObserver.observe(getPanelGroupElement(groupId)!);
365
+
366
+ return () => {
367
+ resizeObserver.disconnect();
368
+ };
369
+ }
370
+ }, [groupId, panelDataArray]);
371
+
372
+ // DEV warnings
373
+ useEffect(() => {
359
374
  if (isDevelopment) {
360
- const { didLogIdAndOrderWarning, prevPanelIds } = devWarningsRef.current;
375
+ const {
376
+ didLogIdAndOrderWarning,
377
+ didLogPanelConstraintsWarning,
378
+ prevPanelIds,
379
+ } = devWarningsRef.current;
380
+
361
381
  if (!didLogIdAndOrderWarning) {
362
- const { panels } = committedValuesRef.current;
382
+ const { panelDataArray } = committedValuesRef.current;
363
383
 
364
- const panelIds = Array.from(panels.keys());
384
+ const panelIds = panelDataArray.map(({ id }) => id);
365
385
 
366
386
  devWarningsRef.current.prevPanelIds = panelIds;
367
387
 
@@ -369,9 +389,8 @@ function PanelGroupWithForwardedRef({
369
389
  prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
370
390
  if (panelsHaveChanged) {
371
391
  if (
372
- Array.from(panels.values()).find(
373
- (panel) =>
374
- panel.current.idWasAutoGenerated || panel.current.order == null
392
+ panelDataArray.find(
393
+ ({ idIsFromProps, order }) => !idIsFromProps || order == null
375
394
  )
376
395
  ) {
377
396
  devWarningsRef.current.didLogIdAndOrderWarning = true;
@@ -382,512 +401,509 @@ function PanelGroupWithForwardedRef({
382
401
  }
383
402
  }
384
403
  }
385
- }
386
- }, [autoSaveId, panels, sizes, storage]);
387
404
 
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;
405
+ if (!didLogPanelConstraintsWarning) {
406
+ const panelConstraints = panelDataArray.map(
407
+ (panelData) => panelData.constraints
408
+ );
394
409
 
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
- });
410
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
406
411
 
407
- resizeObserver.observe(getPanelGroup(groupId)!);
412
+ for (
413
+ let panelIndex = 0;
414
+ panelIndex < panelConstraints.length;
415
+ panelIndex++
416
+ ) {
417
+ const isValid = validatePanelConstraints({
418
+ groupSizePixels,
419
+ panelConstraints,
420
+ panelId: panelDataArray[panelIndex].id,
421
+ panelIndex,
422
+ });
408
423
 
409
- return () => {
410
- resizeObserver.disconnect();
411
- };
412
- }
413
- }, [groupId, units]);
424
+ if (!isValid) {
425
+ devWarningsRef.current.didLogPanelConstraintsWarning = true;
414
426
 
415
- const getPanelSize = useCallback(
416
- (id: string, unitsFromParams?: Units) => {
417
- const { panels, units: unitsFromProps } = committedValuesRef.current;
427
+ break;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ });
418
433
 
419
- const panelsArray = panelsMapToSortedArray(panels);
434
+ // External APIs are safe to memoize via committed values ref
435
+ const collapsePanel = useCallback(
436
+ (panelData: PanelData) => {
437
+ const {
438
+ layout: prevLayout,
439
+ onLayout,
440
+ panelDataArray,
441
+ } = committedValuesRef.current;
420
442
 
421
- const index = panelsArray.findIndex((panel) => panel.current.id === id);
422
- const size = sizes[index];
443
+ if (panelData.constraints.collapsible) {
444
+ const panelConstraintsArray = panelDataArray.map(
445
+ (panelData) => panelData.constraints
446
+ );
423
447
 
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
- );
448
+ const {
449
+ collapsedSizePercentage,
450
+ panelSizePercentage,
451
+ pivotIndices,
452
+ groupSizePixels,
453
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
454
+
455
+ if (panelSizePercentage !== collapsedSizePercentage) {
456
+ // Store size before collapse;
457
+ // This is the size that gets restored if the expand() API is used.
458
+ panelSizeBeforeCollapseRef.current.set(
459
+ panelData.id,
460
+ panelSizePercentage
461
+ );
434
462
 
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`
463
+ const isLastPanel =
464
+ panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
465
+ const delta = isLastPanel
466
+ ? panelSizePercentage - collapsedSizePercentage
467
+ : collapsedSizePercentage - panelSizePercentage;
468
+
469
+ const nextLayout = adjustLayoutByDelta({
470
+ delta,
471
+ groupSizePixels,
472
+ layout: prevLayout,
473
+ panelConstraints: panelConstraintsArray,
474
+ pivotIndices,
475
+ trigger: "imperative-api",
476
+ });
477
+
478
+ if (!compareLayouts(prevLayout, nextLayout)) {
479
+ setLayout(nextLayout);
480
+
481
+ if (onLayout) {
482
+ onLayout(
483
+ nextLayout.map((sizePercentage) => ({
484
+ sizePercentage,
485
+ sizePixels: convertPercentageToPixels(
486
+ sizePercentage,
487
+ groupSizePixels
488
+ ),
489
+ }))
449
490
  );
450
491
  }
492
+
493
+ callPanelCallbacks(
494
+ groupId,
495
+ panelDataArray,
496
+ nextLayout,
497
+ panelIdToLastNotifiedMixedSizesMapRef.current
498
+ );
451
499
  }
452
500
  }
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
501
  }
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
502
  },
482
- [activeHandleId, disablePointerEventsDuringResize, sizes]
503
+ [groupId]
483
504
  );
484
505
 
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
- }
506
+ // External APIs are safe to memoize via committed values ref
507
+ const expandPanel = useCallback(
508
+ (panelData: PanelData) => {
509
+ const {
510
+ layout: prevLayout,
511
+ onLayout,
512
+ panelDataArray,
513
+ } = committedValuesRef.current;
545
514
 
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
515
+ if (panelData.constraints.collapsible) {
516
+ const panelConstraintsArray = panelDataArray.map(
517
+ (panelData) => panelData.constraints
560
518
  );
561
519
 
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");
520
+ const {
521
+ collapsedSizePercentage,
522
+ panelSizePercentage,
523
+ minSizePercentage,
524
+ pivotIndices,
525
+ groupSizePixels,
526
+ } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
527
+
528
+ if (panelSizePercentage === collapsedSizePercentage) {
529
+ // Restore this panel to the size it was before it was collapsed, if possible.
530
+ const prevPanelSizePercentage =
531
+ panelSizeBeforeCollapseRef.current.get(panelData.id);
532
+
533
+ const baseSizePercentage =
534
+ prevPanelSizePercentage != null
535
+ ? prevPanelSizePercentage
536
+ : minSizePercentage;
537
+
538
+ const isLastPanel =
539
+ panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
540
+ const delta = isLastPanel
541
+ ? panelSizePercentage - baseSizePercentage
542
+ : baseSizePercentage - panelSizePercentage;
543
+
544
+ const nextLayout = adjustLayoutByDelta({
545
+ delta,
546
+ groupSizePixels,
547
+ layout: prevLayout,
548
+ panelConstraints: panelConstraintsArray,
549
+ pivotIndices,
550
+ trigger: "imperative-api",
551
+ });
552
+
553
+ if (!compareLayouts(prevLayout, nextLayout)) {
554
+ setLayout(nextLayout);
555
+
556
+ if (onLayout) {
557
+ onLayout(
558
+ nextLayout.map((sizePercentage) => ({
559
+ sizePercentage,
560
+ sizePixels: convertPercentageToPixels(
561
+ sizePercentage,
562
+ groupSizePixels
563
+ ),
564
+ }))
565
+ );
587
566
  }
567
+
568
+ callPanelCallbacks(
569
+ groupId,
570
+ panelDataArray,
571
+ nextLayout,
572
+ panelIdToLastNotifiedMixedSizesMapRef.current
573
+ );
588
574
  }
589
575
  }
576
+ }
577
+ },
578
+ [groupId]
579
+ );
590
580
 
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);
581
+ // External APIs are safe to memoize via committed values ref
582
+ const getPanelSize = useCallback(
583
+ (panelData: PanelData) => {
584
+ const { layout, panelDataArray } = committedValuesRef.current;
597
585
 
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
- }
586
+ const { panelSizePercentage, panelSizePixels } = panelDataHelper(
587
+ groupId,
588
+ panelDataArray,
589
+ panelData,
590
+ layout
591
+ );
606
592
 
607
- prevDeltaRef.current = delta;
593
+ return {
594
+ sizePercentage: panelSizePercentage,
595
+ sizePixels: panelSizePixels,
608
596
  };
609
-
610
- return resizeHandler;
611
597
  },
612
598
  [groupId]
613
599
  );
614
600
 
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);
601
+ // This API should never read from committedValuesRef
602
+ const getPanelStyle = useCallback(
603
+ (panelData: PanelData) => {
604
+ const panelIndex = panelDataArray.indexOf(panelData);
605
+
606
+ return computePanelFlexBoxStyle({
607
+ dragState,
608
+ layout,
609
+ panelData: panelDataArray,
610
+ panelIndex,
611
+ });
612
+ },
613
+ [dragState, layout, panelDataArray]
614
+ );
642
615
 
643
- const index = panelsArray.indexOf(panel);
644
- if (index < 0) {
645
- return;
646
- }
616
+ // External APIs are safe to memoize via committed values ref
617
+ const isPanelCollapsed = useCallback(
618
+ (panelData: PanelData) => {
619
+ const { layout, panelDataArray } = committedValuesRef.current;
647
620
 
648
- const currentSize = prevSizes[index];
649
- if (currentSize === collapsedSize) {
650
- // Panel is already collapsed.
651
- return;
652
- }
621
+ const { collapsedSizePercentage, collapsible, panelSizePercentage } =
622
+ panelDataHelper(groupId, panelDataArray, panelData, layout);
653
623
 
654
- panelSizeBeforeCollapse.current.set(id, currentSize);
624
+ return (
625
+ collapsible === true && panelSizePercentage === collapsedSizePercentage
626
+ );
627
+ },
628
+ [groupId]
629
+ );
655
630
 
656
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
657
- if (idBefore == null || idAfter == null) {
658
- return;
659
- }
631
+ // External APIs are safe to memoize via committed values ref
632
+ const isPanelExpanded = useCallback(
633
+ (panelData: PanelData) => {
634
+ const { layout, panelDataArray } = committedValuesRef.current;
660
635
 
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;
636
+ const { collapsedSizePercentage, collapsible, panelSizePercentage } =
637
+ panelDataHelper(groupId, panelDataArray, panelData, layout);
677
638
 
678
- setSizes(nextSizes);
639
+ return !collapsible || panelSizePercentage > collapsedSizePercentage;
640
+ },
641
+ [groupId]
642
+ );
679
643
 
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
- }
644
+ const registerPanel = useCallback((panelData: PanelData) => {
645
+ setPanelDataArray((prevPanelDataArray) => {
646
+ const nextPanelDataArray = [...prevPanelDataArray, panelData];
647
+ return nextPanelDataArray.sort((panelA, panelB) => {
648
+ const orderA = panelA.order;
649
+ const orderB = panelB.order;
650
+ if (orderA == null && orderB == null) {
651
+ return 0;
652
+ } else if (orderA == null) {
653
+ return -1;
654
+ } else if (orderB == null) {
655
+ return 1;
656
+ } else {
657
+ return orderA - orderB;
658
+ }
659
+ });
660
+ });
684
661
  }, []);
685
662
 
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);
740
-
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
- }, []);
663
+ const registerResizeHandle = useCallback((dragHandleId: string) => {
664
+ return function resizeHandler(event: ResizeEvent) {
665
+ event.preventDefault();
746
666
 
747
- const resizePanel = useCallback(
748
- (id: string, nextSize: number, unitsFromParams?: Units) => {
749
667
  const {
668
+ direction,
669
+ dragState,
750
670
  id: groupId,
751
- panels,
752
- sizes: prevSizes,
753
- units,
671
+ keyboardResizeByPercentage,
672
+ keyboardResizeByPixels,
673
+ onLayout,
674
+ panelDataArray,
675
+ layout: prevLayout,
754
676
  } = committedValuesRef.current;
755
677
 
756
- if ((unitsFromParams || units) === "pixels") {
757
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
758
- nextSize = (nextSize / groupSizePixels) * 100;
759
- }
678
+ const { initialLayout } = dragState ?? {};
760
679
 
761
- const panel = panels.get(id);
762
- if (panel == null) {
763
- return;
764
- }
765
-
766
- let { collapsedSize, collapsible, maxSize, minSize } = panel.current;
680
+ const pivotIndices = determinePivotIndices(groupId, dragHandleId);
767
681
 
768
- if (units === "pixels") {
769
- const groupSizePixels = getAvailableGroupSizePixels(groupId);
770
- minSize = (minSize / groupSizePixels) * 100;
771
- if (maxSize != null) {
772
- maxSize = (maxSize / groupSizePixels) * 100;
682
+ let delta = calculateDeltaPercentage(
683
+ event,
684
+ groupId,
685
+ dragHandleId,
686
+ direction,
687
+ dragState!,
688
+ {
689
+ percentage: keyboardResizeByPercentage,
690
+ pixels: keyboardResizeByPixels,
773
691
  }
774
- }
775
-
776
- const panelsArray = panelsMapToSortedArray(panels);
777
-
778
- const index = panelsArray.indexOf(panel);
779
- if (index < 0) {
692
+ );
693
+ if (delta === 0) {
780
694
  return;
781
695
  }
782
696
 
783
- const currentSize = prevSizes[index];
784
- if (currentSize === nextSize) {
785
- return;
697
+ // Support RTL layouts
698
+ const isHorizontal = direction === "horizontal";
699
+ if (document.dir === "rtl" && isHorizontal) {
700
+ delta = -delta;
786
701
  }
787
702
 
788
- if (collapsible && nextSize === collapsedSize) {
789
- // This is a valid resize state.
790
- } else {
791
- const unsafeNextSize = nextSize;
703
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
704
+ const panelConstraints = panelDataArray.map(
705
+ (panelData) => panelData.constraints
706
+ );
792
707
 
793
- nextSize = Math.min(
794
- maxSize != null ? maxSize : 100,
795
- Math.max(minSize, nextSize)
796
- );
708
+ const nextLayout = adjustLayoutByDelta({
709
+ delta,
710
+ groupSizePixels,
711
+ layout: initialLayout ?? prevLayout,
712
+ panelConstraints,
713
+ pivotIndices,
714
+ trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch",
715
+ });
797
716
 
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
- );
717
+ const layoutChanged = !compareLayouts(prevLayout, nextLayout);
718
+
719
+ // Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
720
+ // Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
721
+ if (isMouseEvent(event) || isTouchEvent(event)) {
722
+ // Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
723
+ // In this case, Panel sizes might not change–
724
+ // but updating cursor in this scenario would cause a flicker.
725
+ if (prevDeltaRef.current != delta) {
726
+ prevDeltaRef.current = delta;
727
+
728
+ if (!layoutChanged) {
729
+ // If the pointer has moved too far to resize the panel any further,
730
+ // update the cursor style for a visual clue.
731
+ // This mimics VS Code behavior.
732
+
733
+ if (isHorizontal) {
734
+ setGlobalCursorStyle(
735
+ delta < 0 ? "horizontal-min" : "horizontal-max"
736
+ );
737
+ } else {
738
+ setGlobalCursorStyle(delta < 0 ? "vertical-min" : "vertical-max");
739
+ }
740
+ } else {
741
+ // Reset the cursor style to the the normal resize cursor.
742
+ setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical");
803
743
  }
804
744
  }
805
745
  }
806
746
 
807
- const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
808
- if (idBefore == null || idAfter == null) {
809
- return;
747
+ if (layoutChanged) {
748
+ setLayout(nextLayout);
749
+
750
+ if (onLayout) {
751
+ onLayout(
752
+ nextLayout.map((sizePercentage) => ({
753
+ sizePercentage,
754
+ sizePixels: convertPercentageToPixels(
755
+ sizePercentage,
756
+ groupSizePixels
757
+ ),
758
+ }))
759
+ );
760
+ }
761
+
762
+ callPanelCallbacks(
763
+ groupId,
764
+ panelDataArray,
765
+ nextLayout,
766
+ panelIdToLastNotifiedMixedSizesMapRef.current
767
+ );
810
768
  }
769
+ };
770
+ }, []);
811
771
 
812
- const isLastPanel = index === panelsArray.length - 1;
772
+ // External APIs are safe to memoize via committed values ref
773
+ const resizePanel = useCallback(
774
+ (panelData: PanelData, mixedSizes: Partial<MixedSizes>) => {
775
+ const {
776
+ layout: prevLayout,
777
+ onLayout,
778
+ panelDataArray,
779
+ } = committedValuesRef.current;
780
+
781
+ const panelConstraintsArray = panelDataArray.map(
782
+ (panelData) => panelData.constraints
783
+ );
784
+
785
+ const { groupSizePixels, panelSizePercentage, pivotIndices } =
786
+ panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
787
+
788
+ const sizePercentage = getPercentageSizeFromMixedSizes(
789
+ mixedSizes,
790
+ groupSizePixels
791
+ )!;
792
+
793
+ const isLastPanel =
794
+ panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
813
795
  const delta = isLastPanel
814
- ? currentSize - nextSize
815
- : nextSize - currentSize;
816
-
817
- const nextSizes = adjustByDelta(
818
- null,
819
- committedValuesRef.current,
820
- idBefore,
821
- idAfter,
796
+ ? panelSizePercentage - sizePercentage
797
+ : sizePercentage - panelSizePercentage;
798
+
799
+ const nextLayout = adjustLayoutByDelta({
822
800
  delta,
823
- prevSizes,
824
- panelSizeBeforeCollapse.current,
825
- null
826
- );
827
- if (prevSizes !== nextSizes) {
828
- const panelIdToLastNotifiedSizeMap =
829
- panelIdToLastNotifiedSizeMapRef.current;
801
+ groupSizePixels,
802
+ layout: prevLayout,
803
+ panelConstraints: panelConstraintsArray,
804
+ pivotIndices,
805
+ trigger: "imperative-api",
806
+ });
830
807
 
831
- setSizes(nextSizes);
808
+ if (!compareLayouts(prevLayout, nextLayout)) {
809
+ setLayout(nextLayout);
810
+
811
+ if (onLayout) {
812
+ onLayout(
813
+ nextLayout.map((sizePercentage) => ({
814
+ sizePercentage,
815
+ sizePixels: convertPercentageToPixels(
816
+ sizePercentage,
817
+ groupSizePixels
818
+ ),
819
+ }))
820
+ );
821
+ }
832
822
 
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
823
  callPanelCallbacks(
836
- panelsArray,
837
- nextSizes,
838
- panelIdToLastNotifiedSizeMap
824
+ groupId,
825
+ panelDataArray,
826
+ nextLayout,
827
+ panelIdToLastNotifiedMixedSizesMapRef.current
839
828
  );
840
829
  }
841
830
  },
831
+ [groupId]
832
+ );
833
+
834
+ const startDragging = useCallback(
835
+ (dragHandleId: string, event: ResizeEvent) => {
836
+ const { direction, layout } = committedValuesRef.current;
837
+
838
+ const handleElement = getResizeHandleElement(dragHandleId)!;
839
+
840
+ const initialCursorPosition = getResizeEventCursorPosition(
841
+ direction,
842
+ event
843
+ );
844
+
845
+ setDragState({
846
+ dragHandleId,
847
+ dragHandleRect: handleElement.getBoundingClientRect(),
848
+ initialCursorPosition,
849
+ initialLayout: layout,
850
+ });
851
+ },
842
852
  []
843
853
  );
844
854
 
855
+ const stopDragging = useCallback(() => {
856
+ resetGlobalCursorStyle();
857
+ setDragState(null);
858
+ }, []);
859
+
860
+ const unregisterPanel = useCallback((panelData: PanelData) => {
861
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
862
+
863
+ setPanelDataArray((panelDataArray) => {
864
+ const index = panelDataArray.indexOf(panelData);
865
+ if (index >= 0) {
866
+ panelDataArray = [...panelDataArray];
867
+ panelDataArray.splice(index, 1);
868
+ }
869
+
870
+ return panelDataArray;
871
+ });
872
+ }, []);
873
+
845
874
  const context = useMemo(
846
875
  () => ({
847
- activeHandleId,
848
876
  collapsePanel,
849
877
  direction,
878
+ dragState,
850
879
  expandPanel,
851
880
  getPanelSize,
852
881
  getPanelStyle,
853
882
  groupId,
883
+ isPanelCollapsed,
884
+ isPanelExpanded,
854
885
  registerPanel,
855
886
  registerResizeHandle,
856
887
  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,
888
+ startDragging,
889
+ stopDragging,
877
890
  unregisterPanel,
878
891
  }),
879
892
  [
880
- activeHandleId,
881
893
  collapsePanel,
894
+ dragState,
882
895
  direction,
883
896
  expandPanel,
884
897
  getPanelSize,
885
898
  getPanelStyle,
886
899
  groupId,
900
+ isPanelCollapsed,
901
+ isPanelExpanded,
887
902
  registerPanel,
888
903
  registerResizeHandle,
889
904
  resizePanel,
890
- units,
905
+ startDragging,
906
+ stopDragging,
891
907
  unregisterPanel,
892
908
  ]
893
909
  );
@@ -900,18 +916,23 @@ function PanelGroupWithForwardedRef({
900
916
  width: "100%",
901
917
  };
902
918
 
903
- return createElement(PanelGroupContext.Provider, {
904
- children: createElement(Type, {
919
+ return createElement(
920
+ PanelGroupContext.Provider,
921
+ { value: context },
922
+ createElement(Type, {
905
923
  children,
906
924
  className: classNameFromProps,
925
+ style: {
926
+ ...style,
927
+ ...styleFromProps,
928
+ },
929
+
930
+ // CSS selectors
907
931
  "data-panel-group": "",
908
932
  "data-panel-group-direction": direction,
909
933
  "data-panel-group-id": groupId,
910
- "data-panel-group-units": units,
911
- style: { ...style, ...styleFromProps },
912
- }),
913
- value: context,
914
- });
934
+ })
935
+ );
915
936
  }
916
937
 
917
938
  export const PanelGroup = forwardRef<
@@ -923,3 +944,45 @@ export const PanelGroup = forwardRef<
923
944
 
924
945
  PanelGroupWithForwardedRef.displayName = "PanelGroup";
925
946
  PanelGroup.displayName = "forwardRef(PanelGroup)";
947
+
948
+ function panelDataHelper(
949
+ groupId: string,
950
+ panelDataArray: PanelData[],
951
+ panelData: PanelData,
952
+ layout: number[]
953
+ ) {
954
+ const panelConstraintsArray = panelDataArray.map(
955
+ (panelData) => panelData.constraints
956
+ );
957
+
958
+ const panelIndex = panelDataArray.indexOf(panelData);
959
+ const panelConstraints = panelConstraintsArray[panelIndex];
960
+
961
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
962
+
963
+ const percentagePanelConstraints = computePercentagePanelConstraints(
964
+ panelConstraintsArray,
965
+ panelIndex,
966
+ groupSizePixels
967
+ );
968
+
969
+ const isLastPanel = panelIndex === panelDataArray.length - 1;
970
+ const pivotIndices = isLastPanel
971
+ ? [panelIndex - 1, panelIndex]
972
+ : [panelIndex, panelIndex + 1];
973
+
974
+ const panelSizePercentage = layout[panelIndex];
975
+ const panelSizePixels = convertPercentageToPixels(
976
+ panelSizePercentage,
977
+ groupSizePixels
978
+ );
979
+
980
+ return {
981
+ ...percentagePanelConstraints,
982
+ collapsible: panelConstraints.collapsible,
983
+ panelSizePercentage,
984
+ panelSizePixels,
985
+ groupSizePixels,
986
+ pivotIndices,
987
+ };
988
+ }