react-resizable-panels 2.0.2 → 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.
@@ -1,8 +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";
7
+ import {
8
+ dispatchPointerEvent,
9
+ mockBoundingClientRect,
10
+ verifyAttribute,
11
+ } from "./utils/test-utils";
6
12
 
7
13
  describe("PanelResizeHandle", () => {
8
14
  let expectedWarnings: string[] = [];
@@ -66,9 +72,201 @@ describe("PanelResizeHandle", () => {
66
72
  expect(element.title).toBe("bar");
67
73
  });
68
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
+
69
120
  describe("callbacks", () => {
70
121
  describe("onDragging", () => {
71
- // TODO: Test this
122
+ it("should fire when dragging starts/stops", () => {
123
+ const onDragging = jest.fn();
124
+
125
+ const { leftElement } = setupMockedGroup({
126
+ leftProps: { onDragging },
127
+ });
128
+
129
+ act(() => {
130
+ dispatchPointerEvent("mousemove", leftElement);
131
+ });
132
+ expect(onDragging).not.toHaveBeenCalled();
133
+
134
+ act(() => {
135
+ dispatchPointerEvent("mousedown", leftElement);
136
+ });
137
+ expect(onDragging).toHaveBeenCalledTimes(1);
138
+ expect(onDragging).toHaveBeenCalledWith(true);
139
+
140
+ act(() => {
141
+ dispatchPointerEvent("mouseup", leftElement);
142
+ });
143
+ expect(onDragging).toHaveBeenCalledTimes(2);
144
+ expect(onDragging).toHaveBeenCalledWith(false);
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);
72
270
  });
73
271
  });
74
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,27 +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);
116
-
117
- switch (action) {
118
- case "down": {
119
- startDragging(resizeHandleId, event);
120
- break;
121
- }
122
- case "up": {
123
- stopDragging();
124
- break;
125
- }
126
- }
127
-
128
- switch (state) {
129
- case "drag": {
130
- resizeHandler(event);
131
- break;
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;
141
+
142
+ if (state !== "drag") {
143
+ setState("hover");
144
+ }
145
+
146
+ resizeHandler(event);
147
+ break;
148
+ }
149
+ case "up": {
150
+ setState("hover");
151
+
152
+ stopDragging();
153
+
154
+ const { onDragging } = callbacksRef.current;
155
+ if (onDragging) {
156
+ onDragging(false);
157
+ }
158
+ break;
159
+ }
132
160
  }
161
+ } else {
162
+ setState("inactive");
133
163
  }
134
164
  };
135
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
  }
@@ -68,8 +68,8 @@ export function useWindowSplitterResizeHandlerBehavior({
68
68
  ? index - 1
69
69
  : handles.length - 1
70
70
  : index + 1 < handles.length
71
- ? index + 1
72
- : 0;
71
+ ? index + 1
72
+ : 0;
73
73
 
74
74
  const nextHandle = handles[nextIndex] as HTMLElement;
75
75
  nextHandle.focus();
@@ -2,6 +2,33 @@ import { assert } from "./assert";
2
2
 
3
3
  const util = require("util");
4
4
 
5
+ export function dispatchPointerEvent(type: string, target: HTMLElement) {
6
+ const rect = target.getBoundingClientRect();
7
+
8
+ const clientX = rect.left + rect.width / 2;
9
+ const clientY = rect.top + rect.height / 2;
10
+
11
+ const event = new MouseEvent(type, {
12
+ bubbles: true,
13
+ clientX,
14
+ clientY,
15
+ });
16
+ Object.defineProperties(event, {
17
+ pageX: {
18
+ get() {
19
+ return clientX;
20
+ },
21
+ },
22
+ pageY: {
23
+ get() {
24
+ return clientY;
25
+ },
26
+ },
27
+ });
28
+
29
+ target.dispatchEvent(event);
30
+ }
31
+
5
32
  export function expectToBeCloseToArray(
6
33
  actualNumbers: number[],
7
34
  expectedNumbers: number[]
@@ -20,6 +47,36 @@ export function expectToBeCloseToArray(
20
47
  }
21
48
  }
22
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
+
23
80
  export function mockPanelGroupOffsetWidthAndHeight(
24
81
  mockWidth = 1_000,
25
82
  mockHeight = 1_000
@@ -75,6 +132,15 @@ export function mockPanelGroupOffsetWidthAndHeight(
75
132
  };
76
133
  }
77
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
+
78
144
  export function verifyExpandedPanelGroupLayout(
79
145
  actualLayout: number[],
80
146
  expectedLayout: number[]