react-resizable-panels 2.0.3 → 2.0.4

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.
@@ -13,7 +13,10 @@ import {
13
13
  } from ".";
14
14
  import { assert } from "./utils/assert";
15
15
  import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement";
16
- import { mockPanelGroupOffsetWidthAndHeight } from "./utils/test-utils";
16
+ import {
17
+ mockPanelGroupOffsetWidthAndHeight,
18
+ verifyAttribute,
19
+ } from "./utils/test-utils";
17
20
  import { createRef } from "./vendor/react";
18
21
 
19
22
  describe("PanelGroup", () => {
@@ -256,6 +259,23 @@ describe("PanelGroup", () => {
256
259
  });
257
260
  });
258
261
 
262
+ describe("data attributes", () => {
263
+ it("should initialize with the correct props based attributes", () => {
264
+ act(() => {
265
+ root.render(
266
+ <PanelGroup direction="horizontal" id="test-group"></PanelGroup>
267
+ );
268
+ });
269
+
270
+ const element = getPanelGroupElement("test-group", container);
271
+ assert(element);
272
+
273
+ verifyAttribute(element, "data-panel-group", "");
274
+ verifyAttribute(element, "data-panel-group-direction", "horizontal");
275
+ verifyAttribute(element, "data-panel-group-id", "test-group");
276
+ });
277
+ });
278
+
259
279
  describe("DEV warnings", () => {
260
280
  it("should warn about unstable layouts without id and order props", () => {
261
281
  act(() => {
@@ -1,9 +1,14 @@
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";
3
4
  import { Panel, PanelGroup, PanelResizeHandle } from ".";
4
5
  import { assert } from "./utils/assert";
5
6
  import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
6
- import { dispatchPointerEvent } from "./utils/test-utils";
7
+ import {
8
+ dispatchPointerEvent,
9
+ mockBoundingClientRect,
10
+ verifyAttribute,
11
+ } from "./utils/test-utils";
7
12
 
8
13
  describe("PanelResizeHandle", () => {
9
14
  let expectedWarnings: string[] = [];
@@ -67,47 +72,201 @@ describe("PanelResizeHandle", () => {
67
72
  expect(element.title).toBe("bar");
68
73
  });
69
74
 
75
+ function setupMockedGroup({
76
+ leftProps = {},
77
+ rightProps = {},
78
+ }: {
79
+ leftProps?: Partial<PanelResizeHandleProps>;
80
+ rightProps?: Partial<PanelResizeHandleProps>;
81
+ } = {}) {
82
+ act(() => {
83
+ root.render(
84
+ <PanelGroup direction="horizontal" id="test-group">
85
+ <Panel />
86
+ <PanelResizeHandle id="handle-left" tabIndex={1} {...leftProps} />
87
+ <Panel />
88
+ <PanelResizeHandle id="handle-right" tabIndex={2} {...rightProps} />
89
+ <Panel />
90
+ </PanelGroup>
91
+ );
92
+ });
93
+
94
+ const leftElement = getResizeHandleElement("handle-left", container);
95
+ const rightElement = getResizeHandleElement("handle-right", container);
96
+
97
+ assert(leftElement);
98
+ assert(rightElement);
99
+
100
+ // JSDom doesn't properly handle bounding rects
101
+ mockBoundingClientRect(leftElement, {
102
+ x: 50,
103
+ y: 0,
104
+ height: 50,
105
+ width: 2,
106
+ });
107
+ mockBoundingClientRect(rightElement, {
108
+ x: 100,
109
+ y: 0,
110
+ height: 50,
111
+ width: 2,
112
+ });
113
+
114
+ return {
115
+ leftElement,
116
+ rightElement,
117
+ };
118
+ }
119
+
70
120
  describe("callbacks", () => {
71
121
  describe("onDragging", () => {
72
- it("should fire when dragging starts/stops", async () => {
122
+ it("should fire when dragging starts/stops", () => {
73
123
  const onDragging = jest.fn();
74
124
 
75
- act(() => {
76
- root.render(
77
- <PanelGroup direction="horizontal">
78
- <Panel />
79
- <PanelResizeHandle
80
- id="handle"
81
- onDragging={onDragging}
82
- tabIndex={123}
83
- title="bar"
84
- />
85
- <Panel />
86
- </PanelGroup>
87
- );
125
+ const { leftElement } = setupMockedGroup({
126
+ leftProps: { onDragging },
88
127
  });
89
128
 
90
- const handleElement = container.querySelector(
91
- '[data-panel-resize-handle-id="handle"]'
92
- ) as HTMLElement;
93
-
94
129
  act(() => {
95
- dispatchPointerEvent("mouseover", handleElement);
130
+ dispatchPointerEvent("mousemove", leftElement);
96
131
  });
97
132
  expect(onDragging).not.toHaveBeenCalled();
98
133
 
99
134
  act(() => {
100
- dispatchPointerEvent("mousedown", handleElement);
135
+ dispatchPointerEvent("mousedown", leftElement);
101
136
  });
102
137
  expect(onDragging).toHaveBeenCalledTimes(1);
103
138
  expect(onDragging).toHaveBeenCalledWith(true);
104
139
 
105
140
  act(() => {
106
- dispatchPointerEvent("mouseup", handleElement);
141
+ dispatchPointerEvent("mouseup", leftElement);
107
142
  });
108
143
  expect(onDragging).toHaveBeenCalledTimes(2);
109
144
  expect(onDragging).toHaveBeenCalledWith(false);
110
145
  });
146
+
147
+ it("should only fire for the handle that has been dragged", () => {
148
+ const onDraggingLeft = jest.fn();
149
+ const onDraggingRight = jest.fn();
150
+
151
+ const { leftElement } = setupMockedGroup({
152
+ leftProps: { onDragging: onDraggingLeft },
153
+ rightProps: { onDragging: onDraggingRight },
154
+ });
155
+
156
+ act(() => {
157
+ dispatchPointerEvent("mousemove", leftElement);
158
+ });
159
+ expect(onDraggingLeft).not.toHaveBeenCalled();
160
+ expect(onDraggingRight).not.toHaveBeenCalled();
161
+
162
+ act(() => {
163
+ dispatchPointerEvent("mousedown", leftElement);
164
+ });
165
+ expect(onDraggingLeft).toHaveBeenCalledTimes(1);
166
+ expect(onDraggingLeft).toHaveBeenCalledWith(true);
167
+ expect(onDraggingRight).not.toHaveBeenCalled();
168
+
169
+ act(() => {
170
+ dispatchPointerEvent("mouseup", leftElement);
171
+ });
172
+ expect(onDraggingLeft).toHaveBeenCalledTimes(2);
173
+ expect(onDraggingLeft).toHaveBeenCalledWith(false);
174
+ expect(onDraggingRight).not.toHaveBeenCalled();
175
+ });
176
+ });
177
+ });
178
+
179
+ describe("data attributes", () => {
180
+ it("should initialize with the correct props based attributes", () => {
181
+ const { leftElement, rightElement } = setupMockedGroup();
182
+
183
+ verifyAttribute(leftElement, "data-panel-group-id", "test-group");
184
+ verifyAttribute(leftElement, "data-resize-handle", "");
185
+ verifyAttribute(leftElement, "data-panel-group-direction", "horizontal");
186
+ verifyAttribute(leftElement, "data-panel-resize-handle-enabled", "true");
187
+ verifyAttribute(
188
+ leftElement,
189
+ "data-panel-resize-handle-id",
190
+ "handle-left"
191
+ );
192
+
193
+ verifyAttribute(rightElement, "data-panel-group-id", "test-group");
194
+ verifyAttribute(rightElement, "data-resize-handle", "");
195
+ verifyAttribute(rightElement, "data-panel-group-direction", "horizontal");
196
+ verifyAttribute(rightElement, "data-panel-resize-handle-enabled", "true");
197
+ verifyAttribute(
198
+ rightElement,
199
+ "data-panel-resize-handle-id",
200
+ "handle-right"
201
+ );
202
+ });
203
+
204
+ it("should update data-resize-handle-active and data-resize-handle-state when dragging starts/stops", () => {
205
+ const { leftElement, rightElement } = setupMockedGroup();
206
+ verifyAttribute(leftElement, "data-resize-handle-active", null);
207
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
208
+ verifyAttribute(leftElement, "data-resize-handle-state", "inactive");
209
+ verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
210
+
211
+ act(() => {
212
+ dispatchPointerEvent("mousemove", leftElement);
213
+ });
214
+ verifyAttribute(leftElement, "data-resize-handle-active", null);
215
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
216
+ verifyAttribute(leftElement, "data-resize-handle-state", "hover");
217
+ verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
218
+
219
+ act(() => {
220
+ dispatchPointerEvent("mousedown", leftElement);
221
+ });
222
+ verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
223
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
224
+ verifyAttribute(leftElement, "data-resize-handle-state", "drag");
225
+ verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
226
+
227
+ act(() => {
228
+ dispatchPointerEvent("mousemove", leftElement);
229
+ });
230
+ verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
231
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
232
+ verifyAttribute(leftElement, "data-resize-handle-state", "drag");
233
+ verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
234
+
235
+ act(() => {
236
+ dispatchPointerEvent("mouseup", leftElement);
237
+ });
238
+ verifyAttribute(leftElement, "data-resize-handle-active", null);
239
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
240
+ verifyAttribute(leftElement, "data-resize-handle-state", "hover");
241
+ verifyAttribute(rightElement, "data-resize-handle-state", "inactive");
242
+
243
+ act(() => {
244
+ dispatchPointerEvent("mousemove", rightElement);
245
+ });
246
+ verifyAttribute(leftElement, "data-resize-handle-active", null);
247
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
248
+ verifyAttribute(leftElement, "data-resize-handle-state", "inactive");
249
+ verifyAttribute(rightElement, "data-resize-handle-state", "hover");
250
+ });
251
+
252
+ it("should update data-resize-handle-active when focused", () => {
253
+ const { leftElement, rightElement } = setupMockedGroup();
254
+ verifyAttribute(leftElement, "data-resize-handle-active", null);
255
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
256
+
257
+ act(() => {
258
+ leftElement.focus();
259
+ });
260
+ expect(document.activeElement).toBe(leftElement);
261
+ verifyAttribute(leftElement, "data-resize-handle-active", "keyboard");
262
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
263
+
264
+ act(() => {
265
+ leftElement.blur();
266
+ });
267
+ expect(document.activeElement).not.toBe(leftElement);
268
+ verifyAttribute(leftElement, "data-resize-handle-active", null);
269
+ verifyAttribute(rightElement, "data-resize-handle-active", null);
111
270
  });
112
271
  });
113
272
  });
@@ -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
 
@@ -4,10 +4,9 @@ import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordina
4
4
  import { getInputType } from "./utils/getInputType";
5
5
 
6
6
  export type ResizeHandlerAction = "down" | "move" | "up";
7
- export type ResizeHandlerState = "drag" | "hover" | "inactive";
8
7
  export type SetResizeHandlerState = (
9
8
  action: ResizeHandlerAction,
10
- state: ResizeHandlerState,
9
+ isActive: boolean,
11
10
  event: ResizeEvent
12
11
  ) => void;
13
12
 
@@ -93,21 +92,18 @@ function handlePointerDown(event: ResizeEvent) {
93
92
  function handlePointerMove(event: ResizeEvent) {
94
93
  const { x, y } = getResizeEventCoordinates(event);
95
94
 
96
- if (isPointerDown) {
97
- intersectingHandles.forEach((data) => {
98
- const { setResizeHandlerState } = data;
99
-
100
- setResizeHandlerState("move", "drag", event);
101
- });
102
-
103
- // Update cursor based on return value(s) from active handles
104
- updateCursor();
105
- } else {
95
+ if (!isPointerDown) {
96
+ // Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed
97
+ // at that point, the handles may not move with the pointer (depending on constraints)
98
+ // but the same set of active handles should be locked until the pointer is released
106
99
  recalculateIntersectingHandles({ x, y });
107
- updateResizeHandlerStates("move", event);
108
- updateCursor();
109
100
  }
110
101
 
102
+ updateResizeHandlerStates("move", event);
103
+
104
+ // Update cursor based on return value(s) from active handles
105
+ updateCursor();
106
+
111
107
  if (intersectingHandles.length > 0) {
112
108
  event.preventDefault();
113
109
  }
@@ -250,14 +246,8 @@ function updateResizeHandlerStates(
250
246
  registeredResizeHandlers.forEach((data) => {
251
247
  const { setResizeHandlerState } = data;
252
248
 
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
- }
249
+ const isActive = intersectingHandles.includes(data);
250
+
251
+ setResizeHandlerState(action, isActive, event);
262
252
  });
263
253
  }
@@ -47,6 +47,36 @@ export function expectToBeCloseToArray(
47
47
  }
48
48
  }
49
49
 
50
+ export function mockBoundingClientRect(
51
+ element: HTMLElement,
52
+ rect: {
53
+ height: number;
54
+ width: number;
55
+ x: number;
56
+ y: number;
57
+ }
58
+ ) {
59
+ const { height, width, x, y } = rect;
60
+
61
+ Object.defineProperty(element, "getBoundingClientRect", {
62
+ configurable: true,
63
+ value: () =>
64
+ ({
65
+ bottom: y + height,
66
+ height,
67
+ left: x,
68
+ right: x + width,
69
+ toJSON() {
70
+ return "";
71
+ },
72
+ top: y,
73
+ width,
74
+ x,
75
+ y,
76
+ }) satisfies DOMRect,
77
+ });
78
+ }
79
+
50
80
  export function mockPanelGroupOffsetWidthAndHeight(
51
81
  mockWidth = 1_000,
52
82
  mockHeight = 1_000
@@ -102,6 +132,15 @@ export function mockPanelGroupOffsetWidthAndHeight(
102
132
  };
103
133
  }
104
134
 
135
+ export function verifyAttribute(
136
+ element: HTMLElement,
137
+ attributeName: string,
138
+ expectedValue: string | null
139
+ ) {
140
+ const actualValue = element.getAttribute(attributeName);
141
+ expect(actualValue).toBe(expectedValue);
142
+ }
143
+
105
144
  export function verifyExpandedPanelGroupLayout(
106
145
  actualLayout: number[],
107
146
  expectedLayout: number[]