Skip to content

Commit 9b69a44

Browse files
authored
Support custom cursors (#503) (#504)
1 parent 4ec29cf commit 9b69a44

File tree

8 files changed

+213
-38
lines changed

8 files changed

+213
-38
lines changed

packages/react-resizable-panels-website/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
import { StrictMode, useEffect } from "react";
1+
import { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
33
import { createBrowserRouter, RouterProvider } from "react-router-dom";
44

5+
import EndToEndTestingRoute from "./src/routes/EndToEndTesting";
56
import HomeRoute from "./src/routes/Home";
7+
import CollapsibleExampleRoute from "./src/routes/examples/Collapsible";
68
import ConditionalExampleRoute from "./src/routes/examples/Conditional";
9+
import CustomCursorExampleRoute from "./src/routes/examples/CustomCursorExampleRoute";
710
import ExternalPersistenceExampleRoute from "./src/routes/examples/ExternalPersistence";
811
import HorizontalExampleRoute from "./src/routes/examples/Horizontal";
912
import ImperativePanelApiExampleRoute from "./src/routes/examples/ImperativePanelApi";
1013
import ImperativePanelGroupApiExampleRoute from "./src/routes/examples/ImperativePanelGroupApi";
1114
import NestedExampleRoute from "./src/routes/examples/Nested";
1215
import OverflowExampleRoute from "./src/routes/examples/Overflow";
1316
import PersistenceExampleRoute from "./src/routes/examples/Persistence";
14-
import CollapsibleExampleRoute from "./src/routes/examples/Collapsible";
1517
import VerticalExampleRoute from "./src/routes/examples/Vertical";
16-
import EndToEndTestingRoute from "./src/routes/EndToEndTesting";
1718
import IframeRoute from "./src/routes/iframe";
1819

1920
const router = createBrowserRouter([
@@ -25,6 +26,10 @@ const router = createBrowserRouter([
2526
path: "/examples/conditional",
2627
element: <ConditionalExampleRoute />,
2728
},
29+
{
30+
path: "/examples/custom-cursor",
31+
element: <CustomCursorExampleRoute />,
32+
},
2833
{
2934
path: "/examples/external-persistence",
3035
element: <ExternalPersistenceExampleRoute />,

packages/react-resizable-panels-website/src/routes/Home/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const LINKS = [
1414
{ path: "collapsible", title: "Collapsible panels" },
1515
{ path: "conditional", title: "Conditional panels" },
1616
{ path: "external-persistence", title: "External persistence" },
17+
{ path: "custom-cursor", title: "Custom cursor" },
1718
{ path: "imperative-panel-api", title: "Imperative Panel API" },
1819
{ path: "imperative-panel-group-api", title: "Imperative PanelGroup API" },
1920
];
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
CustomCursorStyleConfig,
3+
customizeGlobalCursorStyles,
4+
Panel,
5+
PanelGroup,
6+
} from "react-resizable-panels";
7+
8+
import { ResizeHandle } from "../../components/ResizeHandle";
9+
10+
import { useLayoutEffect } from "react";
11+
import Example from "./Example";
12+
import styles from "./shared.module.css";
13+
14+
export default function CustomCursorExampleRoute() {
15+
return (
16+
<Example
17+
code={CODE}
18+
exampleNode={<Content />}
19+
headerNode={
20+
<>
21+
<p>
22+
By default, this library manages cursor styles in response to user
23+
input. You <em>can</em> override the default cursor for advanced use
24+
cases, though it's generally not recommended.
25+
</p>
26+
</>
27+
}
28+
title="Custom cursor"
29+
/>
30+
);
31+
}
32+
33+
function Content() {
34+
useLayoutEffect(() => {
35+
function customCursor({ isPointerDown }: CustomCursorStyleConfig) {
36+
return isPointerDown ? "grabbing" : "grab";
37+
}
38+
39+
customizeGlobalCursorStyles(customCursor);
40+
return () => {
41+
customizeGlobalCursorStyles(null);
42+
};
43+
}, []);
44+
45+
return (
46+
<div className={styles.PanelGroupWrapper}>
47+
<PanelGroup className={styles.PanelGroup} direction="horizontal">
48+
<Panel className={styles.PanelRow} minSize={10}>
49+
<div className={styles.Centered}>left</div>
50+
</Panel>
51+
<ResizeHandle className={styles.ResizeHandle} />
52+
<Panel className={styles.PanelRow} minSize={10}>
53+
<div className={styles.Centered}>right</div>
54+
</Panel>
55+
</PanelGroup>
56+
</div>
57+
);
58+
}
59+
60+
const CODE = `
61+
import { customizeGlobalCursorStyles, type CustomCursorStyleConfig } from "react-resizable-panels";
62+
63+
function Example() {
64+
useLayoutEffect(() => {
65+
function customCursor({ isPointerDown }: CustomCursorStyleConfig) {
66+
return isPointerDown ? "grabbing" : "grab";
67+
}
68+
69+
customizeGlobalCursorStyles(customCursor);
70+
return () => {
71+
customizeGlobalCursorStyles(null);
72+
};
73+
}, []);
74+
75+
// ...
76+
}
77+
`;

packages/react-resizable-panels-website/tests/CursorStyle.spec.ts

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,37 +60,53 @@ test.describe("cursor style", () => {
6060
await dragResizeTo(
6161
page,
6262
"first-panel",
63-
{ size: 15, expectedCursor: getCursorStyle("horizontal", 0) },
63+
{ size: 15, expectedCursor: getCursorStyle("horizontal", 0, false) },
6464
{
6565
size: 5,
66-
expectedCursor: getCursorStyle("horizontal", EXCEEDED_HORIZONTAL_MIN),
66+
expectedCursor: getCursorStyle(
67+
"horizontal",
68+
EXCEEDED_HORIZONTAL_MIN,
69+
false
70+
),
6771
},
68-
{ size: 15, expectedCursor: getCursorStyle("horizontal", 0) },
69-
{ size: 85, expectedCursor: getCursorStyle("horizontal", 0) },
72+
{ size: 15, expectedCursor: getCursorStyle("horizontal", 0, false) },
73+
{ size: 85, expectedCursor: getCursorStyle("horizontal", 0, false) },
7074
{
7175
size: 95,
72-
expectedCursor: getCursorStyle("horizontal", EXCEEDED_HORIZONTAL_MAX),
76+
expectedCursor: getCursorStyle(
77+
"horizontal",
78+
EXCEEDED_HORIZONTAL_MAX,
79+
false
80+
),
7381
},
74-
{ size: 85, expectedCursor: getCursorStyle("horizontal", 0) }
82+
{ size: 85, expectedCursor: getCursorStyle("horizontal", 0, false) }
7583
);
7684

7785
await openPage(page, "vertical");
7886

7987
await dragResizeTo(
8088
page,
8189
"first-panel",
82-
{ size: 15, expectedCursor: getCursorStyle("vertical", 0) },
90+
{ size: 15, expectedCursor: getCursorStyle("vertical", 0, false) },
8391
{
8492
size: 5,
85-
expectedCursor: getCursorStyle("vertical", EXCEEDED_VERTICAL_MIN),
93+
expectedCursor: getCursorStyle(
94+
"vertical",
95+
EXCEEDED_VERTICAL_MIN,
96+
false
97+
),
8698
},
87-
{ size: 15, expectedCursor: getCursorStyle("vertical", 0) },
88-
{ size: 85, expectedCursor: getCursorStyle("vertical", 0) },
99+
{ size: 15, expectedCursor: getCursorStyle("vertical", 0, false) },
100+
{ size: 85, expectedCursor: getCursorStyle("vertical", 0, false) },
89101
{
90102
size: 95,
91-
expectedCursor: getCursorStyle("vertical", EXCEEDED_VERTICAL_MAX),
103+
expectedCursor: getCursorStyle(
104+
"vertical",
105+
EXCEEDED_VERTICAL_MAX,
106+
false
107+
),
92108
},
93-
{ size: 85, expectedCursor: getCursorStyle("vertical", 0) }
109+
{ size: 85, expectedCursor: getCursorStyle("vertical", 0, false) }
94110
);
95111
});
96112

@@ -143,59 +159,79 @@ test.describe("cursor style", () => {
143159
"outer-group",
144160
["horizontal-handle", "vertical-handle"],
145161
{
146-
expectedCursor: getCursorStyle("intersection", 0),
162+
expectedCursor: getCursorStyle("intersection", 0, false),
147163
sizeX: 0.4,
148164
sizeY: 0.4,
149165
},
150166
{
151-
expectedCursor: getCursorStyle("intersection", 0),
167+
expectedCursor: getCursorStyle("intersection", 0, false),
152168
sizeX: 0.6,
153169
sizeY: 0.6,
154170
},
155171
{
156-
expectedCursor: getCursorStyle("intersection", EXCEEDED_HORIZONTAL_MIN),
172+
expectedCursor: getCursorStyle(
173+
"intersection",
174+
EXCEEDED_HORIZONTAL_MIN,
175+
false
176+
),
157177
sizeX: 0.1,
158178
},
159179
{
160-
expectedCursor: getCursorStyle("intersection", EXCEEDED_HORIZONTAL_MAX),
180+
expectedCursor: getCursorStyle(
181+
"intersection",
182+
EXCEEDED_HORIZONTAL_MAX,
183+
false
184+
),
161185
sizeX: 0.9,
162186
},
163187
{
164-
expectedCursor: getCursorStyle("intersection", EXCEEDED_VERTICAL_MIN),
188+
expectedCursor: getCursorStyle(
189+
"intersection",
190+
EXCEEDED_VERTICAL_MIN,
191+
false
192+
),
165193
sizeY: 0.1,
166194
},
167195
{
168-
expectedCursor: getCursorStyle("intersection", EXCEEDED_VERTICAL_MAX),
196+
expectedCursor: getCursorStyle(
197+
"intersection",
198+
EXCEEDED_VERTICAL_MAX,
199+
false
200+
),
169201
sizeY: 0.9,
170202
},
171203
{
172204
expectedCursor: getCursorStyle(
173205
"intersection",
174-
EXCEEDED_HORIZONTAL_MIN | EXCEEDED_VERTICAL_MIN
206+
EXCEEDED_HORIZONTAL_MIN | EXCEEDED_VERTICAL_MIN,
207+
false
175208
),
176209
sizeX: 0.1,
177210
sizeY: 0.1,
178211
},
179212
{
180213
expectedCursor: getCursorStyle(
181214
"intersection",
182-
EXCEEDED_HORIZONTAL_MIN | EXCEEDED_VERTICAL_MAX
215+
EXCEEDED_HORIZONTAL_MIN | EXCEEDED_VERTICAL_MAX,
216+
false
183217
),
184218
sizeX: 0.1,
185219
sizeY: 0.9,
186220
},
187221
{
188222
expectedCursor: getCursorStyle(
189223
"intersection",
190-
EXCEEDED_HORIZONTAL_MAX | EXCEEDED_VERTICAL_MIN
224+
EXCEEDED_HORIZONTAL_MAX | EXCEEDED_VERTICAL_MIN,
225+
false
191226
),
192227
sizeX: 0.9,
193228
sizeY: 0.1,
194229
},
195230
{
196231
expectedCursor: getCursorStyle(
197232
"intersection",
198-
EXCEEDED_HORIZONTAL_MAX | EXCEEDED_VERTICAL_MAX
233+
EXCEEDED_HORIZONTAL_MAX | EXCEEDED_VERTICAL_MAX,
234+
false
199235
),
200236
sizeX: 0.9,
201237
sizeY: 0.9,

packages/react-resizable-panels/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,15 @@ import { disableGlobalCursorStyles } from "react-resizable-panels";
247247

248248
disableGlobalCursorStyles();
249249
```
250+
251+
#### How can I override the global cursor styles?
252+
253+
```js
254+
import { customizeGlobalCursorStyles, type CustomCursorStyleConfig } from "react-resizable-panels";
255+
256+
function customCursor({ isPointerDown }: CustomCursorStyleConfig) {
257+
return isPointerDown ? "grabbing" : "grab";
258+
}
259+
260+
customizeGlobalCursorStyles(customCursor);
261+
```

packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ function handlePointerDown(event: PointerEvent) {
104104
if (intersectingHandles.length > 0) {
105105
updateResizeHandlerStates("down", event);
106106

107+
// Update cursor based on return value(s) from active handles
108+
updateCursor();
109+
107110
event.preventDefault();
108111

109112
if (!isWithinResizeHandle(target as HTMLElement)) {
@@ -290,11 +293,11 @@ function updateCursor() {
290293
});
291294

292295
if (intersectsHorizontal && intersectsVertical) {
293-
setGlobalCursorStyle("intersection", constraintFlags);
296+
setGlobalCursorStyle("intersection", constraintFlags, isPointerDown);
294297
} else if (intersectsHorizontal) {
295-
setGlobalCursorStyle("horizontal", constraintFlags);
298+
setGlobalCursorStyle("horizontal", constraintFlags, isPointerDown);
296299
} else if (intersectsVertical) {
297-
setGlobalCursorStyle("vertical", constraintFlags);
300+
setGlobalCursorStyle("vertical", constraintFlags, isPointerDown);
298301
} else {
299302
resetGlobalCursorStyle();
300303
}

packages/react-resizable-panels/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { usePanelGroupContext } from "./hooks/usePanelGroupContext";
66
import { assert } from "./utils/assert";
77
import { setNonce } from "./utils/csp";
88
import {
9+
customizeGlobalCursorStyles,
910
disableGlobalCursorStyles,
1011
enableGlobalCursorStyles,
1112
} from "./utils/cursor";
@@ -37,6 +38,7 @@ import type {
3738
PanelResizeHandleProps,
3839
} from "./PanelResizeHandle";
3940
import type { PointerHitAreaMargins } from "./PanelResizeHandleRegistry";
41+
import type { CustomCursorStyleConfig } from "./utils/cursor";
4042

4143
export {
4244
// TypeScript types
@@ -76,10 +78,14 @@ export {
7678
getResizeHandlePanelIds,
7779

7880
// Styles and CSP (see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)
79-
enableGlobalCursorStyles,
80-
disableGlobalCursorStyles,
8181
setNonce,
8282

83+
// Global cursor configuration
84+
customizeGlobalCursorStyles,
85+
disableGlobalCursorStyles,
86+
enableGlobalCursorStyles,
87+
CustomCursorStyleConfig,
88+
8389
// Data attributes (primarily intended for e2e testing)
8490
DATA_ATTRIBUTES,
8591
};

0 commit comments

Comments
 (0)