react-resizable-panels 0.0.32 → 0.0.34

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.
package/src/PanelGroup.ts CHANGED
@@ -11,15 +11,27 @@ import {
11
11
  } from "react";
12
12
 
13
13
  import { PanelGroupContext } from "./PanelContexts";
14
- import { Direction, PanelData, PanelGroupOnLayout, ResizeEvent } from "./types";
14
+ import {
15
+ Direction,
16
+ PanelData,
17
+ PanelGroupOnLayout,
18
+ ResizeEvent,
19
+ PanelGroupStorage,
20
+ } from "./types";
15
21
  import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization";
16
- import { getDragOffset, getMovement } from "./utils/coordinates";
22
+ import {
23
+ getDragOffset,
24
+ getMovement,
25
+ isMouseEvent,
26
+ isTouchEvent,
27
+ } from "./utils/coordinates";
17
28
  import {
18
29
  adjustByDelta,
19
30
  callPanelCallbacks,
20
31
  getBeforeAndAfterIds,
21
32
  getFlexGrow,
22
33
  getPanelGroup,
34
+ getResizeHandle,
23
35
  getResizeHandlePanelIds,
24
36
  panelsMapToSortedArray,
25
37
  } from "./utils/group";
@@ -28,10 +40,26 @@ import useUniqueId from "./hooks/useUniqueId";
28
40
  import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterBehavior";
29
41
  import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
30
42
  import debounce from "./utils/debounce";
43
+ import { areEqual } from "./utils/arrays";
31
44
 
32
45
  // Limit the frequency of localStorage updates.
33
46
  const savePanelGroupLayoutDebounced = debounce(savePanelGroupLayout, 100);
34
47
 
