react-resizable-panels 2.0.3 → 2.0.5

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 (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/declarations/src/PanelResizeHandle.d.ts +1 -0
  3. package/dist/declarations/src/PanelResizeHandleRegistry.d.ts +1 -2
  4. package/dist/declarations/src/index.d.ts +3 -1
  5. package/dist/declarations/src/utils/rects/getIntersectingRectangle.d.ts +2 -0
  6. package/dist/declarations/src/utils/rects/intersects.d.ts +2 -0
  7. package/dist/declarations/src/utils/rects/types.d.ts +6 -0
  8. package/dist/react-resizable-panels.browser.cjs.js +138 -56
  9. package/dist/react-resizable-panels.browser.cjs.mjs +3 -1
  10. package/dist/react-resizable-panels.browser.development.cjs.js +138 -56
  11. package/dist/react-resizable-panels.browser.development.cjs.mjs +3 -1
  12. package/dist/react-resizable-panels.browser.development.esm.js +137 -57
  13. package/dist/react-resizable-panels.browser.esm.js +137 -57
  14. package/dist/react-resizable-panels.cjs.js +138 -56
  15. package/dist/react-resizable-panels.cjs.mjs +3 -1
  16. package/dist/react-resizable-panels.development.cjs.js +138 -56
  17. package/dist/react-resizable-panels.development.cjs.mjs +3 -1
  18. package/dist/react-resizable-panels.development.esm.js +137 -57
  19. package/dist/react-resizable-panels.development.node.cjs.js +138 -56
  20. package/dist/react-resizable-panels.development.node.cjs.mjs +3 -1
  21. package/dist/react-resizable-panels.development.node.esm.js +137 -57
  22. package/dist/react-resizable-panels.esm.js +137 -57
  23. package/dist/react-resizable-panels.node.cjs.js +138 -56
  24. package/dist/react-resizable-panels.node.cjs.mjs +3 -1
  25. package/dist/react-resizable-panels.node.esm.js +137 -57
  26. package/package.json +4 -1
  27. package/src/Panel.test.tsx +63 -0
  28. package/src/PanelGroup.test.tsx +21 -1
  29. package/src/PanelResizeHandle.test.tsx +181 -22
  30. package/src/PanelResizeHandle.ts +44 -24
  31. package/src/PanelResizeHandleRegistry.ts +87 -30
  32. package/src/index.ts +4 -0
  33. package/src/utils/rects/getIntersectingRectangle.test.ts +198 -0
  34. package/src/utils/rects/getIntersectingRectangle.ts +28 -0
  35. package/src/utils/rects/intersects.test.ts +197 -0
  36. package/src/utils/rects/intersects.ts +23 -0
  37. package/src/utils/rects/types.ts +6 -0
  38. package/src/utils/test-utils.ts +39 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.5
4
+
5
+ - Resize handle hit detection considers [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context) when determining hit detection (#291)
6
+
7
+ ## 2.0.4
8
+
9
+ - Fixed `PanelResizeHandle` `onDragging` prop to only be called for the handle being dragged (#289)
10
+
3
11
  ## 2.0.3
4
12
 
5
13
  - Fix resize handle onDragging callback (#278)
@@ -1,6 +1,7 @@
1
1
  import { CSSProperties, HTMLAttributes, PropsWithChildren, ReactElement } from "./vendor/react.js";
2
2
  import { PointerHitAreaMargins } from "./PanelResizeHandleRegistry.js";
3
3
  export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
4
+ export type ResizeHandlerState = "drag" | "hover" | "inactive";
4
5
  export type PanelResizeHandleProps = Omit<HTMLAttributes<keyof HTMLElementTagNameMap>, "id"> & PropsWithChildren<{
5
6
  className?: string;
6
7
  disabled?: boolean;
@@ -1,7 +1,6 @@
1
1
  import { Direction, ResizeEvent } from "./types.js";
2
2
  export type ResizeHandlerAction = "down" | "move" | "up";
3
- export type ResizeHandlerState = "drag" | "hover" | "inactive";
4
- export type SetResizeHandlerState = (action: ResizeHandlerAction, state: ResizeHandlerState, event: ResizeEvent) => void;
3
+ export type SetResizeHandlerState = (action: ResizeHandlerAction, isActive: boolean, event: ResizeEvent) => void;
5
4
  export type PointerHitAreaMargins = {
6
5
  coarse: number;
7
6
  fine: number;
@@ -9,7 +9,9 @@ import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement.js";
9
9
  import { getResizeHandleElementIndex } from "./utils/dom/getResizeHandleElementIndex.js";
10
10
  import { getResizeHandleElementsForGroup } from "./utils/dom/getResizeHandleElementsForGroup.js";
11
11
  import { getResizeHandlePanelIds } from "./utils/dom/getResizeHandlePanelIds.js";
12
+ import { getIntersectingRectangle } from "./utils/rects/getIntersectingRectangle.js";
13
+ import { intersects } from "./utils/rects/intersects.js";
12
14
  import type { ImperativePanelHandle, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps } from "./Panel.js";
13
15
  import type { ImperativePanelGroupHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage } from "./PanelGroup.js";
14
16
  import type { PanelResizeHandleOnDragging, PanelResizeHandleProps } from "./PanelResizeHandle.js";
15
- export { ImperativePanelGroupHandle, ImperativePanelHandle, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage, PanelOnCollapse, PanelOnExpand, PanelOnResize, PanelProps, PanelResizeHandleOnDragging, PanelResizeHandleProps, Panel, PanelGroup, PanelResizeHandle, assert, getPanelElement, getPanelElementsForGroup, getPanelGroupElement, getResizeHandleElement, getResizeHandleElementIndex, getResizeHandleElementsForGroup, getResizeHandlePanelIds, };
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, };
@@ -0,0 +1,2 @@
1
+ import { Rectangle } from "./types.js";
2
+ export declare function getIntersectingRectangle(rectOne: Rectangle, rectTwo: Rectangle, strict: boolean): Rectangle;
@@ -0,0 +1,2 @@
1
+ import { Rectangle } from "./types.js";
2
+ export declare function intersects(rectOne: Rectangle, rectTwo: Rectangle, strict: boolean): boolean;
@@ -0,0 +1,6 @@
1
+ export interface Rectangle {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var React = require('react');
6
+ var stackingOrder = require('stacking-order');
6
7
 
7
8
  function _interopNamespace(e) {
8
9
  if (e && e.__esModule) return e;
@@ -294,6 +295,14 @@ function getInputType() {
294
295
  }
295
296
  }
296
297
 
298
+ function intersects(rectOne, rectTwo, strict) {
299
+ if (strict) {
300
+ return rectOne.x < rectTwo.x + rectTwo.width && rectOne.x + rectOne.width > rectTwo.x && rectOne.y < rectTwo.y + rectTwo.height && rectOne.y + rectOne.height > rectTwo.y;
301
+ } else {
302
+ return rectOne.x <= rectTwo.x + rectTwo.width && rectOne.x + rectOne.width >= rectTwo.x && rectOne.y <= rectTwo.y + rectTwo.height && rectOne.y + rectOne.height >= rectTwo.y;
303
+ }
304
+ }
305
+
297
306
  const EXCEEDED_HORIZONTAL_MIN = 0b0001;
298
307
  const EXCEEDED_HORIZONTAL_MAX = 0b0010;
299
308
  const EXCEEDED_VERTICAL_MIN = 0b0100;
@@ -332,12 +341,16 @@ function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins
332
341
  };
333
342
  }
334
343
  function handlePointerDown(event) {
344
+ const {
345
+ target
346
+ } = event;
335
347
  const {
336
348
  x,
337
349
  y
338
350
  } = getResizeEventCoordinates(event);
339
351
  isPointerDown = true;
340
352
  recalculateIntersectingHandles({
353
+ target,
341
354
  x,
342
355
  y
343
356
  });
@@ -352,29 +365,32 @@ function handlePointerMove(event) {
352
365
  x,
353
366
  y
354
367
  } = getResizeEventCoordinates(event);
355
- if (isPointerDown) {
356
- intersectingHandles.forEach(data => {
357
- const {
358
- setResizeHandlerState
359
- } = data;
360
- setResizeHandlerState("move", "drag", event);
361
- });
368
+ if (!isPointerDown) {
369
+ const {
370
+ target
371
+ } = event;
362
372
 
363
- // Update cursor based on return value(s) from active handles
364
- updateCursor();
365
- } else {
373
+ // Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed
374
+ // at that point, the handles may not move with the pointer (depending on constraints)
375
+ // but the same set of active handles should be locked until the pointer is released
366
376
  recalculateIntersectingHandles({
377
+ target,
367
378
  x,
368
379
  y
369
380
  });
370
- updateResizeHandlerStates("move", event);
371
- updateCursor();
372
381
  }
382
+ updateResizeHandlerStates("move", event);
383
+
384
+ // Update cursor based on return value(s) from active handles
385
+ updateCursor();
373
386
  if (intersectingHandles.length > 0) {
374
387
  event.preventDefault();
375
388
  }
376
389
  }
377
390
  function handlePointerUp(event) {
391
+ const {
392
+ target
393
+ } = event;
378
394
  const {
379
395
  x,
380
396
  y
@@ -384,33 +400,72 @@ function handlePointerUp(event) {
384
400
  if (intersectingHandles.length > 0) {
385
401
  event.preventDefault();
386
402
  }
403
+ updateResizeHandlerStates("up", event);
387
404
  recalculateIntersectingHandles({
405
+ target,
388
406
  x,
389
407
  y
390
408
  });
391
- updateResizeHandlerStates("up", event);
392
409
  updateCursor();
393
410
  updateListeners();
394
411
  }
395
412
  function recalculateIntersectingHandles({
413
+ target,
396
414
  x,
397
415
  y
398
416
  }) {
399
417
  intersectingHandles.splice(0);
418
+ let targetElement = null;
419
+ if (target instanceof HTMLElement) {
420
+ targetElement = target;
421
+ }
400
422
  registeredResizeHandlers.forEach(data => {
401
423
  const {
402
- element,
424
+ element: dragHandleElement,
403
425
  hitAreaMargins
404
426
  } = data;
427
+ const dragHandleRect = dragHandleElement.getBoundingClientRect();
405
428
  const {
406
429
  bottom,
407
430
  left,
408
431
  right,
409
432
  top
410
- } = element.getBoundingClientRect();
433
+ } = dragHandleRect;
411
434
  const margin = isCoarsePointer ? hitAreaMargins.coarse : hitAreaMargins.fine;
412
- const intersects = x >= left - margin && x <= right + margin && y >= top - margin && y <= bottom + margin;
413
- if (intersects) {
435
+ const eventIntersects = x >= left - margin && x <= right + margin && y >= top - margin && y <= bottom + margin;
436
+ if (eventIntersects) {
437
+ // TRICKY
438
+ // We listen for pointers events at the root in order to support hit area margins
439
+ // (determining when the pointer is close enough to an element to be considered a "hit")
440
+ // Clicking on an element "above" a handle (e.g. a modal) should prevent a hit though
441
+ // so at this point we need to compare stacking order of a potentially intersecting drag handle,
442
+ // and the element that was actually clicked/touched
443
+ if (targetElement !== null && dragHandleElement !== targetElement && !dragHandleElement.contains(targetElement) && !targetElement.contains(dragHandleElement) &&
444
+ // Calculating stacking order has a cost, so we should avoid it if possible
445
+ // That is why we only check potentially intersecting handles,
446
+ // and why we skip if the event target is within the handle's DOM
447
+ stackingOrder.compare(targetElement, dragHandleElement) > 0) {
448
+ // If the target is above the drag handle, then we also need to confirm they overlap
449
+ // If they are beside each other (e.g. a panel and its drag handle) then the handle is still interactive
450
+ //
451
+ // It's not enough to compare only the target
452
+ // The target might be a small element inside of a larger container
453
+ // (For example, a SPAN or a DIV inside of a larger modal dialog)
454
+ let currentElement = targetElement;
455
+ let didIntersect = false;
456
+ while (currentElement) {
457
+ if (currentElement.contains(dragHandleElement)) {
458
+ break;
459
+ } else if (intersects(currentElement.getBoundingClientRect(), dragHandleRect, true)) {
460
+ didIntersect = true;
461
+ break;
462
+ }
463
+ currentElement = currentElement.parentElement;
464
+ }
465
+ if (didIntersect) {
466
+ return;
467
+ }
468
+ }
414
469
  intersectingHandles.push(data);
415
470
  }
416
471
  });
@@ -502,15 +557,8 @@ function updateResizeHandlerStates(action, event) {
502
557
  const {
503
558
  setResizeHandlerState
504
559
  } = data;
505
- if (intersectingHandles.includes(data)) {
506
- if (isPointerDown) {
507
- setResizeHandlerState(action, "drag", event);
508
- } else {
509
- setResizeHandlerState(action, "hover", event);
510
- }
511
- } else {
512
- setResizeHandlerState(action, "inactive", event);
513
- }
560
+ const isActive = intersectingHandles.includes(data);
561
+ setResizeHandlerState(action, isActive, event);
514
562
  });
515
563
  }
516
564
 
@@ -2047,6 +2095,12 @@ function PanelResizeHandle({
2047
2095
  const [state, setState] = useState("inactive");
2048
2096
  const [isFocused, setIsFocused] = useState(false);
2049
2097
  const [resizeHandler, setResizeHandler] = useState(null);
2098
+ const committedValuesRef = useRef({
2099
+ state
2100
+ });
2101
+ useLayoutEffect(() => {
2102
+ committedValuesRef.current.state = state;
2103
+ });
2050
2104
  useEffect(() => {
2051
2105
  if (disabled) {
2052
2106
  setResizeHandler(null);
@@ -2062,38 +2116,47 @@ function PanelResizeHandle({
2062
2116
  }
2063
2117
  const element = elementRef.current;
2064
2118
  assert(element);
2065
- const setResizeHandlerState = (action, state, event) => {
2066
- setState(state);
2067
- switch (action) {
2068
- case "down":
2069
- {
2070
- startDragging(resizeHandleId, event);
2071
- const {
2072
- onDragging
2073
- } = callbacksRef.current;
2074
- if (onDragging) {
2075
- onDragging(true);
2119
+ const setResizeHandlerState = (action, isActive, event) => {
2120
+ if (isActive) {
2121
+ switch (action) {
2122
+ case "down":
2123
+ {
2124
+ setState("drag");
2125
+ startDragging(resizeHandleId, event);
2126
+ const {
2127
+ onDragging
2128
+ } = callbacksRef.current;
2129
+ if (onDragging) {
2130
+ onDragging(true);
2131
+ }
2132
+ break;
2076
2133
  }
2077
- break;
2078
- }
2079
- case "up":
2080
- {
2081
- stopDragging();
2082
- const {
2083
- onDragging
2084
- } = callbacksRef.current;
2085
- if (onDragging) {
2086
- onDragging(false);
2134
+ case "move":
2135
+ {
2136
+ const {
2137
+ state
2138
+ } = committedValuesRef.current;
2139
+ if (state !== "drag") {
2140
+ setState("hover");
2141
+ }
2142
+ resizeHandler(event);
2143
+ break;
2087
2144
  }
2088
- break;
2089
- }
2090
- }
2091
- switch (state) {
2092
- case "drag":
2093
- {
2094
- resizeHandler(event);
2095
- break;
2096
- }
2145
+ case "up":
2146
+ {
2147
+ setState("hover");
2148
+ stopDragging();
2149
+ const {
2150
+ onDragging
2151
+ } = callbacksRef.current;
2152
+ if (onDragging) {
2153
+ onDragging(false);
2154
+ }
2155
+ break;
2156
+ }
2157
+ }
2158
+ } else {
2159
+ setState("inactive");
2097
2160
  }
2098
2161
  };
2099
2162
  return registerResizeHandle(resizeHandleId, element, direction, {
@@ -2150,10 +2213,28 @@ function getPanelElementsForGroup(groupId, scope = document) {
2150
2213
  return Array.from(scope.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
2151
2214
  }
2152
2215
 
2216
+ function getIntersectingRectangle(rectOne, rectTwo, strict) {
2217
+ if (!intersects(rectOne, rectTwo, strict)) {
2218
+ return {
2219
+ x: 0,
2220
+ y: 0,
2221
+ width: 0,
2222
+ height: 0
2223
+ };
2224
+ }
2225
+ return {
2226
+ x: Math.max(rectOne.x, rectTwo.x),
2227
+ y: Math.max(rectOne.y, rectTwo.y),
2228
+ width: Math.min(rectOne.x + rectOne.width, rectTwo.x + rectTwo.width) - Math.max(rectOne.x, rectTwo.x),
2229
+ height: Math.min(rectOne.y + rectOne.height, rectTwo.y + rectTwo.height) - Math.max(rectOne.y, rectTwo.y)
2230
+ };
2231
+ }
2232
+
2153
2233
  exports.Panel = Panel;
2154
2234
  exports.PanelGroup = PanelGroup;
2155
2235
  exports.PanelResizeHandle = PanelResizeHandle;
2156
2236
  exports.assert = assert;
2237
+ exports.getIntersectingRectangle = getIntersectingRectangle;
2157
2238
  exports.getPanelElement = getPanelElement;
2158
2239
  exports.getPanelElementsForGroup = getPanelElementsForGroup;
2159
2240
  exports.getPanelGroupElement = getPanelGroupElement;
@@ -2161,3 +2242,4 @@ exports.getResizeHandleElement = getResizeHandleElement;
2161
2242
  exports.getResizeHandleElementIndex = getResizeHandleElementIndex;
2162
2243
  exports.getResizeHandleElementsForGroup = getResizeHandleElementsForGroup;
2163
2244
  exports.getResizeHandlePanelIds = getResizeHandlePanelIds;
2245
+ exports.intersects = intersects;
@@ -3,11 +3,13 @@ export {
3
3
  PanelGroup,
4
4
  PanelResizeHandle,
5
5
  assert,
6
+ getIntersectingRectangle,
6
7
  getPanelElement,
7
8
  getPanelElementsForGroup,
8
9
  getPanelGroupElement,
9
10
  getResizeHandleElement,
10
11
  getResizeHandleElementIndex,
11
12
  getResizeHandleElementsForGroup,
12
- getResizeHandlePanelIds
13
+ getResizeHandlePanelIds,
14
+ intersects
13
15
  } from "./react-resizable-panels.browser.cjs.js";