react-resizable-panels 2.0.4 → 2.0.6

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 (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/declarations/src/index.d.ts +3 -1
  3. package/dist/declarations/src/utils/rects/getIntersectingRectangle.d.ts +2 -0
  4. package/dist/declarations/src/utils/rects/intersects.d.ts +2 -0
  5. package/dist/declarations/src/utils/rects/types.d.ts +6 -0
  6. package/dist/declarations/src/vendor/react.d.ts +3 -2
  7. package/dist/react-resizable-panels.browser.cjs.js +87 -7
  8. package/dist/react-resizable-panels.browser.cjs.mjs +3 -1
  9. package/dist/react-resizable-panels.browser.development.cjs.js +87 -7
  10. package/dist/react-resizable-panels.browser.development.cjs.mjs +3 -1
  11. package/dist/react-resizable-panels.browser.development.esm.js +86 -8
  12. package/dist/react-resizable-panels.browser.esm.js +86 -8
  13. package/dist/react-resizable-panels.cjs.js +87 -7
  14. package/dist/react-resizable-panels.cjs.mjs +3 -1
  15. package/dist/react-resizable-panels.development.cjs.js +87 -7
  16. package/dist/react-resizable-panels.development.cjs.mjs +3 -1
  17. package/dist/react-resizable-panels.development.esm.js +86 -8
  18. package/dist/react-resizable-panels.development.node.cjs.js +84 -8
  19. package/dist/react-resizable-panels.development.node.cjs.mjs +3 -1
  20. package/dist/react-resizable-panels.development.node.esm.js +83 -9
  21. package/dist/react-resizable-panels.esm.js +86 -8
  22. package/dist/react-resizable-panels.node.cjs.js +84 -8
  23. package/dist/react-resizable-panels.node.cjs.mjs +3 -1
  24. package/dist/react-resizable-panels.node.esm.js +83 -9
  25. package/package.json +4 -1
  26. package/src/PanelResizeHandle.ts +2 -2
  27. package/src/PanelResizeHandleRegistry.ts +75 -8
  28. package/src/hooks/useIsomorphicEffect.ts +4 -2
  29. package/src/index.ts +4 -0
  30. package/src/utils/rects/getIntersectingRectangle.test.ts +198 -0
  31. package/src/utils/rects/getIntersectingRectangle.ts +28 -0
  32. package/src/utils/rects/intersects.test.ts +197 -0
  33. package/src/utils/rects/intersects.ts +23 -0
  34. package/src/utils/rects/types.ts +6 -0
  35. package/src/vendor/react.ts +3 -1
@@ -1,7 +1,9 @@
1
+ import { compare } from "stacking-order";
1
2
  import { Direction, ResizeEvent } from "./types";
2
3
  import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
3
4
  import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordinates";
4
5
  import { getInputType } from "./utils/getInputType";
6
+ import { intersects } from "./utils/rects/intersects";
5
7
 
6
8
  export type ResizeHandlerAction = "down" | "move" | "up";
7
9
  export type SetResizeHandlerState = (
@@ -75,11 +77,12 @@ export function registerResizeHandle(
75
77
  }
76
78
 
77
79
  function handlePointerDown(event: ResizeEvent) {
80
+ const { target } = event;
78
81
  const { x, y } = getResizeEventCoordinates(event);
79
82
 
80
83
  isPointerDown = true;
81
84
 
82
- recalculateIntersectingHandles({ x, y });
85
+ recalculateIntersectingHandles({ target, x, y });
83
86
  updateListeners();
84
87
 
85
88
  if (intersectingHandles.length > 0) {
@@ -93,10 +96,12 @@ function handlePointerMove(event: ResizeEvent) {
93
96
  const { x, y } = getResizeEventCoordinates(event);
94
97
 
95
98
  if (!isPointerDown) {
99
+ const { target } = event;
100
+
96
101
  // Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed
97
102
  // at that point, the handles may not move with the pointer (depending on constraints)
98
103
  // but the same set of active handles should be locked until the pointer is released
99
- recalculateIntersectingHandles({ x, y });
104
+ recalculateIntersectingHandles({ target, x, y });
100
105
  }
101
106
 
102
107
  updateResizeHandlerStates("move", event);
@@ -110,6 +115,7 @@ function handlePointerMove(event: ResizeEvent) {
110
115
  }
111
116
 
112
117
  function handlePointerUp(event: ResizeEvent) {
118
+ const { target } = event;
113
119
  const { x, y } = getResizeEventCoordinates(event);
114
120
 
115
121
  panelConstraintFlags.clear();
@@ -119,31 +125,92 @@ function handlePointerUp(event: ResizeEvent) {
119
125
  event.preventDefault();
120
126
  }
121
127
 
122
- recalculateIntersectingHandles({ x, y });
123
128
  updateResizeHandlerStates("up", event);
129
+ recalculateIntersectingHandles({ target, x, y });
124
130
  updateCursor();
125
131
 
126
132
  updateListeners();
127
133
  }
128
134
 
129
- function recalculateIntersectingHandles({ x, y }: { x: number; y: number }) {
135
+ function recalculateIntersectingHandles({
136
+ target,
137
+ x,
138
+ y,
139
+ }: {
140
+ target: EventTarget | null;
141
+ x: number;
142
+ y: number;
143
+ }) {
130
144
  intersectingHandles.splice(0);
131
145
 
146
+ let targetElement: HTMLElement | null = null;
147
+ if (target instanceof HTMLElement) {
148
+ targetElement = target;
149
+ }
150
+
132
151
  registeredResizeHandlers.forEach((data) => {
133
- const { element, hitAreaMargins } = data;
134
- const { bottom, left, right, top } = element.getBoundingClientRect();
152
+ const { element: dragHandleElement, hitAreaMargins } = data;
153
+
154
+ const dragHandleRect = dragHandleElement.getBoundingClientRect();
155
+ const { bottom, left, right, top } = dragHandleRect;
135
156
 
136
157
  const margin = isCoarsePointer
137
158
  ? hitAreaMargins.coarse
138
159
  : hitAreaMargins.fine;
139
160
 
140
- const intersects =
161
+ const eventIntersects =
141
162
  x >= left - margin &&
142
163
  x <= right + margin &&
143
164
  y >= top - margin &&
144
165
  y <= bottom + margin;
145
166
 
146
- if (intersects) {
167
+ if (eventIntersects) {
168
+ // TRICKY
169
+ // We listen for pointers events at the root in order to support hit area margins
170
+ // (determining when the pointer is close enough to an element to be considered a "hit")
171
+ // Clicking on an element "above" a handle (e.g. a modal) should prevent a hit though
172
+ // so at this point we need to compare stacking order of a potentially intersecting drag handle,
173
+ // and the element that was actually clicked/touched
174
+ if (
175
+ targetElement !== null &&
176
+ dragHandleElement !== targetElement &&
177
+ !dragHandleElement.contains(targetElement) &&
178
+ !targetElement.contains(dragHandleElement) &&
179
+ // Calculating stacking order has a cost, so we should avoid it if possible
180
+ // That is why we only check potentially intersecting handles,
181
+ // and why we skip if the event target is within the handle's DOM
182
+ compare(targetElement, dragHandleElement) > 0
183
+ ) {
184
+ // If the target is above the drag handle, then we also need to confirm they overlap
185
+ // If they are beside each other (e.g. a panel and its drag handle) then the handle is still interactive
186
+ //
187
+ // It's not enough to compare only the target
188
+ // The target might be a small element inside of a larger container
189
+ // (For example, a SPAN or a DIV inside of a larger modal dialog)
190
+ let currentElement: HTMLElement | null = targetElement;
191
+ let didIntersect = false;
192
+ while (currentElement) {
193
+ if (currentElement.contains(dragHandleElement)) {
194
+ break;
195
+ } else if (
196
+ intersects(
197
+ currentElement.getBoundingClientRect(),
198
+ dragHandleRect,
199
+ true
200
+ )
201
+ ) {
202
+ didIntersect = true;
203
+ break;
204
+ }
205
+
206
+ currentElement = currentElement.parentElement;
207
+ }
208
+
209
+ if (didIntersect) {
210
+ return;
211
+ }
212
+ }
213
+
147
214
  intersectingHandles.push(data);
148
215
  }
149
216
  });
@@ -1,6 +1,8 @@
1
1
  import { isBrowser } from "#is-browser";
2
- import { useLayoutEffect } from "../vendor/react";
2
+ import { useLayoutEffect_do_not_use_directly } from "../vendor/react";
3
3
 
4
- const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : () => {};
4
+ const useIsomorphicLayoutEffect = isBrowser
5
+ ? useLayoutEffect_do_not_use_directly
6
+ : () => {};
5
7
 
6
8
  export default useIsomorphicLayoutEffect;
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
9
9
  import { getResizeHandleElementIndex } from "./utils/dom/getResizeHandleElementIndex";
10
10
  import { getResizeHandleElementsForGroup } from "./utils/dom/getResizeHandleElementsForGroup";
11
11
  import { getResizeHandlePanelIds } from "./utils/dom/getResizeHandlePanelIds";
12
+ import { getIntersectingRectangle } from "./utils/rects/getIntersectingRectangle";
13
+ import { intersects } from "./utils/rects/intersects";
12
14
 
13
15
  import type {
14
16
  ImperativePanelHandle,
@@ -49,6 +51,8 @@ export {
49
51
 
50
52
  // Utility methods
51
53
  assert,
54
+ getIntersectingRectangle,
55
+ intersects,
52
56
 
53
57
  // DOM helpers
54
58
  getPanelElement,
@@ -0,0 +1,198 @@
1
+ import { getIntersectingRectangle } from "./getIntersectingRectangle";
2
+ import { Rectangle } from "./types";
3
+
4
+ const emptyRect = { x: 0, y: 0, width: 0, height: 0 };
5
+ const rect = { x: 25, y: 25, width: 50, height: 50 };
6
+
7
+ function forkRect(partial: Partial<Rectangle>, baseRect: Rectangle = rect) {
8
+ return { ...rect, ...partial };
9
+ }
10
+
11
+ describe("getIntersectingRectangle", () => {
12
+ let strict: boolean = false;
13
+
14
+ function verify(rectOne: Rectangle, rectTwo: Rectangle, expected: Rectangle) {
15
+ const actual = getIntersectingRectangle(rectOne, rectTwo, strict);
16
+
17
+ try {
18
+ expect(actual).toEqual(expected);
19
+ } catch (thrown) {
20
+ console.log(
21
+ "Expect",
22
+ strict ? "strict mode" : "loose mode",
23
+ "\n",
24
+ rectOne,
25
+ "\n",
26
+ rectTwo,
27
+ "\n\nto intersect as:\n",
28
+ expected,
29
+ "\n\nbut got:\n",
30
+ actual
31
+ );
32
+
33
+ throw thrown;
34
+ }
35
+ }
36
+
37
+ describe("loose", () => {
38
+ beforeEach(() => {
39
+ strict = false;
40
+ });
41
+
42
+ it("should support empty rects", () => {
43
+ verify(emptyRect, emptyRect, emptyRect);
44
+ });
45
+
46
+ it("should support fully overlapping rects", () => {
47
+ verify(rect, forkRect({ x: 35, width: 30 }), {
48
+ x: 35,
49
+ y: 25,
50
+ width: 30,
51
+ height: 50,
52
+ });
53
+
54
+ verify(rect, forkRect({ y: 35, height: 30 }), {
55
+ x: 25,
56
+ y: 35,
57
+ width: 50,
58
+ height: 30,
59
+ });
60
+
61
+ verify(
62
+ rect,
63
+ forkRect({
64
+ x: 35,
65
+ y: 35,
66
+ width: 30,
67
+ height: 30,
68
+ }),
69
+
70
+ {
71
+ x: 35,
72
+ y: 35,
73
+ width: 30,
74
+ height: 30,
75
+ }
76
+ );
77
+ });
78
+
79
+ it("should support partially overlapping rects", () => {
80
+ verify(rect, forkRect({ x: 10, y: 10 }), {
81
+ x: 25,
82
+ y: 25,
83
+ width: 35,
84
+ height: 35,
85
+ });
86
+
87
+ verify(rect, forkRect({ x: 45, y: 30 }), {
88
+ x: 45,
89
+ y: 30,
90
+ width: 30,
91
+ height: 45,
92
+ });
93
+ });
94
+
95
+ it("should support non-overlapping rects", () => {
96
+ verify(rect, forkRect({ x: 100, y: 100 }), emptyRect);
97
+ });
98
+
99
+ it("should support all negative coordinates", () => {
100
+ verify(
101
+ {
102
+ x: -100,
103
+ y: -100,
104
+ width: 50,
105
+ height: 50,
106
+ },
107
+ { x: -80, y: -80, width: 50, height: 50 },
108
+ {
109
+ x: -80,
110
+ y: -80,
111
+ width: 30,
112
+ height: 30,
113
+ }
114
+ );
115
+ });
116
+ });
117
+
118
+ describe("strict", () => {
119
+ beforeEach(() => {
120
+ strict = true;
121
+ });
122
+
123
+ it("should support empty rects", () => {
124
+ verify(emptyRect, emptyRect, emptyRect);
125
+ });
126
+
127
+ it("should support fully overlapping rects", () => {
128
+ verify(rect, forkRect({ x: 35, width: 30 }), {
129
+ x: 35,
130
+ y: 25,
131
+ width: 30,
132
+ height: 50,
133
+ });
134
+
135
+ verify(rect, forkRect({ y: 35, height: 30 }), {
136
+ x: 25,
137
+ y: 35,
138
+ width: 50,
139
+ height: 30,
140
+ });
141
+
142
+ verify(
143
+ rect,
144
+ forkRect({
145
+ x: 35,
146
+ y: 35,
147
+ width: 30,
148
+ height: 30,
149
+ }),
150
+
151
+ {
152
+ x: 35,
153
+ y: 35,
154
+ width: 30,
155
+ height: 30,
156
+ }
157
+ );
158
+ });
159
+
160
+ it("should support partially overlapping rects", () => {
161
+ verify(rect, forkRect({ x: 10, y: 10 }), {
162
+ x: 25,
163
+ y: 25,
164
+ width: 35,
165
+ height: 35,
166
+ });
167
+
168
+ verify(rect, forkRect({ x: 45, y: 30 }), {
169
+ x: 45,
170
+ y: 30,
171
+ width: 30,
172
+ height: 45,
173
+ });
174
+ });
175
+
176
+ it("should support non-overlapping rects", () => {
177
+ verify(rect, forkRect({ x: 100, y: 100 }), emptyRect);
178
+ });
179
+
180
+ it("should support all negative coordinates", () => {
181
+ verify(
182
+ {
183
+ x: -100,
184
+ y: -100,
185
+ width: 50,
186
+ height: 50,
187
+ },
188
+ { x: -80, y: -80, width: 50, height: 50 },
189
+ {
190
+ x: -80,
191
+ y: -80,
192
+ width: 30,
193
+ height: 30,
194
+ }
195
+ );
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,28 @@
1
+ import { intersects } from "./intersects";
2
+ import { Rectangle } from "./types";
3
+
4
+ export function getIntersectingRectangle(
5
+ rectOne: Rectangle,
6
+ rectTwo: Rectangle,
7
+ strict: boolean
8
+ ): Rectangle {
9
+ if (!intersects(rectOne, rectTwo, strict)) {
10
+ return {
11
+ x: 0,
12
+ y: 0,
13
+ width: 0,
14
+ height: 0,
15
+ };
16
+ }
17
+
18
+ return {
19
+ x: Math.max(rectOne.x, rectTwo.x),
20
+ y: Math.max(rectOne.y, rectTwo.y),
21
+ width:
22
+ Math.min(rectOne.x + rectOne.width, rectTwo.x + rectTwo.width) -
23
+ Math.max(rectOne.x, rectTwo.x),
24
+ height:
25
+ Math.min(rectOne.y + rectOne.height, rectTwo.y + rectTwo.height) -
26
+ Math.max(rectOne.y, rectTwo.y),
27
+ };
28
+ }
@@ -0,0 +1,197 @@
1
+ import { intersects } from "./intersects";
2
+ import { Rectangle } from "./types";
3
+
4
+ const emptyRect = { x: 0, y: 0, width: 0, height: 0 };
5
+ const rect = { x: 25, y: 25, width: 50, height: 50 };
6
+
7
+ function forkRect(partial: Partial<Rectangle>, baseRect: Rectangle = rect) {
8
+ return { ...rect, ...partial };
9
+ }
10
+
11
+ describe("intersects", () => {
12
+ let strict: boolean = false;
13
+
14
+ function verify(rectOne: Rectangle, rectTwo: Rectangle, expected: boolean) {
15
+ const actual = intersects(rectOne, rectTwo, strict);
16
+
17
+ try {
18
+ expect(actual).toBe(expected);
19
+ } catch (thrown) {
20
+ console.log(
21
+ "Expected",
22
+ rectOne,
23
+ "to",
24
+ expected ? "intersect" : "not intersect",
25
+ rectTwo,
26
+ strict ? "in strict mode" : "in loose mode"
27
+ );
28
+
29
+ throw thrown;
30
+ }
31
+ }
32
+
33
+ describe("loose", () => {
34
+ beforeEach(() => {
35
+ strict = false;
36
+ });
37
+
38
+ it("should handle empty rects", () => {
39
+ verify(emptyRect, emptyRect, true);
40
+ });
41
+
42
+ it("should support fully overlapping rects", () => {
43
+ verify(rect, rect, true);
44
+
45
+ verify(rect, forkRect({ x: 35, width: 30 }), true);
46
+ verify(rect, forkRect({ y: 35, height: 30 }), true);
47
+ verify(
48
+ rect,
49
+ forkRect({
50
+ x: 35,
51
+ y: 35,
52
+ width: 30,
53
+ height: 30,
54
+ }),
55
+ true
56
+ );
57
+
58
+ verify(rect, forkRect({ x: 10, width: 100 }), true);
59
+ verify(rect, forkRect({ y: 10, height: 100 }), true);
60
+ verify(
61
+ rect,
62
+ forkRect({
63
+ x: 10,
64
+ y: 10,
65
+ width: 100,
66
+ height: 100,
67
+ }),
68
+ true
69
+ );
70
+ });
71
+
72
+ it("should support partially overlapping rects", () => {
73
+ const cases: Partial<Rectangle>[] = [
74
+ { x: 0 },
75
+ { y: 0 },
76
+
77
+ // Loose mode only
78
+ { x: -25 },
79
+ { x: 75 },
80
+ { y: -25 },
81
+ { y: 75 },
82
+ { x: -25, y: -25 },
83
+ { x: 75, y: 75 },
84
+ ];
85
+
86
+ cases.forEach((partial) => {
87
+ verify(forkRect(partial), rect, true);
88
+ });
89
+ });
90
+
91
+ it("should support non-overlapping rects", () => {
92
+ const cases: Partial<Rectangle>[] = [
93
+ { x: 100 },
94
+ { x: -100 },
95
+ { y: 100 },
96
+ { y: -100 },
97
+ { x: -100, y: -100 },
98
+ { x: 100, y: 100 },
99
+ ];
100
+
101
+ cases.forEach((partial) => {
102
+ verify(forkRect(partial), rect, false);
103
+ });
104
+ });
105
+
106
+ it("should support all negative coordinates", () => {
107
+ expect(
108
+ intersects(
109
+ { x: -100, y: -100, width: 50, height: 50 },
110
+ { x: -110, y: -90, width: 50, height: 50 },
111
+ false
112
+ )
113
+ ).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe("strict", () => {
118
+ beforeEach(() => {
119
+ strict = true;
120
+ });
121
+
122
+ it("should handle empty rects", () => {
123
+ verify(emptyRect, emptyRect, false);
124
+ });
125
+
126
+ it("should support fully overlapping rects", () => {
127
+ verify(rect, rect, true);
128
+
129
+ verify(rect, forkRect({ x: 35, width: 30 }), true);
130
+ verify(rect, forkRect({ y: 35, height: 30 }), true);
131
+ verify(
132
+ rect,
133
+ forkRect({
134
+ x: 35,
135
+ y: 35,
136
+ width: 30,
137
+ height: 30,
138
+ }),
139
+ true
140
+ );
141
+
142
+ verify(rect, forkRect({ x: 10, width: 100 }), true);
143
+ verify(rect, forkRect({ y: 10, height: 100 }), true);
144
+ verify(
145
+ rect,
146
+ forkRect({
147
+ x: 10,
148
+ y: 10,
149
+ width: 100,
150
+ height: 100,
151
+ }),
152
+ true
153
+ );
154
+ });
155
+
156
+ it("should support partially overlapping rects", () => {
157
+ const cases: Partial<Rectangle>[] = [{ x: 0 }, { y: 0 }];
158
+
159
+ cases.forEach((partial) => {
160
+ verify(forkRect(partial), rect, true);
161
+ });
162
+ });
163
+
164
+ it("should support non-overlapping rects", () => {
165
+ const cases: Partial<Rectangle>[] = [
166
+ { x: 100 },
167
+ { x: -100 },
168
+ { y: 100 },
169
+ { y: -100 },
170
+ { x: -100, y: -100 },
171
+ { x: 100, y: 100 },
172
+
173
+ // Strict mode only
174
+ { x: -25 },
175
+ { x: 75 },
176
+ { y: -25 },
177
+ { y: 75 },
178
+ { x: -25, y: -25 },
179
+ { x: 75, y: 75 },
180
+ ];
181
+
182
+ cases.forEach((partial) => {
183
+ verify(forkRect(partial), rect, false);
184
+ });
185
+ });
186
+
187
+ it("should support all negative coordinates", () => {
188
+ expect(
189
+ intersects(
190
+ { x: -100, y: -100, width: 50, height: 50 },
191
+ { x: -110, y: -90, width: 50, height: 50 },
192
+ true
193
+ )
194
+ ).toBe(true);
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,23 @@
1
+ import { Rectangle } from "./types";
2
+
3
+ export function intersects(
4
+ rectOne: Rectangle,
5
+ rectTwo: Rectangle,
6
+ strict: boolean
7
+ ): boolean {
8
+ if (strict) {
9
+ return (
10
+ rectOne.x < rectTwo.x + rectTwo.width &&
11
+ rectOne.x + rectOne.width > rectTwo.x &&
12
+ rectOne.y < rectTwo.y + rectTwo.height &&
13
+ rectOne.y + rectOne.height > rectTwo.y
14
+ );
15
+ } else {
16
+ return (
17
+ rectOne.x <= rectTwo.x + rectTwo.width &&
18
+ rectOne.x + rectOne.width >= rectTwo.x &&
19
+ rectOne.y <= rectTwo.y + rectTwo.height &&
20
+ rectOne.y + rectOne.height >= rectTwo.y
21
+ );
22
+ }
23
+ }
@@ -0,0 +1,6 @@
1
+ export interface Rectangle {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
@@ -39,6 +39,8 @@ const {
39
39
  // `toString()` prevents bundlers from trying to `import { useId } from 'react'`
40
40
  const useId = (React as any)["useId".toString()] as () => string;
41
41
 
42
+ const useLayoutEffect_do_not_use_directly = useLayoutEffect;
43
+
42
44
  export {
43
45
  createElement,
44
46
  createContext,
@@ -49,7 +51,7 @@ export {
49
51
  useEffect,
50
52
  useId,
51
53
  useImperativeHandle,
52
- useLayoutEffect,
54
+ useLayoutEffect_do_not_use_directly,
53
55
  useMemo,
54
56
  useRef,
55
57
  useState,