48
+ function throwServerError() {
49
+ throw new Error('PanelGroup "storage" prop required for server rendering.');
50
+ }
51
+
52
+ const defaultStorage: PanelGroupStorage = {
53
+ getItem:
54
+ typeof localStorage !== "undefined"
55
+ ? (name: string) => localStorage.getItem(name)
56
+ : (throwServerError as any),
57
+ setItem:
58
+ typeof localStorage !== "undefined"
59
+ ? (name: string, value: string) => localStorage.setItem(name, value)
60
+ : (throwServerError as any),
61
+ };
62
+
35
63
  export type CommittedValues = {
36
64
  direction: Direction;
37
65
  panels: Map<string, PanelData>;
@@ -40,6 +68,19 @@ export type CommittedValues = {
40
68
 
41
69
  export type PanelDataMap = Map<string, PanelData>;
42
70
 
71
+ // Initial drag state serves a few purposes:
72
+ // * dragOffset:
73
+ // Resize is calculated by the distance between the current pointer event and the resize handle being "dragged"
74
+ // This value accounts for the initial offset when the touch/click starts, so the handle doesn't appear to "jump"
75
+ // * dragHandleRect, sizes:
76
+ // When resizing is done via mouse/touch event– some initial state is stored
77
+ // so that any panels that contract will also expand if drag direction is reversed.
78
+ export type InitialDragState = {
79
+ dragHandleRect: DOMRect;
80
+ dragOffset: number;
81
+ sizes: number[];
82
+ };
83
+
43
84
  // TODO
44
85
  // Within an active drag, remember original positions to refine more easily on expand.
45
86
  // Look at what the Chrome devtools Sources does.
@@ -51,6 +92,7 @@ export type PanelGroupProps = {
51
92
  direction: Direction;
52
93
  id?: string | null;
53
94
  onLayout?: PanelGroupOnLayout;
95
+ storage?: PanelGroupStorage;
54
96
  style?: CSSProperties;
55
97
  tagName?: ElementType;
56
98
  };
@@ -62,6 +104,7 @@ export function PanelGroup({
62
104
  direction,
63
105
  id: idFromProps = null,
64
106
  onLayout = null,
107
+ storage = defaultStorage,
65
108
  style: styleFromProps = {},
66
109
  tagName: Type = "div",
67
110
  }: PanelGroupProps) {
@@ -70,6 +113,11 @@ export function PanelGroup({
70
113
  const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
71
114
  const [panels, setPanels] = useState<PanelDataMap>(new Map());
72
115
 
116
+ // When resizing is done via mouse/touch event–
117
+ // We store the initial Panel sizes in this ref, and apply move deltas to them instead of to the current sizes.
118
+ // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
119
+ const initialDragStateRef = useRef<InitialDragState | null>(null);
120
+
73
121
  // Use a ref to guard against users passing inline props
74
122
  const callbacksRef = useRef<{
75
123
  onLayout: PanelGroupOnLayout | null;
@@ -81,13 +129,11 @@ export function PanelGroup({
81
129
  // 0-1 values representing the relative size of each panel.
82
130
  const [sizes, setSizes] = useState<number[]>([]);
83
131
 
84
- // Resize is calculated by the distance between the current pointer event and the resize handle being "dragged"
85
- // This value accounts for the initial offset when the touch/click starts, so the handle doesn't appear to "jump"
86
- const dragOffsetRef = useRef<number>(0);
87
-
88
132
  // Used to support imperative collapse/expand API.
89
133
  const panelSizeBeforeCollapse = useRef<Map<string, number>>(new Map());
90
134
 
135
+ const prevDeltaRef = useRef<number>(0);
136
+
91
137
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
92
138
  const committedValuesRef = useRef<CommittedValues>({
93
139
  direction,
@@ -155,7 +201,7 @@ export function PanelGroup({
155
201
  let defaultSizes: number[] | undefined = undefined;
156
202
  if (autoSaveId) {
157
203
  const panelsArray = panelsMapToSortedArray(panels);
158
- defaultSizes = loadPanelLayout(autoSaveId, panelsArray);
204
+ defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
159
205
  }
160
206
 
161
207
  if (defaultSizes != null) {
@@ -213,7 +259,7 @@ export function PanelGroup({
213
259
 
214
260
  const panelsArray = panelsMapToSortedArray(panels);
215
261
 
216
- savePanelGroupLayoutDebounced(autoSaveId, panelsArray, sizes);
262
+ savePanelGroupLayoutDebounced(autoSaveId, panelsArray, sizes, storage);
217
263
  }
218
264
  }, [autoSaveId, panels, sizes]);
219
265
 
@@ -295,7 +341,7 @@ export function PanelGroup({
295
341
  panelsArray,
296
342
  direction,
297
343
  prevSizes,
298
- dragOffsetRef.current
344
+ initialDragStateRef.current
299
345
  );
300
346
  if (movement === 0) {
301
347
  return;
@@ -308,35 +354,53 @@ export function PanelGroup({
308
354
  const delta = (movement / size) * 100;
309
355
 
310
356
  const nextSizes = adjustByDelta(
357
+ event,
311
358
  panels,
312
359
  idBefore,
313
360
  idAfter,
314
361
  delta,
315
362
  prevSizes,
316
- panelSizeBeforeCollapse.current
363
+ panelSizeBeforeCollapse.current,
364
+ initialDragStateRef.current
317
365
  );
318
- if (prevSizes === nextSizes) {
319
- // If the pointer has moved too far to resize the panel any further,
320
- // update the cursor style for a visual clue.
321
- // This mimics VS Code behavior.
322
- if (isHorizontal) {
323
- setGlobalCursorStyle(
324
- movement < 0 ? "horizontal-min" : "horizontal-max"
325
- );
326
- } else {
327
- setGlobalCursorStyle(
328
- movement < 0 ? "vertical-min" : "vertical-max"
329
- );
366
+
367
+ const sizesChanged = !areEqual(prevSizes, nextSizes);
368
+
369
+ // Don't update cursor for resizes triggered by keyboard interactions.
370
+ if (isMouseEvent(event) || isTouchEvent(event)) {
371
+ // Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
372
+ // In this case, Panel sizes might not change–
373
+ // but updating cursor in this scenario would cause a flicker.
374
+ if (prevDeltaRef.current != delta) {
375
+ if (!sizesChanged) {
376
+ // If the pointer has moved too far to resize the panel any further,
377
+ // update the cursor style for a visual clue.
378
+ // This mimics VS Code behavior.
379
+
380
+ if (isHorizontal) {
381
+ setGlobalCursorStyle(
382
+ movement < 0 ? "horizontal-min" : "horizontal-max"
383
+ );
384
+ } else {
385
+ setGlobalCursorStyle(
386
+ movement < 0 ? "vertical-min" : "vertical-max"
387
+ );
388
+ }
389
+ } else {
390
+ // Reset the cursor style to the the normal resize cursor.
391
+ setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical");
392
+ }
330
393
  }
331
- } else {
332
- // Reset the cursor style to the the normal resize cursor.
333
- setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical");
394
+ }
334
395
 
396
+ if (sizesChanged) {
335
397
  // If resize change handlers have been declared, this is the time to call them.
336
398
  callPanelCallbacks(panelsArray, prevSizes, nextSizes);
337
399
 
338
400
  setSizes(nextSizes);
339
401
  }
402
+
403
+ prevDeltaRef.current = delta;
340
404
  };
341
405
 
342
406
  return resizeHandler;
@@ -389,12 +453,14 @@ export function PanelGroup({
389
453
  const delta = isLastPanel ? currentSize : 0 - currentSize;
390
454
 
391
455
  const nextSizes = adjustByDelta(
456
+ null,
392
457
  panels,
393
458
  idBefore,
394
459
  idAfter,
395
460
  delta,
396
461
  prevSizes,
397
- panelSizeBeforeCollapse.current
462
+ panelSizeBeforeCollapse.current,
463
+ null
398
464
  );
399
465
  if (prevSizes !== nextSizes) {
400
466
  // If resize change handlers have been declared, this is the time to call them.
@@ -440,12 +506,14 @@ export function PanelGroup({
440
506
  const delta = isLastPanel ? 0 - sizeBeforeCollapse : sizeBeforeCollapse;
441
507
 
442
508
  const nextSizes = adjustByDelta(
509
+ null,
443
510
  panels,
444
511
  idBefore,
445
512
  idAfter,
446
513
  delta,
447
514
  prevSizes,
448
- panelSizeBeforeCollapse.current
515
+ panelSizeBeforeCollapse.current,
516
+ null
449
517
  );
450
518
  if (prevSizes !== nextSizes) {
451
519
  // If resize change handlers have been declared, this is the time to call them.
@@ -475,6 +543,12 @@ export function PanelGroup({
475
543
  return;
476
544
  }
477
545
 
546
+ if (panel.collapsible && nextSize === 0) {
547
+ // This is a valid resize state.
548
+ } else {
549
+ nextSize = Math.min(panel.maxSize, Math.max(panel.minSize, nextSize));
550
+ }
551
+
478
552
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
479
553
  if (idBefore == null || idAfter == null) {
480
554
  return;
@@ -484,12 +558,14 @@ export function PanelGroup({
484
558
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
485
559
 
486
560
  const nextSizes = adjustByDelta(
561
+ null,
487
562
  panels,
488
563
  idBefore,
489
564
  idAfter,
490
565
  delta,
491
566
  prevSizes,
492
- panelSizeBeforeCollapse.current
567
+ panelSizeBeforeCollapse.current,
568
+ null
493
569
  );
494
570
  if (prevSizes !== nextSizes) {
495
571
  // If resize change handlers have been declared, this is the time to call them.
@@ -513,11 +589,21 @@ export function PanelGroup({
513
589
  startDragging: (id: string, event: ResizeEvent) => {
514
590
  setActiveHandleId(id);
515
591
 
516
- dragOffsetRef.current = getDragOffset(event, id, direction);
592
+ if (isMouseEvent(event) || isTouchEvent(event)) {
593
+ const handleElement = getResizeHandle(id);
594
+
595
+ initialDragStateRef.current = {
596
+ dragHandleRect: handleElement.getBoundingClientRect(),
597
+ dragOffset: getDragOffset(event, id, direction),
598
+ sizes: committedValuesRef.current.sizes,
599
+ };
600
+ }
517
601
  },
518
602
  stopDragging: () => {
519
603
  resetGlobalCursorStyle();
520
604
  setActiveHandleId(null);
605
+
606
+ initialDragStateRef.current = null;
521
607
  },
522
608
  unregisterPanel,
523
609
  }),
@@ -547,6 +633,7 @@ export function PanelGroup({
547
633
  children: createElement(Type, {
548
634
  children,
549
635
  className: classNameFromProps,
636
+ "data-panel-group": "",
550
637
  "data-panel-group-direction": direction,
551
638
  "data-panel-group-id": groupId,
552
639
  style: { ...style, ...styleFromProps },
@@ -102,12 +102,14 @@ export function useWindowSplitterPanelGroupBehavior({
102
102
  }
103
103
 
104
104
  const nextSizes = adjustByDelta(
105
+ event,
105
106
  panels,
106
107
  idBefore,
107
108
  idAfter,
108
109
  delta,
109
110
  sizes,
110
- panelSizeBeforeCollapse.current
111
+ panelSizeBeforeCollapse.current,
112
+ null
111
113
  );
112
114
  if (sizes !== nextSizes) {
113
115
  setSizes(nextSizes);
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { PanelResizeHandle } from "./PanelResizeHandle";
5
5
  import type { ImperativePanelHandle, PanelProps } from "./Panel";
6
6
  import type { PanelGroupProps } from "./PanelGroup";
7
7
  import type { PanelResizeHandleProps } from "./PanelResizeHandle";
8
+ import type { PanelGroupStorage } from "./types";
8
9
 
9
10
  export {
10
11
  Panel,
@@ -14,6 +15,7 @@ export {
14
15
  // TypeScript types
15
16
  ImperativePanelHandle,
16
17
  PanelGroupProps,
18
+ PanelGroupStorage,
17
19
  PanelProps,
18
20
  PanelResizeHandleProps,
19
21
  };
package/src/types.ts CHANGED
@@ -2,6 +2,11 @@ import { RefObject } from "react";
2
2
 
3
3
  export type Direction = "horizontal" | "vertical";
4
4
 
5
+ export type PanelGroupStorage = {
6
+ getItem(name: string): string | null;
7
+ setItem(name: string, value: string): void;
8
+ };
9
+
5
10
  export type PanelGroupOnLayout = (sizes: number[]) => void;
6
11
  export type PanelOnCollapse = (collapsed: boolean) => void;
7
12
  export type PanelOnResize = (size: number) => void;
@@ -0,0 +1,13 @@
1
+ export function areEqual(arrayA: any[], arrayB: any[]): boolean {
2
+ if (arrayA.length !== arrayB.length) {
3
+ return false;
4
+ }
5
+
6
+ for (let index = 0; index < arrayA.length; index++) {
7
+ if (arrayA[index] !== arrayB[index]) {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ return true;
13
+ }
@@ -1,4 +1,5 @@
1
1
  import { PRECISION } from "../constants";
2
+ import { InitialDragState } from "../PanelGroup";
2
3
  import { Direction, PanelData, ResizeEvent } from "../types";
3
4
  import {
4
5
  getPanelGroup,
@@ -20,7 +21,8 @@ export function getDragOffset(
20
21
  event: ResizeEvent,
21
22
  handleId: string,
22
23
  direction: Direction,
23
- initialOffset: number = 0
24
+ initialOffset: number = 0,
25
+ initialHandleElementRect: DOMRect | null = null
24
26
  ): number {
25
27
  const isHorizontal = direction === "horizontal";
26
28
 
@@ -35,7 +37,8 @@ export function getDragOffset(
35
37
  }
36
38
 
37
39
  const handleElement = getResizeHandle(handleId);
38
- const rect = handleElement.getBoundingClientRect();
40
+ const rect =
41
+ initialHandleElementRect || handleElement.getBoundingClientRect();
39
42
  const elementOffset = isHorizontal ? rect.left : rect.top;
40
43
 
41
44
  return pointerOffset - elementOffset - initialOffset;
@@ -48,9 +51,19 @@ export function getMovement(
48
51
  handleId: string,
49
52
  panelsArray: PanelData[],
50
53
  direction: Direction,
51
- sizes: number[],
52
- initialOffset: number
54
+ prevSizes: number[],
55
+ initialDragState: InitialDragState | null
53
56
  ): number {
57
+ const {
58
+ dragOffset = 0,
59
+ dragHandleRect,
60
+ sizes: initialSizes,
61
+ } = initialDragState || {};
62
+
63
+ // If we're resizing by mouse or touch, use the initial sizes as a base.
64
+ // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
65
+ const baseSizes = initialSizes || prevSizes;
66
+
54
67
  if (isKeyDown(event)) {
55
68
  const isHorizontal = direction === "horizontal";
56
69
 
@@ -98,10 +111,10 @@ export function getMovement(
98
111
  );
99
112
  const targetPanel = panelsArray[targetPanelIndex];
100
113
  if (targetPanel.collapsible) {
101
- const prevSize = sizes[targetPanelIndex];
114
+ const baseSize = baseSizes[targetPanelIndex];
102
115
  if (
103
- prevSize === 0 ||
104
- prevSize.toPrecision(PRECISION) ===
116
+ baseSize === 0 ||
117
+ baseSize.toPrecision(PRECISION) ===
105
118
  targetPanel.minSize.toPrecision(PRECISION)
106
119
  ) {
107
120
  movement =
@@ -113,7 +126,13 @@ export function getMovement(
113
126
 
114
127
  return movement;
115
128
  } else {
116
- return getDragOffset(event, handleId, direction, initialOffset);
129
+ return getDragOffset(
130
+ event,
131
+ handleId,
132
+ direction,
133
+ dragOffset,
134
+ dragHandleRect
135
+ );
117
136
  }
118
137
  }
119
138
 
@@ -1,21 +1,30 @@
1
1
  import { PRECISION } from "../constants";
2
- import { PanelData } from "../types";
2
+ import { InitialDragState } from "../PanelGroup";
3
+ import { PanelData, ResizeEvent } from "../types";
3
4
 
4
5
  export function adjustByDelta(
6
+ event: ResizeEvent | null,
5
7
  panels: Map<string, PanelData>,
6
8
  idBefore: string,
7
9
  idAfter: string,
8
10
  delta: number,
9
11
  prevSizes: number[],
10
- panelSizeBeforeCollapse: Map<string, number>
12
+ panelSizeBeforeCollapse: Map<string, number>,
13
+ initialDragState: InitialDragState | null
11
14
  ): number[] {
15
+ const { sizes: initialSizes } = initialDragState || {};
16
+
17
+ // If we're resizing by mouse or touch, use the initial sizes as a base.
18
+ // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
19
+ const baseSizes = initialSizes || prevSizes;
20
+
12
21
  if (delta === 0) {
13
- return prevSizes;
22
+ return baseSizes;
14
23
  }
15
24
 
16
25
  const panelsArray = panelsMapToSortedArray(panels);
17
26
 
18
- const nextSizes = prevSizes.concat();
27
+ const nextSizes = baseSizes.concat();
19
28
 
20
29
  let deltaApplied = 0;
21
30
 
@@ -32,17 +41,18 @@ export function adjustByDelta(
32
41
  const pivotId = delta < 0 ? idAfter : idBefore;
33
42
  const index = panelsArray.findIndex((panel) => panel.id === pivotId);
34
43
  const panel = panelsArray[index];
35
- const prevSize = prevSizes[index];
44
+ const baseSize = baseSizes[index];
36
45
 
37
- const nextSize = safeResizePanel(panel, Math.abs(delta), prevSize);
38
- if (prevSize === nextSize) {
39
- return prevSizes;
46
+ const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
47
+ if (baseSize === nextSize) {
48
+ // If there's no room for the pivot panel to grow, we can ignore this drag update.
49
+ return baseSizes;
40
50
  } else {
41
- if (nextSize === 0 && prevSize > 0) {
42
- panelSizeBeforeCollapse.set(pivotId, prevSize);
51
+ if (nextSize === 0 && baseSize > 0) {
52
+ panelSizeBeforeCollapse.set(pivotId, baseSize);
43
53
  }
44
54
 
45
- delta = delta < 0 ? prevSize - nextSize : nextSize - prevSize;
55
+ delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
46
56
  }
47
57
  }
48
58
 
@@ -50,19 +60,32 @@ export function adjustByDelta(
50
60
  let index = panelsArray.findIndex((panel) => panel.id === pivotId);
51
61
  while (true) {
52
62
  const panel = panelsArray[index];
53
- const prevSize = prevSizes[index];
54
-
55
- const nextSize = safeResizePanel(panel, 0 - Math.abs(delta), prevSize);
56
- if (prevSize !== nextSize) {
57
- if (nextSize === 0 && prevSize > 0) {
58
- panelSizeBeforeCollapse.set(panel.id, prevSize);
63
+ const baseSize = baseSizes[index];
64
+
65
+ const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
66
+
67
+ const nextSize = safeResizePanel(
68
+ panel,
69
+ 0 - deltaRemaining,
70
+ baseSize,
71
+ event
72
+ );
73
+ if (baseSize !== nextSize) {
74
+ if (nextSize === 0 && baseSize > 0) {
75
+ panelSizeBeforeCollapse.set(panel.id, baseSize);
59
76
  }
60
77
 
61
- deltaApplied += prevSize - nextSize;
78
+ deltaApplied += baseSize - nextSize;
62
79
 
63
80
  nextSizes[index] = nextSize;
64
81
 
65
- if (deltaApplied.toPrecision(PRECISION) >= delta.toPrecision(PRECISION)) {
82
+ if (
83
+ deltaApplied
84
+ .toPrecision(PRECISION)
85
+ .localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
86
+ numeric: true,
87
+ }) >= 0
88
+ ) {
66
89
  break;
67
90
  }
68
91
  }
@@ -81,13 +104,13 @@ export function adjustByDelta(
81
104
  // If we were unable to resize any of the panels panels, return the previous state.
82
105
  // This will essentially bailout and ignore the "mousemove" event.
83
106
  if (deltaApplied === 0) {
84
- return prevSizes;
107
+ return baseSizes;
85
108
  }
86
109
 
87
110
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
88
111
  pivotId = delta < 0 ? idAfter : idBefore;
89
112
  index = panelsArray.findIndex((panel) => panel.id === pivotId);
90
- nextSizes[index] = prevSizes[index] + deltaApplied;
113
+ nextSizes[index] = baseSizes[index] + deltaApplied;
91
114
 
92
115
  return nextSizes;
93
116
  }
@@ -100,15 +123,17 @@ export function callPanelCallbacks(
100
123
  nextSizes.forEach((nextSize, index) => {
101
124
  const prevSize = prevSizes[index];
102
125
  if (prevSize !== nextSize) {
103
- const { callbacksRef } = panelsArray[index];
126
+ const { callbacksRef, collapsible } = panelsArray[index];
104
127
  const { onCollapse, onResize } = callbacksRef.current;
105
128
 
106
129
  if (onResize) {
107
130
  onResize(nextSize);
108
131
  }
109
132
 
110
- if (onCollapse) {
111
- if (prevSize === 0 && nextSize !== 0) {
133
+ if (collapsible && onCollapse) {
134
+ // Falsy check handles both previous size of 0
135
+ // and initial size of undefined (when mounting)
136
+ if (!prevSize && nextSize !== 0) {
112
137
  onCollapse(false);
113
138
  } else if (prevSize !== 0 && nextSize === 0) {
114
139
  onCollapse(true);
@@ -230,7 +255,8 @@ export function panelsMapToSortedArray(
230
255
  function safeResizePanel(
231
256
  panel: PanelData,
232
257
  delta: number,
233
- prevSize: number
258
+ prevSize: number,
259
+ event: ResizeEvent | null
234
260
  ): number {
235
261
  const nextSizeUnsafe = prevSize + delta;
236
262
 
@@ -240,8 +266,14 @@ function safeResizePanel(
240
266
  return 0;
241
267
  }
242
268
  } else {
243
- if (nextSizeUnsafe < panel.minSize) {
244
- return 0;
269
+ const isKeyboardEvent = event?.type?.startsWith("key");
270
+ if (!isKeyboardEvent) {
271
+ // Keyboard events should expand a collapsed panel to the min size,
272
+ // but mouse events should wait until the panel has reached its min size
273
+ // to avoid a visual flickering when dragging between collapsed and min size.
274
+ if (nextSizeUnsafe < panel.minSize) {
275
+ return 0;
276
+ }
245
277
  }
246
278
  }
247
279
  }
@@ -1,4 +1,4 @@
1
- import { PanelData } from "../types";
1
+ import { PanelData, PanelGroupStorage } from "../types";
2
2
 
3
3
  type SerializedPanelGroupState = { [panelIds: string]: number[] };
4
4
 
@@ -17,10 +17,11 @@ function getSerializationKey(panels: PanelData[]): string {
17
17
  }
18
18
 
19
19
  function loadSerializedPanelGroupState(
20
- autoSaveId: string
20
+ autoSaveId: string,
21
+ storage: PanelGroupStorage
21
22
  ): SerializedPanelGroupState | null {
22
23
  try {
23
- const serialized = localStorage.getItem(`PanelGroup:sizes:${autoSaveId}`);
24
+ const serialized = storage.getItem(`PanelGroup:sizes:${autoSaveId}`);
24
25
  if (serialized) {
25
26
  const parsed = JSON.parse(serialized);
26
27
  if (typeof parsed === "object" && parsed != null) {
@@ -34,9 +35,10 @@ function loadSerializedPanelGroupState(
34
35
 
35
36
  export function loadPanelLayout(
36
37
  autoSaveId: string,
37
- panels: PanelData[]
38
+ panels: PanelData[],
39
+ storage: PanelGroupStorage
38
40
  ): number[] | null {
39
- const state = loadSerializedPanelGroupState(autoSaveId);
41
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
40
42
  if (state) {
41
43
  const key = getSerializationKey(panels);
42
44
  return state[key] || null;
@@ -48,17 +50,15 @@ export function loadPanelLayout(
48
50
  export function savePanelGroupLayout(
49
51
  autoSaveId: string,
50
52
  panels: PanelData[],
51
- sizes: number[]
53
+ sizes: number[],
54
+ storage: PanelGroupStorage
52
55
  ): void {
53
56
  const key = getSerializationKey(panels);
54
- const state = loadSerializedPanelGroupState(autoSaveId) || {};
57
+ const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
55
58
  state[key] = sizes;
56
59
 
57
60
  try {
58
- localStorage.setItem(
59
- `PanelGroup:sizes:${autoSaveId}`,
60
- JSON.stringify(state)
61
- );
61
+ storage.setItem(`PanelGroup:sizes:${autoSaveId}`, JSON.stringify(state));
62
62
  } catch (error) {
63
63
  console.error(error);
64
64
  }