react-resizable-panels 0.0.28 → 0.0.30

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,11 +11,13 @@ import {
11
11
  } from "react";
12
12
 
13
13
  import { PanelGroupContext } from "./PanelContexts";
14
- import { Direction, PanelData, ResizeEvent } from "./types";
14
+ import { Direction, PanelData, PanelGroupOnLayout, ResizeEvent } from "./types";
15
15
  import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization";
16
16
  import { getDragOffset, getMovement } from "./utils/coordinates";
17
17
  import {
18
18
  adjustByDelta,
19
+ callPanelCallbacks,
20
+ getBeforeAndAfterIds,
19
21
  getFlexGrow,
20
22
  getPanelGroup,
21
23
  getResizeHandlePanelIds,
@@ -25,6 +27,10 @@ import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
25
27
  import useUniqueId from "./hooks/useUniqueId";
26
28
  import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterBehavior";
27
29
  import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
30
+ import debounce from "./utils/debounce";
31
+
32
+ // Limit the frequency of localStorage updates.
33
+ const savePanelGroupLayoutDebounced = debounce(savePanelGroupLayout, 100);
28
34
 
29
35
  export type CommittedValues = {
30
36
  direction: Direction;
@@ -44,16 +50,18 @@ export type PanelGroupProps = {
44
50
  className?: string;
45
51
  direction: Direction;
46
52
  id?: string | null;
53
+ onLayout?: PanelGroupOnLayout;
47
54
  style?: CSSProperties;
48
55
  tagName?: ElementType;
49
56
  };
50
57
 
51
- export default function PanelGroup({
58
+ export function PanelGroup({
52
59
  autoSaveId,
53
60
  children = null,
54
61
  className: classNameFromProps = "",
55
62
  direction,
56
63
  id: idFromProps = null,
64
+ onLayout = null,
57
65
  style: styleFromProps = {},
58
66
  tagName: Type = "div",
59
67
  }: PanelGroupProps) {
@@ -62,11 +70,22 @@ export default function PanelGroup({
62
70
  const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
63
71
  const [panels, setPanels] = useState<PanelDataMap>(new Map());
64
72
 
73
+ // Use a ref to guard against users passing inline props
74
+ const callbacksRef = useRef<{
75
+ onLayout: PanelGroupOnLayout | null;
76
+ }>({ onLayout });
77
+ useEffect(() => {
78
+ callbacksRef.current.onLayout = onLayout;
79
+ });
80
+
65
81
  // 0-1 values representing the relative size of each panel.
66
82
  const [sizes, setSizes] = useState<number[]>([]);
67
83
 
68
84
  const dragOffsetRef = useRef<number>(0);
69
85
 
86
+ // Used to support imperative collapse/expand API.
87
+ const panelSizeBeforeCollapse = useRef<Map<string, number>>(new Map());
88
+
70
89
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
71
90
  const committedValuesRef = useRef<CommittedValues>({
72
91
  direction,
@@ -88,6 +107,15 @@ export default function PanelGroup({
88
107
  sizes,
89
108
  });
90
109
 
110
+ // Notify external code when sizes have changed.
111
+ useEffect(() => {
112
+ const { onLayout } = callbacksRef.current;
113
+ if (onLayout) {
114
+ const { sizes } = committedValuesRef.current;
115
+ onLayout(sizes);
116
+ }
117
+ }, [sizes]);
118
+
91
119
  // Once all panels have registered themselves,
92
120
  // Compute the initial sizes based on default weights.
93
121
  // This assumes that panels register during initial mount (no conditional rendering)!
@@ -160,7 +188,8 @@ export default function PanelGroup({
160
188
  }
161
189
 
162
190
  const panelsArray = panelsMapToSortedArray(panels);
163
- savePanelGroupLayout(autoSaveId, panelsArray, sizes);
191
+
192
+ savePanelGroupLayoutDebounced(autoSaveId, panelsArray, sizes);
164
193
  }
165
194
  }, [autoSaveId, panels, sizes]);
166
195
 
@@ -191,9 +220,13 @@ export default function PanelGroup({
191
220
 
192
221
  // Without this, Panel sizes may be unintentionally overridden by their content.
193
222
  overflow: "hidden",
223
+
224
+ // Disable pointer events inside of a panel during resize.
225
+ // This avoid edge cases like nested iframes.
226
+ pointerEvents: activeHandleId !== null ? "none" : undefined,
194
227
  };
195
228
  },
196
- [direction, sizes]
229
+ [activeHandleId, direction, sizes]
197
230
  );
198
231
 
199
232
  const registerPanel = useCallback((id: string, panel: PanelData) => {
@@ -275,24 +308,7 @@ export default function PanelGroup({
275
308
  setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical");
276
309
 
277
310
  // If resize change handlers have been declared, this is the time to call them.
278
- nextSizes.forEach((nextSize, index) => {
279
- const prevSize = prevSizes[index];
280
- if (prevSize !== nextSize) {
281
- const { onCollapse, onResize } =
282
- panelsArray[index].callbacksRef.current;
283
- if (onResize) {
284
- onResize(nextSize);
285
- }
286
-
287
- if (onCollapse) {
288
- if (prevSize === 0 && nextSize !== 0) {
289
- onCollapse(false);
290
- } else if (prevSize !== 0 && nextSize === 0) {
291
- onCollapse(true);
292
- }
293
- }
294
- }
295
- });
311
+ callPanelCallbacks(panelsArray, prevSizes, nextSizes);
296
312
 
297
313
  setSizes(nextSizes);
298
314
  }
@@ -316,14 +332,156 @@ export default function PanelGroup({
316
332
  });
317
333
  }, []);
318
334
 
335
+ const collapsePanel = useCallback((id: string) => {
336
+ const { panels, sizes: prevSizes } = committedValuesRef.current;
337
+
338
+ const panel = panels.get(id);
339
+ if (panel == null || !panel.collapsible) {
340
+ return;
341
+ }
342
+
343
+ const panelsArray = panelsMapToSortedArray(panels);
344
+
345
+ const index = panelsArray.indexOf(panel);
346
+ if (index < 0) {
347
+ return;
348
+ }
349
+
350
+ const currentSize = prevSizes[index];
351
+ if (currentSize === 0) {
352
+ // Panel is already collapsed.
353
+ return;
354
+ }
355
+
356
+ panelSizeBeforeCollapse.current.set(id, currentSize);
357
+
358
+ const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
359
+ if (idBefore == null || idAfter == null) {
360
+ return;
361
+ }
362
+
363
+ const isLastPanel = index === panelsArray.length - 1;
364
+ const delta = isLastPanel ? currentSize : 0 - currentSize;
365
+
366
+ const nextSizes = adjustByDelta(
367
+ panels,
368
+ idBefore,
369
+ idAfter,
370
+ delta,
371
+ prevSizes
372
+ );
373
+ if (prevSizes !== nextSizes) {
374
+ // If resize change handlers have been declared, this is the time to call them.
375
+ callPanelCallbacks(panelsArray, prevSizes, nextSizes);
376
+
377
+ setSizes(nextSizes);
378
+ }
379
+ }, []);
380
+
381
+ const expandPanel = useCallback((id: string) => {
382
+ const { panels, sizes: prevSizes } = committedValuesRef.current;
383
+
384
+ const panel = panels.get(id);
385
+ if (panel == null) {
386
+ return;
387
+ }
388
+
389
+ const sizeBeforeCollapse =
390
+ panelSizeBeforeCollapse.current.get(id) || panel.minSize;
391
+ if (!sizeBeforeCollapse) {
392
+ return;
393
+ }
394
+
395
+ const panelsArray = panelsMapToSortedArray(panels);
396
+
397
+ const index = panelsArray.indexOf(panel);
398
+ if (index < 0) {
399
+ return;
400
+ }
401
+
402
+ const currentSize = prevSizes[index];
403
+ if (currentSize !== 0) {
404
+ // Panel is already expanded.
405
+ return;
406
+ }
407
+
408
+ const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
409
+ if (idBefore == null || idAfter == null) {
410
+ return;
411
+ }
412
+
413
+ const isLastPanel = index === panelsArray.length - 1;
414
+ const delta = isLastPanel ? 0 - sizeBeforeCollapse : sizeBeforeCollapse;
415
+
416
+ const nextSizes = adjustByDelta(
417
+ panels,
418
+ idBefore,
419
+ idAfter,
420
+ delta,
421
+ prevSizes
422
+ );
423
+ if (prevSizes !== nextSizes) {
424
+ // If resize change handlers have been declared, this is the time to call them.
425
+ callPanelCallbacks(panelsArray, prevSizes, nextSizes);
426
+
427
+ setSizes(nextSizes);
428
+ }
429
+ }, []);
430
+
431
+ const resizePanel = useCallback((id: string, nextSize: number) => {
432
+ const { panels, sizes: prevSizes } = committedValuesRef.current;
433
+
434
+ const panel = panels.get(id);
435
+ if (panel == null) {
436
+ return;
437
+ }
438
+
439
+ const panelsArray = panelsMapToSortedArray(panels);
440
+
441
+ const index = panelsArray.indexOf(panel);
442
+ if (index < 0) {
443
+ return;
444
+ }
445
+
446
+ const currentSize = prevSizes[index];
447
+ if (currentSize === nextSize) {
448
+ return;
449
+ }
450
+
451
+ const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
452
+ if (idBefore == null || idAfter == null) {
453
+ return;
454
+ }
455
+
456
+ const isLastPanel = index === panelsArray.length - 1;
457
+ const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
458
+
459
+ const nextSizes = adjustByDelta(
460
+ panels,
461
+ idBefore,
462
+ idAfter,
463
+ delta,
464
+ prevSizes
465
+ );
466
+ if (prevSizes !== nextSizes) {
467
+ // If resize change handlers have been declared, this is the time to call them.
468
+ callPanelCallbacks(panelsArray, prevSizes, nextSizes);
469
+
470
+ setSizes(nextSizes);
471
+ }
472
+ }, []);
473
+
319
474
  const context = useMemo(
320
475
  () => ({
321
476
  activeHandleId,
477
+ collapsePanel,
322
478
  direction,
479
+ expandPanel,
323
480
  getPanelStyle,
324
481
  groupId,
325
482
  registerPanel,
326
483
  registerResizeHandle,
484
+ resizePanel,
327
485
  startDragging: (id: string, event: ResizeEvent) => {
328
486
  setActiveHandleId(id);
329
487
 
@@ -337,11 +495,14 @@ export default function PanelGroup({
337
495
  }),
338
496
  [
339
497
  activeHandleId,
498
+ collapsePanel,
340
499
  direction,
500
+ expandPanel,
341
501
  getPanelStyle,
342
502
  groupId,
343
503
  registerPanel,
344
504
  registerResizeHandle,
505
+ resizePanel,
345
506
  unregisterPanel,
346
507
  ]
347
508
  );
@@ -27,7 +27,7 @@ export type PanelResizeHandleProps = {
27
27
  tagName?: ElementType;
28
28
  };
29
29
 
30
- export default function PanelResizeHandle({
30
+ export function PanelResizeHandle({
31
31
  children = null,
32
32
  className: classNameFromProps = "",
33
33
  disabled = false,
@@ -1,6 +1,13 @@
1
1
  import { useEffect, useLayoutEffect } from "react";
2
2
 
3
- const useIsomorphicLayoutEffect =
4
- typeof window !== "undefined" ? useLayoutEffect : useEffect;
3
+ const canUseEffectHooks = !!(
4
+ typeof window !== "undefined" &&
5
+ typeof window.document !== "undefined" &&
6
+ typeof window.document.createElement !== "undefined"
7
+ );
8
+
9
+ const useIsomorphicLayoutEffect = canUseEffectHooks
10
+ ? useLayoutEffect
11
+ : () => {};
5
12
 
6
13
  export default useIsomorphicLayoutEffect;
package/src/index.ts CHANGED
@@ -1,5 +1,19 @@
1
- import Panel from "./Panel";
2
- import PanelGroup from "./PanelGroup";
3
- import PanelResizeHandle from "./PanelResizeHandle";
1
+ import { Panel } from "./Panel";
2
+ import { PanelGroup } from "./PanelGroup";
3
+ import { PanelResizeHandle } from "./PanelResizeHandle";
4
4
 
5
- export { Panel, PanelGroup, PanelResizeHandle };
5
+ import type { ImperativePanelHandle, PanelProps } from "./Panel";
6
+ import type { PanelGroupProps } from "./PanelGroup";
7
+ import type { PanelResizeHandleProps } from "./PanelResizeHandle";
8
+
9
+ export {
10
+ Panel,
11
+ PanelGroup,
12
+ PanelResizeHandle,
13
+
14
+ // TypeScript types
15
+ ImperativePanelHandle,
16
+ PanelGroupProps,
17
+ PanelProps,
18
+ PanelResizeHandleProps,
19
+ };
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@ import { RefObject } from "react";
2
2
 
3
3
  export type Direction = "horizontal" | "vertical";
4
4
 
5
+ export type PanelGroupOnLayout = (sizes: number[]) => void;
5
6
  export type PanelOnCollapse = (collapsed: boolean) => void;
6
7
  export type PanelOnResize = (size: number) => void;
7
8
 
@@ -0,0 +1,16 @@
1
+ export default function debounce<T extends Function>(
2
+ callback: T,
3
+ durationMs: number = 10
4
+ ) {
5
+ let timeoutId: NodeJS.Timeout | null = null;
6
+
7
+ let callable = (...args: any) => {
8
+ clearTimeout(timeoutId);
9
+
10
+ timeoutId = setTimeout(() => {
11
+ callback(...args);
12
+ }, durationMs);
13
+ };
14
+
15
+ return callable as unknown as T;
16
+ }
@@ -83,6 +83,52 @@ export function adjustByDelta(
83
83
  return nextSizes;
84
84
  }
85
85
 
86
+ export function callPanelCallbacks(
87
+ panelsArray: PanelData[],
88
+ prevSizes: number[],
89
+ nextSizes: number[]
90
+ ) {
91
+ nextSizes.forEach((nextSize, index) => {
92
+ const prevSize = prevSizes[index];
93
+ if (prevSize !== nextSize) {
94
+ const { callbacksRef } = panelsArray[index];
95
+ const { onCollapse, onResize } = callbacksRef.current;
96
+
97
+ if (onResize) {
98
+ onResize(nextSize);
99
+ }
100
+
101
+ if (onCollapse) {
102
+ if (prevSize === 0 && nextSize !== 0) {
103
+ onCollapse(false);
104
+ } else if (prevSize !== 0 && nextSize === 0) {
105
+ onCollapse(true);
106
+ }
107
+ }
108
+ }
109
+ });
110
+ }
111
+
112
+ export function getBeforeAndAfterIds(
113
+ id: string,
114
+ panelsArray: PanelData[]
115
+ ): [idBefore: string | null, idAFter: string | null] {
116
+ if (panelsArray.length < 2) {
117
+ return [null, null];
118
+ }
119
+
120
+ const index = panelsArray.findIndex((panel) => panel.id === id);
121
+ if (index < 0) {
122
+ return [null, null];
123
+ }
124
+
125
+ const isLastPanel = index === panelsArray.length - 1;
126
+ const idBefore = isLastPanel ? panelsArray[index - 1].id : id;
127
+ const idAfter = isLastPanel ? id : panelsArray[index + 1].id;
128
+
129
+ return [idBefore, idAfter];
130
+ }
131
+
86
132
  // This method returns a number between 1 and 100 representing
87
133
  // the % of the group's overall space this panel should occupy.
88
134
  export function getFlexGrow(