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
@@ -7,6 +7,7 @@ import {
7
7
  ReactElement,
8
8
  useContext,
9
9
  useEffect,
10
+ useLayoutEffect,
10
11
  useRef,
11
12
  useState,
12
13
  } from "./vendor/react";
@@ -21,11 +22,11 @@ import {
21
22
  PointerHitAreaMargins,
22
23
  registerResizeHandle,
23
24
  ResizeHandlerAction,
24
- ResizeHandlerState,
25
25
  } from "./PanelResizeHandleRegistry";
26
26
  import { assert } from "./utils/assert";
27
27
 
28
28
  export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
29
+ export type ResizeHandlerState = "drag" | "hover" | "inactive";
29
30
 
30
31
  export type PanelResizeHandleProps = Omit<
31
32
  HTMLAttributes<keyof HTMLElementTagNameMap>,
@@ -90,6 +91,16 @@ export function PanelResizeHandle({
90
91
  null
91
92
  );
92
93
 
94
+ const committedValuesRef = useRef<{
95
+ state: ResizeHandlerState;
96
+ }>({
97
+ state,
98
+ });
99
+
100
+ useLayoutEffect(() => {
101
+ committedValuesRef.current.state = state;
102
+ });
103
+
93
104
  useEffect(() => {
94
105
  if (disabled) {
95
106
  setResizeHandler(null);
@@ -109,37 +120,46 @@ export function PanelResizeHandle({
109
120
 
110
121
  const setResizeHandlerState = (
111
122
  action: ResizeHandlerAction,
112
- state: ResizeHandlerState,
123
+ isActive: boolean,
113
124
  event: ResizeEvent
114
125
  ) => {
115
- setState(state);
126
+ if (isActive) {
127
+ switch (action) {
128
+ case "down": {
129
+ setState("drag");
130
+
131
+ startDragging(resizeHandleId, event);
132
+
133
+ const { onDragging } = callbacksRef.current;
134
+ if (onDragging) {
135
+ onDragging(true);
136
+ }
137
+ break;
138
+ }
139
+ case "move": {
140
+ const { state } = committedValuesRef.current;
116
141
 
117
- switch (action) {
118
- case "down": {
119
- startDragging(resizeHandleId, event);
142
+ if (state !== "drag") {
143
+ setState("hover");
144
+ }
120
145
 
121
- const { onDragging } = callbacksRef.current;
122
- if (onDragging) {
123
- onDragging(true);
146
+ resizeHandler(event);
147
+ break;
124
148
  }
125
- break;
126
- }
127
- case "up": {
128
- stopDragging();
149
+ case "up": {
150
+ setState("hover");
129
151
 
130
- const { onDragging } = callbacksRef.current;
131
- if (onDragging) {
132
- onDragging(false);
133
- }
134
- break;
135
- }
136
- }
152
+ stopDragging();
137
153
 
138
- switch (state) {
139
- case "drag": {
140
- resizeHandler(event);
141
- break;
154
+ const { onDragging } = callbacksRef.current;
155
+ if (onDragging) {
156
+ onDragging(false);
157
+ }
158
+ break;
159
+ }
142
160
  }
161
+ } else {
162
+ setState("inactive");
143
163
  }
144
164
  };
145
165
 
@@ -1,13 +1,14 @@
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
- export type ResizeHandlerState = "drag" | "hover" | "inactive";
8
9
  export type SetResizeHandlerState = (
9
10
  action: ResizeHandlerAction,
10
- state: ResizeHandlerState,
11
+ isActive: boolean,
11
12
  event: ResizeEvent
12
13
  ) => void;
13
14
 
@@ -76,11 +77,12 @@ export function registerResizeHandle(
76
77
  }
77
78
 
78
79
  function handlePointerDown(event: ResizeEvent) {
80
+ const { target } = event;
79
81
  const { x, y } = getResizeEventCoordinates(event);
80
82
 
81
83
  isPointerDown = true;
82
84
 
83
- recalculateIntersectingHandles({ x, y });
85
+ recalculateIntersectingHandles({ target, x, y });
84
86
  updateListeners();
85
87
 
86
88
  if (intersectingHandles.length > 0) {
@@ -93,27 +95,27 @@ function handlePointerDown(event: ResizeEvent) {
93
95
  function handlePointerMove(event: ResizeEvent) {
94
96
  const { x, y } = getResizeEventCoordinates(event);
95
97
 
96
- if (isPointerDown) {
97
- intersectingHandles.forEach((data) => {
98
- const { setResizeHandlerState } = data;
98
+ if (!isPointerDown) {
99
+ const { target } = event;
99
100
 
100
- setResizeHandlerState("move", "drag", event);
101
- });
102
-
103
- // Update cursor based on return value(s) from active handles
104
- updateCursor();
105
- } else {
106
- recalculateIntersectingHandles({ x, y });
107
- updateResizeHandlerStates("move", event);
108
- updateCursor();
101
+ // Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed
102
+ // at that point, the handles may not move with the pointer (depending on constraints)
103
+ // but the same set of active handles should be locked until the pointer is released
104
+ recalculateIntersectingHandles({ target, x, y });
109
105
  }
110
106
 
107
+ updateResizeHandlerStates("move", event);
108
+
109
+ // Update cursor based on return value(s) from active handles
110
+ updateCursor();
111
+
111
112
  if (intersectingHandles.length > 0) {
112
113
  event.preventDefault();
113
114
  }
114
115
  }
115
116
 
116
117
  function handlePointerUp(event: ResizeEvent) {
118
+ const { target } = event;
117
119
  const { x, y } = getResizeEventCoordinates(event);
118
120
 
119
121
  panelConstraintFlags.clear();
@@ -123,31 +125,92 @@ function handlePointerUp(event: ResizeEvent) {
123
125
  event.preventDefault();
124
126
  }
125
127
 
126
- recalculateIntersectingHandles({ x, y });
127
128
  updateResizeHandlerStates("up", event);
129
+ recalculateIntersectingHandles({ target, x, y });
128
130
  updateCursor();
129
131
 
130
132
  updateListeners();
131
133
  }
132
134
 
133
- 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
+ }) {
134
144
  intersectingHandles.splice(0);
135
145
 
146
+ let targetElement: HTMLElement | null = null;
147
+ if (target instanceof HTMLElement) {
148
+ targetElement = target;
149
+ }
150
+
136
151
  registeredResizeHandlers.forEach((data) => {
137
- const { element, hitAreaMargins } = data;
138
- 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;
139
156
 
140
157
  const margin = isCoarsePointer
141
158
  ? hitAreaMargins.coarse
142
159
  : hitAreaMargins.fine;
143
160
 
144
- const intersects =
161
+ const eventIntersects =
145
162
  x >= left - margin &&
146
163
  x <= right + margin &&
147
164
  y >= top - margin &&
148
165
  y <= bottom + margin;
149
166
 
150
- 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
+
151
214
  intersectingHandles.push(data);
152
215
  }
153
216
  });
@@ -250,14 +313,8 @@ function updateResizeHandlerStates(
250
313
  registeredResizeHandlers.forEach((data) => {
251
314
  const { setResizeHandlerState } = data;
252
315
 
253
- if (intersectingHandles.includes(data)) {
254
- if (isPointerDown) {
255
- setResizeHandlerState(action, "drag", event);
256
- } else {
257
- setResizeHandlerState(action, "hover", event);
258
- }
259
- } else {
260
- setResizeHandlerState(action, "inactive", event);
261
- }
316
+ const isActive = intersectingHandles.includes(data);
317
+
318
+ setResizeHandlerState(action, isActive, event);
262
319
  });
263
320
  }
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
+ }