Skip to content

Commit

Permalink
Prevent pointer events from triggering on elements behind hitAreaMarg…
Browse files Browse the repository at this point in the history
…ins (#338)

Fixing a problem described in #336 

To solve the issue of click events passing through the hitAreaMargins to
the elements behind them all we need to do is handle the events in the
capture phase and prevent them from bubbling down to the children/panel
content by calling `event.stopPropagation()`.

However cancelling the mouse/touch events (for exmaple
`mousedown`/`touchstart`), does not cancel their pointer event
counterparts (`pointerdown`), like it does the other way around. So I
also changed the event handlers to listen to the pointer events instead.
  • Loading branch information
4nnikaf authored Apr 15, 2024
1 parent 8b049a6 commit d712d21
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 51 deletions.
4 changes: 2 additions & 2 deletions packages/react-resizable-panels/src/PanelGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { computePanelFlexBoxStyle } from "./utils/computePanelFlexBoxStyle";
import debounce from "./utils/debounce";
import { determinePivotIndices } from "./utils/determinePivotIndices";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import { isKeyDown, isMouseEvent, isTouchEvent } from "./utils/events";
import { isKeyDown, isMouseEvent, isPointerEvent } from "./utils/events";
import { getResizeEventCursorPosition } from "./utils/events/getResizeEventCursorPosition";
import { initializeDefaultStorage } from "./utils/initializeDefaultStorage";
import {
Expand Down Expand Up @@ -658,7 +658,7 @@ function PanelGroupWithForwardedRef({

// Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
// Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
if (isMouseEvent(event) || isTouchEvent(event)) {
if (isPointerEvent(event) || isMouseEvent(event)) {
// Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
// In this case, Panel sizes might not change–
// but updating cursor in this scenario would cause a flicker.
Expand Down
3 changes: 2 additions & 1 deletion packages/react-resizable-panels/src/PanelGroupContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { PanelConstraints, PanelData } from "./Panel";
import { CSSProperties, createContext } from "./vendor/react";

export type ResizeEvent = KeyboardEvent | MouseEvent | TouchEvent;
// The "contextmenu" event is not supported as a PointerEvent in all browsers yet, so MouseEvent still need to be handled
export type ResizeEvent = KeyboardEvent | PointerEvent | MouseEvent;
export type ResizeHandler = (event: ResizeEvent) => void;

export type DragState = {
Expand Down
22 changes: 11 additions & 11 deletions packages/react-resizable-panels/src/PanelResizeHandle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,18 @@ describe("PanelResizeHandle", () => {
});

act(() => {
dispatchPointerEvent("mousemove", leftElement);
dispatchPointerEvent("pointermove", leftElement);
});
expect(onDragging).not.toHaveBeenCalled();

act(() => {
dispatchPointerEvent("mousedown", leftElement);
dispatchPointerEvent("pointerdown", leftElement);
});
expect(onDragging).toHaveBeenCalledTimes(1);
expect(onDragging).toHaveBeenCalledWith(true);

act(() => {
dispatchPointerEvent("mouseup", leftElement);
dispatchPointerEvent("pointerup", leftElement);
});
expect(onDragging).toHaveBeenCalledTimes(2);
expect(onDragging).toHaveBeenCalledWith(false);
Expand All @@ -154,20 +154,20 @@ describe("PanelResizeHandle", () => {
});

act(() => {
dispatchPointerEvent("mousemove", leftElement);
dispatchPointerEvent("pointermove", leftElement);
});
expect(onDraggingLeft).not.toHaveBeenCalled();
expect(onDraggingRight).not.toHaveBeenCalled();

act(() => {
dispatchPointerEvent("mousedown", leftElement);
dispatchPointerEvent("pointerdown", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(1);
expect(onDraggingLeft).toHaveBeenCalledWith(true);
expect(onDraggingRight).not.toHaveBeenCalled();

act(() => {
dispatchPointerEvent("mouseup", leftElement);
dispatchPointerEvent("pointerup", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(2);
expect(onDraggingLeft).toHaveBeenCalledWith(false);
Expand Down Expand Up @@ -209,39 +209,39 @@ describe("PanelResizeHandle", () => {
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousemove", leftElement);
dispatchPointerEvent("pointermove", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "hover");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousedown", leftElement);
dispatchPointerEvent("pointerdown", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "drag");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousemove", leftElement);
dispatchPointerEvent("pointermove", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "drag");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mouseup", leftElement);
dispatchPointerEvent("pointerup", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "hover");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousemove", rightElement);
dispatchPointerEvent("pointermove", rightElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
Expand Down
34 changes: 13 additions & 21 deletions packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function handlePointerDown(event: ResizeEvent) {
updateResizeHandlerStates("down", event);

event.preventDefault();
event.stopPropagation();
}
}

Expand Down Expand Up @@ -258,16 +259,13 @@ function updateListeners() {
const { body } = ownerDocument;

body.removeEventListener("contextmenu", handlePointerUp);
body.removeEventListener("mousedown", handlePointerDown);
body.removeEventListener("mouseleave", handlePointerMove);
body.removeEventListener("mousemove", handlePointerMove);
body.removeEventListener("touchmove", handlePointerMove);
body.removeEventListener("touchstart", handlePointerDown);
body.removeEventListener("pointerdown", handlePointerDown);
body.removeEventListener("pointerleave", handlePointerMove);
body.removeEventListener("pointermove", handlePointerMove);
});

window.removeEventListener("mouseup", handlePointerUp);
window.removeEventListener("touchcancel", handlePointerUp);
window.removeEventListener("touchend", handlePointerUp);
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("pointercancel", handlePointerUp);

if (registeredResizeHandlers.size > 0) {
if (isPointerDown) {
Expand All @@ -277,29 +275,23 @@ function updateListeners() {

if (count > 0) {
body.addEventListener("contextmenu", handlePointerUp);
body.addEventListener("mouseleave", handlePointerMove);
body.addEventListener("mousemove", handlePointerMove);
body.addEventListener("touchmove", handlePointerMove, {
passive: false,
});
body.addEventListener("pointerleave", handlePointerMove);
body.addEventListener("pointermove", handlePointerMove);
}
});
}

window.addEventListener("mouseup", handlePointerUp);
window.addEventListener("touchcancel", handlePointerUp);
window.addEventListener("touchend", handlePointerUp);
window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("pointercancel", handlePointerUp);
} else {
ownerDocumentCounts.forEach((count, ownerDocument) => {
const { body } = ownerDocument;

if (count > 0) {
body.addEventListener("mousedown", handlePointerDown);
body.addEventListener("mousemove", handlePointerMove);
body.addEventListener("touchmove", handlePointerMove, {
passive: false,
body.addEventListener("pointerdown", handlePointerDown, {
capture: true,
});
body.addEventListener("touchstart", handlePointerDown);
body.addEventListener("pointermove", handlePointerMove);
}
});
}
Expand Down
3 changes: 2 additions & 1 deletion packages/react-resizable-panels/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type Direction = "horizontal" | "vertical";

export type ResizeEvent = KeyboardEvent | MouseEvent | TouchEvent;
// The "contextmenu" event is not supported as a PointerEvent in all browsers yet, so MouseEvent still need to be handled
export type ResizeEvent = KeyboardEvent | PointerEvent | MouseEvent;
export type ResizeHandler = (event: ResizeEvent) => void;
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { ResizeEvent } from "../../types";
import { isMouseEvent, isTouchEvent } from ".";
import { isMouseEvent, isPointerEvent } from ".";

export function getResizeEventCoordinates(event: ResizeEvent) {
if (isMouseEvent(event)) {
if (isPointerEvent(event)) {
if (event.isPrimary) {
return {
x: event.clientX,
y: event.clientY,
};
}
} else if (isMouseEvent(event)) {
return {
x: event.clientX,
y: event.clientY,
};
} else if (isTouchEvent(event)) {
const touch = event.touches[0];
if (touch && touch.clientX && touch.clientY) {
return {
x: touch.clientX,
y: touch.clientY,
};
}
}

return {
Expand Down
10 changes: 5 additions & 5 deletions packages/react-resizable-panels/src/utils/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ export function isKeyDown(event: ResizeEvent): event is KeyboardEvent {
return event.type === "keydown";
}

export function isMouseEvent(event: ResizeEvent): event is MouseEvent {
return event.type.startsWith("mouse");
export function isPointerEvent(event: ResizeEvent): event is PointerEvent {
return event.type.startsWith("pointer");
}

export function isTouchEvent(event: ResizeEvent): event is TouchEvent {
return event.type.startsWith("touch");
}
export function isMouseEvent(event: ResizeEvent): event is MouseEvent {
return event.type.startsWith("mouse");
}
3 changes: 3 additions & 0 deletions packages/react-resizable-panels/src/utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export function dispatchPointerEvent(type: string, target: HTMLElement) {
return clientY;
},
},
isPrimary: {
value: true
}
});

target.dispatchEvent(event);
Expand Down

0 comments on commit d712d21

Please sign in to comment.