react-resizable-panels 2.0.19 → 2.0.21

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.21
4
+
5
+ - Handle pointer event edge case with different origin iframes (#374)
6
+
7
+ ## 2.0.20
8
+
9
+ - Reset global cursor if an active resize handle is unmounted (#313)
10
+ - Resize handle supports (optional) `onFocus` or `onBlur` props (#370)
11
+
3
12
  ## 2.0.19
4
13
 
5
14
  - Add optional `minSize` override param to panel `expand` imperative API
package/README.md CHANGED
@@ -223,4 +223,7 @@ export function ClientComponent({
223
223
  }
224
224
  ```
225
225
 
226
+ > [!NOTE]
227
+ > Be sure to specify a `defaultSize` prop for **every** `Panel` component to avoid layout flicker.
228
+
226
229
  A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr).
@@ -1,18 +1,20 @@
1
- import { CSSProperties, HTMLAttributes, PropsWithChildren, ReactElement } from "./vendor/react.js";
2
1
  import { PointerHitAreaMargins } from "./PanelResizeHandleRegistry.js";
2
+ import { CSSProperties, HTMLAttributes, PropsWithChildren, ReactElement } from "./vendor/react.js";
3
3
  export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
4
4
  export type ResizeHandlerState = "drag" | "hover" | "inactive";
5
- export type PanelResizeHandleProps = Omit<HTMLAttributes<keyof HTMLElementTagNameMap>, "id"> & PropsWithChildren<{
5
+ export type PanelResizeHandleProps = Omit<HTMLAttributes<keyof HTMLElementTagNameMap>, "id" | "onBlur" | "onFocus"> & PropsWithChildren<{
6
6
  className?: string;
7
7
  disabled?: boolean;
8
8
  hitAreaMargins?: PointerHitAreaMargins;
9
9
  id?: string | null;
10
+ onBlur?: () => void;
10
11
  onDragging?: PanelResizeHandleOnDragging;
12
+ onFocus?: () => void;
11
13
  style?: CSSProperties;
12
14
  tabIndex?: number;
13
15
  tagName?: keyof HTMLElementTagNameMap;
14
16
  }>;
15
- export declare function PanelResizeHandle({ children, className: classNameFromProps, disabled, hitAreaMargins, id: idFromProps, onDragging, style: styleFromProps, tabIndex, tagName: Type, ...rest }: PanelResizeHandleProps): ReactElement;
17
+ export declare function PanelResizeHandle({ children, className: classNameFromProps, disabled, hitAreaMargins, id: idFromProps, onBlur, onDragging, onFocus, style: styleFromProps, tabIndex, tagName: Type, ...rest }: PanelResizeHandleProps): ReactElement;
16
18
  export declare namespace PanelResizeHandle {
17
19
  var displayName: string;
18
20
  }
@@ -14,4 +14,5 @@ import { intersects } from "./utils/rects/intersects.js";
14
14
  import type { ImperativePanelHandle, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps } from "./Panel.js";
15
15
  import type { ImperativePanelGroupHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage } from "./PanelGroup.js";
16
16
  import type { PanelResizeHandleOnDragging, PanelResizeHandleProps } from "./PanelResizeHandle.js";
17
- export { ImperativePanelGroupHandle, ImperativePanelHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps, PanelResizeHandleOnDragging, PanelResizeHandleProps, Panel, PanelGroup, PanelResizeHandle, assert, getIntersectingRectangle, intersects, getPanelElement, getPanelElementsForGroup, getPanelGroupElement, getResizeHandleElement, getResizeHandleElementIndex, getResizeHandleElementsForGroup, getResizeHandlePanelIds, };
17
+ import type { PointerHitAreaMargins } from "./PanelResizeHandleRegistry.js";
18
+ export { ImperativePanelGroupHandle, ImperativePanelHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps, PanelResizeHandleOnDragging, PanelResizeHandleProps, PointerHitAreaMargins, Panel, PanelGroup, PanelResizeHandle, assert, getIntersectingRectangle, intersects, getPanelElement, getPanelElementsForGroup, getPanelGroupElement, getResizeHandleElement, getResizeHandleElementIndex, getResizeHandleElementsForGroup, getResizeHandlePanelIds, };
@@ -454,6 +454,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
454
454
  if (count === 1) {
455
455
  ownerDocumentCounts.delete(ownerDocument);
456
456
  }
457
+
458
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
459
+ // update the global pointer to account for the change
460
+ if (intersectingHandles.includes(data)) {
461
+ const index = intersectingHandles.indexOf(data);
462
+ if (index >= 0) {
463
+ intersectingHandles.splice(index, 1);
464
+ }
465
+ updateCursor();
466
+ }
457
467
  };
458
468
  }
459
469
  function handlePointerDown(event) {
@@ -482,6 +492,13 @@ function handlePointerMove(event) {
482
492
  x,
483
493
  y
484
494
  } = getResizeEventCoordinates(event);
495
+
496
+ // Edge case (see #340)
497
+ // Detect when the pointer has been released outside an iframe on a different domain
498
+ if (event.buttons === 0) {
499
+ isPointerDown = false;
500
+ updateResizeHandlerStates("up", event);
501
+ }
485
502
  if (!isPointerDown) {
486
503
  const {
487
504
  target
@@ -1921,9 +1938,6 @@ function PanelGroupWithForwardedRef({
1921
1938
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1922
1939
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1923
1940
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1924
- if (delta === 0) {
1925
- return;
1926
- }
1927
1941
 
1928
1942
  // Support RTL layouts
1929
1943
  const isHorizontal = direction === "horizontal";
@@ -2203,7 +2217,9 @@ function PanelResizeHandle({
2203
2217
  disabled = false,
2204
2218
  hitAreaMargins,
2205
2219
  id: idFromProps,
2220
+ onBlur,
2206
2221
  onDragging,
2222
+ onFocus,
2207
2223
  style: styleFromProps = {},
2208
2224
  tabIndex = 0,
2209
2225
  tagName: Type = "div",
@@ -2323,8 +2339,14 @@ function PanelResizeHandle({
2323
2339
  children,
2324
2340
  className: classNameFromProps,
2325
2341
  id: idFromProps,
2326
- onBlur: () => setIsFocused(false),
2327
- onFocus: () => setIsFocused(true),
2342
+ onBlur: () => {
2343
+ setIsFocused(false);
2344
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2345
+ },
2346
+ onFocus: () => {
2347
+ setIsFocused(true);
2348
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2349
+ },
2328
2350
  ref: elementRef,
2329
2351
  role: "separator",
2330
2352
  style: {
@@ -460,6 +460,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
460
460
  if (count === 1) {
461
461
  ownerDocumentCounts.delete(ownerDocument);
462
462
  }
463
+
464
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
465
+ // update the global pointer to account for the change
466
+ if (intersectingHandles.includes(data)) {
467
+ const index = intersectingHandles.indexOf(data);
468
+ if (index >= 0) {
469
+ intersectingHandles.splice(index, 1);
470
+ }
471
+ updateCursor();
472
+ }
463
473
  };
464
474
  }
465
475
  function handlePointerDown(event) {
@@ -488,6 +498,13 @@ function handlePointerMove(event) {
488
498
  x,
489
499
  y
490
500
  } = getResizeEventCoordinates(event);
501
+
502
+ // Edge case (see #340)
503
+ // Detect when the pointer has been released outside an iframe on a different domain
504
+ if (event.buttons === 0) {
505
+ isPointerDown = false;
506
+ updateResizeHandlerStates("up", event);
507
+ }
491
508
  if (!isPointerDown) {
492
509
  const {
493
510
  target
@@ -2027,9 +2044,6 @@ function PanelGroupWithForwardedRef({
2027
2044
  } = dragState !== null && dragState !== void 0 ? dragState : {};
2028
2045
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
2029
2046
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
2030
- if (delta === 0) {
2031
- return;
2032
- }
2033
2047
 
2034
2048
  // Support RTL layouts
2035
2049
  const isHorizontal = direction === "horizontal";
@@ -2309,7 +2323,9 @@ function PanelResizeHandle({
2309
2323
  disabled = false,
2310
2324
  hitAreaMargins,
2311
2325
  id: idFromProps,
2326
+ onBlur,
2312
2327
  onDragging,
2328
+ onFocus,
2313
2329
  style: styleFromProps = {},
2314
2330
  tabIndex = 0,
2315
2331
  tagName: Type = "div",
@@ -2429,8 +2445,14 @@ function PanelResizeHandle({
2429
2445
  children,
2430
2446
  className: classNameFromProps,
2431
2447
  id: idFromProps,
2432
- onBlur: () => setIsFocused(false),
2433
- onFocus: () => setIsFocused(true),
2448
+ onBlur: () => {
2449
+ setIsFocused(false);
2450
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2451
+ },
2452
+ onFocus: () => {
2453
+ setIsFocused(true);
2454
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2455
+ },
2434
2456
  ref: elementRef,
2435
2457
  role: "separator",
2436
2458
  style: {
@@ -436,6 +436,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
436
436
  if (count === 1) {
437
437
  ownerDocumentCounts.delete(ownerDocument);
438
438
  }
439
+
440
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
441
+ // update the global pointer to account for the change
442
+ if (intersectingHandles.includes(data)) {
443
+ const index = intersectingHandles.indexOf(data);
444
+ if (index >= 0) {
445
+ intersectingHandles.splice(index, 1);
446
+ }
447
+ updateCursor();
448
+ }
439
449
  };
440
450
  }
441
451
  function handlePointerDown(event) {
@@ -464,6 +474,13 @@ function handlePointerMove(event) {
464
474
  x,
465
475
  y
466
476
  } = getResizeEventCoordinates(event);
477
+
478
+ // Edge case (see #340)
479
+ // Detect when the pointer has been released outside an iframe on a different domain
480
+ if (event.buttons === 0) {
481
+ isPointerDown = false;
482
+ updateResizeHandlerStates("up", event);
483
+ }
467
484
  if (!isPointerDown) {
468
485
  const {
469
486
  target
@@ -2003,9 +2020,6 @@ function PanelGroupWithForwardedRef({
2003
2020
  } = dragState !== null && dragState !== void 0 ? dragState : {};
2004
2021
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
2005
2022
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
2006
- if (delta === 0) {
2007
- return;
2008
- }
2009
2023
 
2010
2024
  // Support RTL layouts
2011
2025
  const isHorizontal = direction === "horizontal";
@@ -2285,7 +2299,9 @@ function PanelResizeHandle({
2285
2299
  disabled = false,
2286
2300
  hitAreaMargins,
2287
2301
  id: idFromProps,
2302
+ onBlur,
2288
2303
  onDragging,
2304
+ onFocus,
2289
2305
  style: styleFromProps = {},
2290
2306
  tabIndex = 0,
2291
2307
  tagName: Type = "div",
@@ -2405,8 +2421,14 @@ function PanelResizeHandle({
2405
2421
  children,
2406
2422
  className: classNameFromProps,
2407
2423
  id: idFromProps,
2408
- onBlur: () => setIsFocused(false),
2409
- onFocus: () => setIsFocused(true),
2424
+ onBlur: () => {
2425
+ setIsFocused(false);
2426
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2427
+ },
2428
+ onFocus: () => {
2429
+ setIsFocused(true);
2430
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2431
+ },
2410
2432
  ref: elementRef,
2411
2433
  role: "separator",
2412
2434
  style: {
@@ -430,6 +430,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
430
430
  if (count === 1) {
431
431
  ownerDocumentCounts.delete(ownerDocument);
432
432
  }
433
+
434
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
435
+ // update the global pointer to account for the change
436
+ if (intersectingHandles.includes(data)) {
437
+ const index = intersectingHandles.indexOf(data);
438
+ if (index >= 0) {
439
+ intersectingHandles.splice(index, 1);
440
+ }
441
+ updateCursor();
442
+ }
433
443
  };
434
444
  }
435
445
  function handlePointerDown(event) {
@@ -458,6 +468,13 @@ function handlePointerMove(event) {
458
468
  x,
459
469
  y
460
470
  } = getResizeEventCoordinates(event);
471
+
472
+ // Edge case (see #340)
473
+ // Detect when the pointer has been released outside an iframe on a different domain
474
+ if (event.buttons === 0) {
475
+ isPointerDown = false;
476
+ updateResizeHandlerStates("up", event);
477
+ }
461
478
  if (!isPointerDown) {
462
479
  const {
463
480
  target
@@ -1897,9 +1914,6 @@ function PanelGroupWithForwardedRef({
1897
1914
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1898
1915
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1899
1916
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1900
- if (delta === 0) {
1901
- return;
1902
- }
1903
1917
 
1904
1918
  // Support RTL layouts
1905
1919
  const isHorizontal = direction === "horizontal";
@@ -2179,7 +2193,9 @@ function PanelResizeHandle({
2179
2193
  disabled = false,
2180
2194
  hitAreaMargins,
2181
2195
  id: idFromProps,
2196
+ onBlur,
2182
2197
  onDragging,
2198
+ onFocus,
2183
2199
  style: styleFromProps = {},
2184
2200
  tabIndex = 0,
2185
2201
  tagName: Type = "div",
@@ -2299,8 +2315,14 @@ function PanelResizeHandle({
2299
2315
  children,
2300
2316
  className: classNameFromProps,
2301
2317
  id: idFromProps,
2302
- onBlur: () => setIsFocused(false),
2303
- onFocus: () => setIsFocused(true),
2318
+ onBlur: () => {
2319
+ setIsFocused(false);
2320
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2321
+ },
2322
+ onFocus: () => {
2323
+ setIsFocused(true);
2324
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2325
+ },
2304
2326
  ref: elementRef,
2305
2327
  role: "separator",
2306
2328
  style: {
@@ -456,6 +456,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
456
456
  if (count === 1) {
457
457
  ownerDocumentCounts.delete(ownerDocument);
458
458
  }
459
+
460
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
461
+ // update the global pointer to account for the change
462
+ if (intersectingHandles.includes(data)) {
463
+ const index = intersectingHandles.indexOf(data);
464
+ if (index >= 0) {
465
+ intersectingHandles.splice(index, 1);
466
+ }
467
+ updateCursor();
468
+ }
459
469
  };
460
470
  }
461
471
  function handlePointerDown(event) {
@@ -484,6 +494,13 @@ function handlePointerMove(event) {
484
494
  x,
485
495
  y
486
496
  } = getResizeEventCoordinates(event);
497
+
498
+ // Edge case (see #340)
499
+ // Detect when the pointer has been released outside an iframe on a different domain
500
+ if (event.buttons === 0) {
501
+ isPointerDown = false;
502
+ updateResizeHandlerStates("up", event);
503
+ }
487
504
  if (!isPointerDown) {
488
505
  const {
489
506
  target
@@ -1923,9 +1940,6 @@ function PanelGroupWithForwardedRef({
1923
1940
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1924
1941
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1925
1942
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1926
- if (delta === 0) {
1927
- return;
1928
- }
1929
1943
 
1930
1944
  // Support RTL layouts
1931
1945
  const isHorizontal = direction === "horizontal";
@@ -2205,7 +2219,9 @@ function PanelResizeHandle({
2205
2219
  disabled = false,
2206
2220
  hitAreaMargins,
2207
2221
  id: idFromProps,
2222
+ onBlur,
2208
2223
  onDragging,
2224
+ onFocus,
2209
2225
  style: styleFromProps = {},
2210
2226
  tabIndex = 0,
2211
2227
  tagName: Type = "div",
@@ -2325,8 +2341,14 @@ function PanelResizeHandle({
2325
2341
  children,
2326
2342
  className: classNameFromProps,
2327
2343
  id: idFromProps,
2328
- onBlur: () => setIsFocused(false),
2329
- onFocus: () => setIsFocused(true),
2344
+ onBlur: () => {
2345
+ setIsFocused(false);
2346
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2347
+ },
2348
+ onFocus: () => {
2349
+ setIsFocused(true);
2350
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2351
+ },
2330
2352
  ref: elementRef,
2331
2353
  role: "separator",
2332
2354
  style: {
@@ -467,6 +467,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
467
467
  if (count === 1) {
468
468
  ownerDocumentCounts.delete(ownerDocument);
469
469
  }
470
+
471
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
472
+ // update the global pointer to account for the change
473
+ if (intersectingHandles.includes(data)) {
474
+ const index = intersectingHandles.indexOf(data);
475
+ if (index >= 0) {
476
+ intersectingHandles.splice(index, 1);
477
+ }
478
+ updateCursor();
479
+ }
470
480
  };
471
481
  }
472
482
  function handlePointerDown(event) {
@@ -495,6 +505,13 @@ function handlePointerMove(event) {
495
505
  x,
496
506
  y
497
507
  } = getResizeEventCoordinates(event);
508
+
509
+ // Edge case (see #340)
510
+ // Detect when the pointer has been released outside an iframe on a different domain
511
+ if (event.buttons === 0) {
512
+ isPointerDown = false;
513
+ updateResizeHandlerStates("up", event);
514
+ }
498
515
  if (!isPointerDown) {
499
516
  const {
500
517
  target
@@ -2034,9 +2051,6 @@ function PanelGroupWithForwardedRef({
2034
2051
  } = dragState !== null && dragState !== void 0 ? dragState : {};
2035
2052
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
2036
2053
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
2037
- if (delta === 0) {
2038
- return;
2039
- }
2040
2054
 
2041
2055
  // Support RTL layouts
2042
2056
  const isHorizontal = direction === "horizontal";
@@ -2316,7 +2330,9 @@ function PanelResizeHandle({
2316
2330
  disabled = false,
2317
2331
  hitAreaMargins,
2318
2332
  id: idFromProps,
2333
+ onBlur,
2319
2334
  onDragging,
2335
+ onFocus,
2320
2336
  style: styleFromProps = {},
2321
2337
  tabIndex = 0,
2322
2338
  tagName: Type = "div",
@@ -2436,8 +2452,14 @@ function PanelResizeHandle({
2436
2452
  children,
2437
2453
  className: classNameFromProps,
2438
2454
  id: idFromProps,
2439
- onBlur: () => setIsFocused(false),
2440
- onFocus: () => setIsFocused(true),
2455
+ onBlur: () => {
2456
+ setIsFocused(false);
2457
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2458
+ },
2459
+ onFocus: () => {
2460
+ setIsFocused(true);
2461
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2462
+ },
2441
2463
  ref: elementRef,
2442
2464
  role: "separator",
2443
2465
  style: {
@@ -443,6 +443,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
443
443
  if (count === 1) {
444
444
  ownerDocumentCounts.delete(ownerDocument);
445
445
  }
446
+
447
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
448
+ // update the global pointer to account for the change
449
+ if (intersectingHandles.includes(data)) {
450
+ const index = intersectingHandles.indexOf(data);
451
+ if (index >= 0) {
452
+ intersectingHandles.splice(index, 1);
453
+ }
454
+ updateCursor();
455
+ }
446
456
  };
447
457
  }
448
458
  function handlePointerDown(event) {
@@ -471,6 +481,13 @@ function handlePointerMove(event) {
471
481
  x,
472
482
  y
473
483
  } = getResizeEventCoordinates(event);
484
+
485
+ // Edge case (see #340)
486
+ // Detect when the pointer has been released outside an iframe on a different domain
487
+ if (event.buttons === 0) {
488
+ isPointerDown = false;
489
+ updateResizeHandlerStates("up", event);
490
+ }
474
491
  if (!isPointerDown) {
475
492
  const {
476
493
  target
@@ -2010,9 +2027,6 @@ function PanelGroupWithForwardedRef({
2010
2027
  } = dragState !== null && dragState !== void 0 ? dragState : {};
2011
2028
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
2012
2029
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
2013
- if (delta === 0) {
2014
- return;
2015
- }
2016
2030
 
2017
2031
  // Support RTL layouts
2018
2032
  const isHorizontal = direction === "horizontal";
@@ -2292,7 +2306,9 @@ function PanelResizeHandle({
2292
2306
  disabled = false,
2293
2307
  hitAreaMargins,
2294
2308
  id: idFromProps,
2309
+ onBlur,
2295
2310
  onDragging,
2311
+ onFocus,
2296
2312
  style: styleFromProps = {},
2297
2313
  tabIndex = 0,
2298
2314
  tagName: Type = "div",
@@ -2412,8 +2428,14 @@ function PanelResizeHandle({
2412
2428
  children,
2413
2429
  className: classNameFromProps,
2414
2430
  id: idFromProps,
2415
- onBlur: () => setIsFocused(false),
2416
- onFocus: () => setIsFocused(true),
2431
+ onBlur: () => {
2432
+ setIsFocused(false);
2433
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2434
+ },
2435
+ onFocus: () => {
2436
+ setIsFocused(true);
2437
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2438
+ },
2417
2439
  ref: elementRef,
2418
2440
  role: "separator",
2419
2441
  style: {
@@ -429,6 +429,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
429
429
  if (count === 1) {
430
430
  ownerDocumentCounts.delete(ownerDocument);
431
431
  }
432
+
433
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
434
+ // update the global pointer to account for the change
435
+ if (intersectingHandles.includes(data)) {
436
+ const index = intersectingHandles.indexOf(data);
437
+ if (index >= 0) {
438
+ intersectingHandles.splice(index, 1);
439
+ }
440
+ updateCursor();
441
+ }
432
442
  };
433
443
  }
434
444
  function handlePointerDown(event) {
@@ -457,6 +467,13 @@ function handlePointerMove(event) {
457
467
  x,
458
468
  y
459
469
  } = getResizeEventCoordinates(event);
470
+
471
+ // Edge case (see #340)
472
+ // Detect when the pointer has been released outside an iframe on a different domain
473
+ if (event.buttons === 0) {
474
+ isPointerDown = false;
475
+ updateResizeHandlerStates("up", event);
476
+ }
460
477
  if (!isPointerDown) {
461
478
  const {
462
479
  target
@@ -1802,9 +1819,6 @@ function PanelGroupWithForwardedRef({
1802
1819
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1803
1820
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1804
1821
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1805
- if (delta === 0) {
1806
- return;
1807
- }
1808
1822
 
1809
1823
  // Support RTL layouts
1810
1824
  const isHorizontal = direction === "horizontal";
@@ -2084,7 +2098,9 @@ function PanelResizeHandle({
2084
2098
  disabled = false,
2085
2099
  hitAreaMargins,
2086
2100
  id: idFromProps,
2101
+ onBlur,
2087
2102
  onDragging,
2103
+ onFocus,
2088
2104
  style: styleFromProps = {},
2089
2105
  tabIndex = 0,
2090
2106
  tagName: Type = "div",
@@ -2201,8 +2217,14 @@ function PanelResizeHandle({
2201
2217
  children,
2202
2218
  className: classNameFromProps,
2203
2219
  id: idFromProps,
2204
- onBlur: () => setIsFocused(false),
2205
- onFocus: () => setIsFocused(true),
2220
+ onBlur: () => {
2221
+ setIsFocused(false);
2222
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2223
+ },
2224
+ onFocus: () => {
2225
+ setIsFocused(true);
2226
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2227
+ },
2206
2228
  ref: elementRef,
2207
2229
  role: "separator",
2208
2230
  style: {
@@ -405,6 +405,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
405
405
  if (count === 1) {
406
406
  ownerDocumentCounts.delete(ownerDocument);
407
407
  }
408
+
409
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
410
+ // update the global pointer to account for the change
411
+ if (intersectingHandles.includes(data)) {
412
+ const index = intersectingHandles.indexOf(data);
413
+ if (index >= 0) {
414
+ intersectingHandles.splice(index, 1);
415
+ }
416
+ updateCursor();
417
+ }
408
418
  };
409
419
  }
410
420
  function handlePointerDown(event) {
@@ -433,6 +443,13 @@ function handlePointerMove(event) {
433
443
  x,
434
444
  y
435
445
  } = getResizeEventCoordinates(event);
446
+
447
+ // Edge case (see #340)
448
+ // Detect when the pointer has been released outside an iframe on a different domain
449
+ if (event.buttons === 0) {
450
+ isPointerDown = false;
451
+ updateResizeHandlerStates("up", event);
452
+ }
436
453
  if (!isPointerDown) {
437
454
  const {
438
455
  target
@@ -1778,9 +1795,6 @@ function PanelGroupWithForwardedRef({
1778
1795
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1779
1796
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1780
1797
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1781
- if (delta === 0) {
1782
- return;
1783
- }
1784
1798
 
1785
1799
  // Support RTL layouts
1786
1800
  const isHorizontal = direction === "horizontal";
@@ -2060,7 +2074,9 @@ function PanelResizeHandle({
2060
2074
  disabled = false,
2061
2075
  hitAreaMargins,
2062
2076
  id: idFromProps,
2077
+ onBlur,
2063
2078
  onDragging,
2079
+ onFocus,
2064
2080
  style: styleFromProps = {},
2065
2081
  tabIndex = 0,
2066
2082
  tagName: Type = "div",
@@ -2177,8 +2193,14 @@ function PanelResizeHandle({
2177
2193
  children,
2178
2194
  className: classNameFromProps,
2179
2195
  id: idFromProps,
2180
- onBlur: () => setIsFocused(false),
2181
- onFocus: () => setIsFocused(true),
2196
+ onBlur: () => {
2197
+ setIsFocused(false);
2198
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2199
+ },
2200
+ onFocus: () => {
2201
+ setIsFocused(true);
2202
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2203
+ },
2182
2204
  ref: elementRef,
2183
2205
  role: "separator",
2184
2206
  style: {
@@ -432,6 +432,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
432
432
  if (count === 1) {
433
433
  ownerDocumentCounts.delete(ownerDocument);
434
434
  }
435
+
436
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
437
+ // update the global pointer to account for the change
438
+ if (intersectingHandles.includes(data)) {
439
+ const index = intersectingHandles.indexOf(data);
440
+ if (index >= 0) {
441
+ intersectingHandles.splice(index, 1);
442
+ }
443
+ updateCursor();
444
+ }
435
445
  };
436
446
  }
437
447
  function handlePointerDown(event) {
@@ -460,6 +470,13 @@ function handlePointerMove(event) {
460
470
  x,
461
471
  y
462
472
  } = getResizeEventCoordinates(event);
473
+
474
+ // Edge case (see #340)
475
+ // Detect when the pointer has been released outside an iframe on a different domain
476
+ if (event.buttons === 0) {
477
+ isPointerDown = false;
478
+ updateResizeHandlerStates("up", event);
479
+ }
463
480
  if (!isPointerDown) {
464
481
  const {
465
482
  target
@@ -1899,9 +1916,6 @@ function PanelGroupWithForwardedRef({
1899
1916
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1900
1917
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1901
1918
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1902
- if (delta === 0) {
1903
- return;
1904
- }
1905
1919
 
1906
1920
  // Support RTL layouts
1907
1921
  const isHorizontal = direction === "horizontal";
@@ -2181,7 +2195,9 @@ function PanelResizeHandle({
2181
2195
  disabled = false,
2182
2196
  hitAreaMargins,
2183
2197
  id: idFromProps,
2198
+ onBlur,
2184
2199
  onDragging,
2200
+ onFocus,
2185
2201
  style: styleFromProps = {},
2186
2202
  tabIndex = 0,
2187
2203
  tagName: Type = "div",
@@ -2301,8 +2317,14 @@ function PanelResizeHandle({
2301
2317
  children,
2302
2318
  className: classNameFromProps,
2303
2319
  id: idFromProps,
2304
- onBlur: () => setIsFocused(false),
2305
- onFocus: () => setIsFocused(true),
2320
+ onBlur: () => {
2321
+ setIsFocused(false);
2322
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2323
+ },
2324
+ onFocus: () => {
2325
+ setIsFocused(true);
2326
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2327
+ },
2306
2328
  ref: elementRef,
2307
2329
  role: "separator",
2308
2330
  style: {
@@ -418,6 +418,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
418
418
  if (count === 1) {
419
419
  ownerDocumentCounts.delete(ownerDocument);
420
420
  }
421
+
422
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
423
+ // update the global pointer to account for the change
424
+ if (intersectingHandles.includes(data)) {
425
+ const index = intersectingHandles.indexOf(data);
426
+ if (index >= 0) {
427
+ intersectingHandles.splice(index, 1);
428
+ }
429
+ updateCursor();
430
+ }
421
431
  };
422
432
  }
423
433
  function handlePointerDown(event) {
@@ -446,6 +456,13 @@ function handlePointerMove(event) {
446
456
  x,
447
457
  y
448
458
  } = getResizeEventCoordinates(event);
459
+
460
+ // Edge case (see #340)
461
+ // Detect when the pointer has been released outside an iframe on a different domain
462
+ if (event.buttons === 0) {
463
+ isPointerDown = false;
464
+ updateResizeHandlerStates("up", event);
465
+ }
449
466
  if (!isPointerDown) {
450
467
  const {
451
468
  target
@@ -1701,9 +1718,6 @@ function PanelGroupWithForwardedRef({
1701
1718
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1702
1719
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1703
1720
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1704
- if (delta === 0) {
1705
- return;
1706
- }
1707
1721
 
1708
1722
  // Support RTL layouts
1709
1723
  const isHorizontal = direction === "horizontal";
@@ -1983,7 +1997,9 @@ function PanelResizeHandle({
1983
1997
  disabled = false,
1984
1998
  hitAreaMargins,
1985
1999
  id: idFromProps,
2000
+ onBlur,
1986
2001
  onDragging,
2002
+ onFocus,
1987
2003
  style: styleFromProps = {},
1988
2004
  tabIndex = 0,
1989
2005
  tagName: Type = "div",
@@ -2100,8 +2116,14 @@ function PanelResizeHandle({
2100
2116
  children,
2101
2117
  className: classNameFromProps,
2102
2118
  id: idFromProps,
2103
- onBlur: () => setIsFocused(false),
2104
- onFocus: () => setIsFocused(true),
2119
+ onBlur: () => {
2120
+ setIsFocused(false);
2121
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2122
+ },
2123
+ onFocus: () => {
2124
+ setIsFocused(true);
2125
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2126
+ },
2105
2127
  ref: elementRef,
2106
2128
  role: "separator",
2107
2129
  style: {
@@ -394,6 +394,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
394
394
  if (count === 1) {
395
395
  ownerDocumentCounts.delete(ownerDocument);
396
396
  }
397
+
398
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
399
+ // update the global pointer to account for the change
400
+ if (intersectingHandles.includes(data)) {
401
+ const index = intersectingHandles.indexOf(data);
402
+ if (index >= 0) {
403
+ intersectingHandles.splice(index, 1);
404
+ }
405
+ updateCursor();
406
+ }
397
407
  };
398
408
  }
399
409
  function handlePointerDown(event) {
@@ -422,6 +432,13 @@ function handlePointerMove(event) {
422
432
  x,
423
433
  y
424
434
  } = getResizeEventCoordinates(event);
435
+
436
+ // Edge case (see #340)
437
+ // Detect when the pointer has been released outside an iframe on a different domain
438
+ if (event.buttons === 0) {
439
+ isPointerDown = false;
440
+ updateResizeHandlerStates("up", event);
441
+ }
425
442
  if (!isPointerDown) {
426
443
  const {
427
444
  target
@@ -1677,9 +1694,6 @@ function PanelGroupWithForwardedRef({
1677
1694
  } = dragState !== null && dragState !== void 0 ? dragState : {};
1678
1695
  const pivotIndices = determinePivotIndices(groupId, dragHandleId, panelGroupElement);
1679
1696
  let delta = calculateDeltaPercentage(event, dragHandleId, direction, dragState, keyboardResizeBy, panelGroupElement);
1680
- if (delta === 0) {
1681
- return;
1682
- }
1683
1697
 
1684
1698
  // Support RTL layouts
1685
1699
  const isHorizontal = direction === "horizontal";
@@ -1959,7 +1973,9 @@ function PanelResizeHandle({
1959
1973
  disabled = false,
1960
1974
  hitAreaMargins,
1961
1975
  id: idFromProps,
1976
+ onBlur,
1962
1977
  onDragging,
1978
+ onFocus,
1963
1979
  style: styleFromProps = {},
1964
1980
  tabIndex = 0,
1965
1981
  tagName: Type = "div",
@@ -2076,8 +2092,14 @@ function PanelResizeHandle({
2076
2092
  children,
2077
2093
  className: classNameFromProps,
2078
2094
  id: idFromProps,
2079
- onBlur: () => setIsFocused(false),
2080
- onFocus: () => setIsFocused(true),
2095
+ onBlur: () => {
2096
+ setIsFocused(false);
2097
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur();
2098
+ },
2099
+ onFocus: () => {
2100
+ setIsFocused(true);
2101
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus();
2102
+ },
2081
2103
  ref: elementRef,
2082
2104
  role: "separator",
2083
2105
  style: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-resizable-panels",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "React components for resizable panel groups/layouts",
5
5
  "author": "Brian Vaughn <brian.david.vaughn@gmail.com>",
6
6
  "license": "MIT",
package/src/PanelGroup.ts CHANGED
@@ -638,9 +638,6 @@ function PanelGroupWithForwardedRef({
638
638
  keyboardResizeBy,
639
639
  panelGroupElement
640
640
  );
641
- if (delta === 0) {
642
- return;
643
- }
644
641
 
645
642
  // Support RTL layouts
646
643
  const isHorizontal = direction === "horizontal";
@@ -1,8 +1,13 @@
1
1
  import { Root, createRoot } from "react-dom/client";
2
2
  import { act } from "react-dom/test-utils";
3
- import type { PanelResizeHandleProps } from "react-resizable-panels";
4
- import { Panel, PanelGroup, PanelResizeHandle } from ".";
3
+ import {
4
+ Panel,
5
+ PanelGroup,
6
+ PanelResizeHandle,
7
+ type PanelResizeHandleProps,
8
+ } from ".";
5
9
  import { assert } from "./utils/assert";
10
+ import * as cursorUtils from "./utils/cursor";
6
11
  import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
7
12
  import {
8
13
  dispatchPointerEvent,
@@ -10,6 +15,12 @@ import {
10
15
  verifyAttribute,
11
16
  } from "./utils/test-utils";
12
17
 
18
+ jest.mock("./utils/cursor", () => ({
19
+ getCursorStyle: jest.fn(),
20
+ resetGlobalCursorStyle: jest.fn(),
21
+ setGlobalCursorStyle: jest.fn(),
22
+ }));
23
+
13
24
  describe("PanelResizeHandle", () => {
14
25
  let expectedWarnings: string[] = [];
15
26
  let root: Root;
@@ -305,4 +316,33 @@ describe("PanelResizeHandle", () => {
305
316
  expect(element?.getAttribute("id")).toBeNull();
306
317
  });
307
318
  });
319
+
320
+ it("resets the global cursor style on unmount", () => {
321
+ const onDraggingLeft = jest.fn();
322
+
323
+ const { leftElement } = setupMockedGroup({
324
+ leftProps: { onDragging: onDraggingLeft },
325
+ rightProps: {},
326
+ });
327
+
328
+ act(() => {
329
+ dispatchPointerEvent("pointermove", leftElement);
330
+ });
331
+
332
+ act(() => {
333
+ dispatchPointerEvent("pointerdown", leftElement);
334
+ });
335
+ expect(onDraggingLeft).toHaveBeenCalledTimes(1);
336
+ expect(onDraggingLeft).toHaveBeenCalledWith(true);
337
+
338
+ expect(cursorUtils.resetGlobalCursorStyle).not.toHaveBeenCalled();
339
+ expect(cursorUtils.setGlobalCursorStyle).toHaveBeenCalledTimes(1);
340
+
341
+ act(() => {
342
+ root.unmount();
343
+ });
344
+
345
+ expect(cursorUtils.resetGlobalCursorStyle).toHaveBeenCalled();
346
+ expect(cursorUtils.setGlobalCursorStyle).toHaveBeenCalledTimes(1);
347
+ });
308
348
  });
@@ -1,16 +1,5 @@
1
+ import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
1
2
  import useUniqueId from "./hooks/useUniqueId";
2
- import {
3
- createElement,
4
- CSSProperties,
5
- HTMLAttributes,
6
- PropsWithChildren,
7
- ReactElement,
8
- useContext,
9
- useEffect,
10
- useRef,
11
- useState,
12
- } from "./vendor/react";
13
-
14
3
  import { useWindowSplitterResizeHandlerBehavior } from "./hooks/useWindowSplitterBehavior";
15
4
  import {
16
5
  PanelGroupContext,
@@ -23,21 +12,33 @@ import {
23
12
  ResizeHandlerAction,
24
13
  } from "./PanelResizeHandleRegistry";
25
14
  import { assert } from "./utils/assert";
26
- import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
15
+ import {
16
+ createElement,
17
+ CSSProperties,
18
+ HTMLAttributes,
19
+ PropsWithChildren,
20
+ ReactElement,
21
+ useContext,
22
+ useEffect,
23
+ useRef,
24
+ useState,
25
+ } from "./vendor/react";
27
26
 
28
27
  export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
29
28
  export type ResizeHandlerState = "drag" | "hover" | "inactive";
30
29
 
31
30
  export type PanelResizeHandleProps = Omit<
32
31
  HTMLAttributes<keyof HTMLElementTagNameMap>,
33
- "id"
32
+ "id" | "onBlur" | "onFocus"
34
33
  > &
35
34
  PropsWithChildren<{
36
35
  className?: string;
37
36
  disabled?: boolean;
38
37
  hitAreaMargins?: PointerHitAreaMargins;
39
38
  id?: string | null;
39
+ onBlur?: () => void;
40
40
  onDragging?: PanelResizeHandleOnDragging;
41
+ onFocus?: () => void;
41
42
  style?: CSSProperties;
42
43
  tabIndex?: number;
43
44
  tagName?: keyof HTMLElementTagNameMap;
@@ -49,7 +50,9 @@ export function PanelResizeHandle({
49
50
  disabled = false,
50
51
  hitAreaMargins,
51
52
  id: idFromProps,
53
+ onBlur,
52
54
  onDragging,
55
+ onFocus,
53
56
  style: styleFromProps = {},
54
57
  tabIndex = 0,
55
58
  tagName: Type = "div",
@@ -208,8 +211,14 @@ export function PanelResizeHandle({
208
211
  children,
209
212
  className: classNameFromProps,
210
213
  id: idFromProps,
211
- onBlur: () => setIsFocused(false),
212
- onFocus: () => setIsFocused(true),
214
+ onBlur: () => {
215
+ setIsFocused(false);
216
+ onBlur?.();
217
+ },
218
+ onFocus: () => {
219
+ setIsFocused(true);
220
+ onFocus?.();
221
+ },
213
222
  ref: elementRef,
214
223
  role: "separator",
215
224
  style: {
@@ -73,10 +73,21 @@ export function registerResizeHandle(
73
73
  if (count === 1) {
74
74
  ownerDocumentCounts.delete(ownerDocument);
75
75
  }
76
+
77
+ // If the resize handle that is currently unmounting is intersecting with the pointer,
78
+ // update the global pointer to account for the change
79
+ if (intersectingHandles.includes(data)) {
80
+ const index = intersectingHandles.indexOf(data);
81
+ if (index >= 0) {
82
+ intersectingHandles.splice(index, 1);
83
+ }
84
+
85
+ updateCursor();
86
+ }
76
87
  };
77
88
  }
78
89
 
79
- function handlePointerDown(event: ResizeEvent) {
90
+ function handlePointerDown(event: PointerEvent) {
80
91
  const { target } = event;
81
92
  const { x, y } = getResizeEventCoordinates(event);
82
93
 
@@ -93,9 +104,17 @@ function handlePointerDown(event: ResizeEvent) {
93
104
  }
94
105
  }
95
106
 
96
- function handlePointerMove(event: ResizeEvent) {
107
+ function handlePointerMove(event: PointerEvent) {
97
108
  const { x, y } = getResizeEventCoordinates(event);
98
109
 
110
+ // Edge case (see #340)
111
+ // Detect when the pointer has been released outside an iframe on a different domain
112
+ if (event.buttons === 0) {
113
+ isPointerDown = false;
114
+
115
+ updateResizeHandlerStates("up", event);
116
+ }
117
+
99
118
  if (!isPointerDown) {
100
119
  const { target } = event;
101
120
 
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import type {
29
29
  PanelResizeHandleOnDragging,
30
30
  PanelResizeHandleProps,
31
31
  } from "./PanelResizeHandle";
32
+ import type { PointerHitAreaMargins } from "./PanelResizeHandleRegistry";
32
33
 
33
34
  export {
34
35
  // TypeScript types
@@ -43,6 +44,7 @@ export {
43
44
  PanelProps,
44
45
  PanelResizeHandleOnDragging,
45
46
  PanelResizeHandleProps,
47
+ PointerHitAreaMargins,
46
48
 
47
49
  // React components
48
50
  Panel,
@@ -12,6 +12,7 @@ export function dispatchPointerEvent(type: string, target: HTMLElement) {
12
12
  bubbles: true,
13
13
  clientX,
14
14
  clientY,
15
+ buttons: 1,
15
16
  });
16
17
  Object.defineProperties(event, {
17
18
  pageX: {