Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 4.4.2

- [610](https://github.com/bvaughn/react-resizable-panels/pull/610): Fix calculated cursor style when `"pointermove"` event is has low-precision/rounded `clientX` and `clientY` values

## 4.4.1

- [600](https://github.com/bvaughn/react-resizable-panels/pull/600): Bugfix: Collapsible `Panel` should treat `defaultSize={0}` as _collapsed_ on mount
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ pnpm test
To run end to end tests locally:
```sh
pnpm prerelease
pnpm e2e:install
pnpm dev:integrations & pnpm e2e:test
```

### Updating assets

Before subtmitting, also make sure to update generated docs/examples:
Before submitting, also make sure to update generated docs/examples:
```
pnpm compile
pnpm prettier
Expand Down
34 changes: 19 additions & 15 deletions integrations/tests/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
projects: [
{
name: "chromium",
timeout: 10_000,
use: {
...devices["Desktop Chrome"],
viewport: { width: 1000, height: 600 }
const DEVICES = [
{
name: "chromium",
use: devices["Desktop Chrome"]
}
];

// Uncomment to visually debug
// headless: false,
// launchOptions: {
// slowMo: 500
// }
}
export default defineConfig({
projects: DEVICES.map(({ name, use }) => ({
name,
timeout: 10_000,
use: {
...use,
viewport: { width: 1000, height: 600 }
// Uncomment to visually debug
// headless: false,
// launchOptions: {
// slowMo: 250
// }
}
]
}))
});
27 changes: 27 additions & 0 deletions integrations/tests/src/components/Decoder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export function Decoder({
? panelRef
: undefined;

const [cursorData, setCursorData] = useState<{
cursorStyle: string;
movementX: number;
movementY: number;
}>({
cursorStyle: "",
movementX: 0,
movementY: 0
});

const stableCallbacksRef = useRef<{
readGroupLayout: () => void;
readPanelSize: () => void;
Expand Down Expand Up @@ -135,6 +145,22 @@ export function Decoder({
return group;
}, [encoded, groupRefProp, panelRefProp]);

useLayoutEffect(() => {
const onPointerMove = (event: PointerEvent) => {
setCursorData({
cursorStyle: window.getComputedStyle(document.body).cursor,
movementX: event.movementX,
movementY: event.movementY
});
};

window.addEventListener("pointermove", onPointerMove);

return () => {
window.removeEventListener("pointermove", onPointerMove);
};
}, []);

// Debugging
// console.group("Decoder");
// console.log(encoded);
Expand All @@ -145,6 +171,7 @@ export function Decoder({
<Box direction="column" gap={2}>
<div>{children}</div>
<Box className="p-2 overflow-auto" direction="row" gap={2} wrap>
{cursorData && <DebugData data={cursorData} />}
{groupRefProp && (
<DebugData
data={{
Expand Down
92 changes: 63 additions & 29 deletions integrations/tests/tests/cursor.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { calculateHitArea } from "../src/utils/calculateHitArea";
import { getCenterCoordinates } from "../src/utils/getCenterCoordinates";
import { goToUrl } from "../src/utils/goToUrl";

// The cursor boundary check logic relies on the layout not changing between two movements in order to detect "you've moved too far, there's no more resizing that can be done"
// This type of test isn't totally realistic; only one mouse-move between big pixel gaps is unlikely in all but the most extreme perf bottlenecks.
// To mimic something closer to a real world scenario, we need to split move events into multiple pointer move events.
const moveConfig = { steps: 10 };

test.describe("cursor", () => {
for (const usePopUpWindow of [true, false]) {
test.describe(usePopUpWindow ? "in a popup" : "in the main window", () => {
Expand All @@ -25,22 +30,20 @@ test.describe("cursor", () => {
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");

await page.mouse.move(x, y);
await page.mouse.move(x, y, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("ew-resize");

await page.mouse.down();
await page.mouse.move(50, y);
await page.mouse.move(25, y);
await page.mouse.move(25, y, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("e-resize");

await page.mouse.move(950, y);
await page.mouse.move(975, y);
await page.mouse.move(975, y, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
Expand All @@ -65,22 +68,20 @@ test.describe("cursor", () => {
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");

await page.mouse.move(x, y);
await page.mouse.move(x, y, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("ns-resize");

await page.mouse.down();
await page.mouse.move(x, 1);
await page.mouse.move(x, 0);
await page.mouse.move(x, 0, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("s-resize");

await page.mouse.move(x, 599);
await page.mouse.move(x, 600);
await page.mouse.move(x, 600, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
Expand Down Expand Up @@ -114,70 +115,103 @@ test.describe("cursor", () => {
).toBe("auto");

// Centered
await page.mouse.move(x, y);
await page.mouse.move(x, y, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("move");

// Top left
await page.mouse.down();
await page.mouse.move(2, 1);
await page.mouse.move(1, 1);
await page.mouse.move(1, 1, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("se-resize");

// Top
await page.mouse.move(x, 1);
await page.mouse.move(x, 0);
await page.mouse.move(x, 0, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("s-resize");

// Top right
await page.mouse.move(999, 1);
await page.mouse.move(1000, 1);
await page.mouse.move(1000, 1, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("sw-resize");

// Right
await page.mouse.move(950, y);
await page.mouse.move(975, y);
await page.mouse.move(975, y, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("w-resize");

// Bottom right
await page.mouse.move(1000, 599);
await page.mouse.move(1000, 600);
await page.mouse.move(1000, 600, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("nw-resize");

// Bottom
await page.mouse.move(x, 599);
await page.mouse.move(x, 600);
await page.mouse.move(x, 600, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("n-resize");

// Bottom left
await page.mouse.move(1, 599);
await page.mouse.move(1, 600);
await page.mouse.move(1, 600, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("ne-resize");

// Left
await page.mouse.move(50, y);
await page.mouse.move(25, y);
await page.mouse.move(25, y, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("e-resize");

// Centered
await page.mouse.move(x, y);
await page.mouse.move(x, y, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("move");
});

test("edge case", async ({ page: mainPage }) => {
const page = await goToUrl(
mainPage,
<Group className="h-[250px]!" orientation="vertical">
<Panel id="top" minSize="25%" />
<Separator id="vertical-separator" />
<Panel id="bottom" minSize="25%">
<Group orientation="horizontal">
<Panel id="left" minSize="25%" />
<Separator />
<Panel id="right" minSize="25%" />
</Group>
</Panel>
</Group>,
{ usePopUpWindow }
);

const separator = page.getByTestId("vertical-separator");
const boundingBox = (await separator.boundingBox())!;
const x = boundingBox.x + boundingBox.width / 2;
const y = boundingBox.y;

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");

// Centered
await page.mouse.move(x, y, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("move");

await page.mouse.down();

// Moving only in one dimension should not affect the cursor
await page.mouse.move(x, y - 25, moveConfig);
expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("move");
Expand All @@ -201,7 +235,7 @@ test.describe("cursor", () => {
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");

await page.mouse.move(x, y);
await page.mouse.move(x, y, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const CURSOR_FLAG_HORIZONTAL_MIN = 0b0001;
export const CURSOR_FLAG_HORIZONTAL_MAX = 0b0010;
export const CURSOR_FLAG_VERTICAL_MIN = 0b0100;
export const CURSOR_FLAG_VERTICAL_MAX = 0b1000;
export const CURSOR_FLAGS_HORIZONTAL = 0b0011;
export const CURSOR_FLAGS_VERTICAL = 0b1100;

// Misc. shared values
export const DEFAULT_POINTER_PRECISION = {
Expand Down
5 changes: 3 additions & 2 deletions lib/global/event-handlers/onDocumentPointerLeave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { read } from "../mutableState";
import { updateActiveHitRegions } from "../utils/updateActiveHitRegion";

export function onDocumentPointerLeave(event: PointerEvent) {
const { interactionState, mountedGroups } = read();
const { cursorFlags, interactionState, mountedGroups } = read();

switch (interactionState.state) {
case "active": {
Expand All @@ -11,7 +11,8 @@ export function onDocumentPointerLeave(event: PointerEvent) {
event,
hitRegions: interactionState.hitRegions,
initialLayoutMap: interactionState.initialLayoutMap,
mountedGroups
mountedGroups,
prevCursorFlags: cursorFlags
});
}
}
Expand Down
5 changes: 3 additions & 2 deletions lib/global/event-handlers/onDocumentPointerMove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function onDocumentPointerMove(event: PointerEvent) {
return;
}

const { interactionState, mountedGroups } = read();
const { cursorFlags, interactionState, mountedGroups } = read();

switch (interactionState.state) {
case "active": {
Expand Down Expand Up @@ -42,7 +42,8 @@ export function onDocumentPointerMove(event: PointerEvent) {
hitRegions: interactionState.hitRegions,
initialLayoutMap: interactionState.initialLayoutMap,
mountedGroups,
pointerDownAtPoint: interactionState.pointerDownAtPoint
pointerDownAtPoint: interactionState.pointerDownAtPoint,
prevCursorFlags: cursorFlags
});
break;
}
Expand Down
Loading
Loading