Skip to content
Open
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
22 changes: 21 additions & 1 deletion apps/web/src/components/Sidebar.pointer.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function pointer(type: string, values: PointerEventInit = {}) {

function gesture(
extraOptions: Partial<ConstructorParameters<typeof SidebarPointerSensor>[0]["options"]> = {},
press: PointerEventInit = {},
) {
const callbacks = {
onStart: vi.fn(),
Expand All @@ -31,7 +32,7 @@ function gesture(
const onFinish = vi.fn();
const props = {
active: "thread",
event: pointer("pointerdown"),
event: pointer("pointerdown", press),
options: { distance: 6, onAttach: vi.fn(), onFinish, ...extraOptions },
...callbacks,
} as unknown as SensorProps<ConstructorParameters<typeof SidebarPointerSensor>[0]["options"]>;
Expand Down Expand Up @@ -85,4 +86,23 @@ describe("sidebar pointer lifecycle", () => {
expect(drag.onEnd).not.toHaveBeenCalled();
expect(drag.onFinish).toHaveBeenCalledExactlyOnceWith(true);
});

// regression: #26548 a pen tip slides past the mouse drag distance during an ordinary tap
it("keeps a pen tap that slides a few pixels as a click, while a mouse still drags", () => {
using _environment = setup();
const onClick = vi.fn();
document.body.addEventListener("click", onClick);
const tap = gesture({}, { pointerType: "pen" });
document.dispatchEvent(pointer("pointermove", { pointerType: "pen", clientX: 18 }));
document.dispatchEvent(pointer("pointerup", { pointerType: "pen", buttons: 0, clientX: 18 }));
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
document.body.removeEventListener("click", onClick);

expect(tap.onStart).not.toHaveBeenCalled();
expect(onClick).toHaveBeenCalledOnce();

const drag = gesture({}, { pointerType: "mouse" });
document.dispatchEvent(pointer("pointermove", { pointerType: "mouse", clientX: 18 }));
expect(drag.onStart).toHaveBeenCalledOnce();
});
});
15 changes: 12 additions & 3 deletions apps/web/src/components/Sidebar.pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ type Options = {
onDrop?: (point: { x: number; y: number }) => boolean;
};

// A pen tip slides further than a mouse during a tap. Chromium's stylus click slop on ChromeOS,
// which it derived from tap data (gesture_configuration_aura.cc).
const PEN_CLICK_SLOP = 10;

export class SidebarPointerSensor {
static activators = [
{
Expand All @@ -25,9 +29,14 @@ export class SidebarPointerSensor {
private readonly pointer: PointerEvent;
private readonly document: Document;
private readonly window: Window;
private readonly distance: number;

constructor(private readonly props: Omit<SensorProps<Options>, "activeNode" | "context">) {
this.pointer = props.event as PointerEvent;
this.distance =
this.pointer.pointerType === "pen"
? Math.max(props.options.distance, PEN_CLICK_SLOP)
: props.options.distance;
this.document = getOwnerDocument(this.pointer.target);
this.window = getWindow(this.pointer.target);
this.document.addEventListener("pointermove", this.move, { passive: false, capture: true });
Expand All @@ -41,7 +50,7 @@ export class SidebarPointerSensor {
this.document.addEventListener("dragstart", this.preventDefault);
this.document.addEventListener("contextmenu", this.preventDefault);
props.options.onAttach(this);
props.onPending(props.active, { distance: props.options.distance }, this.coordinates());
props.onPending(props.active, { distance: this.distance }, this.coordinates());
}

private coordinates = () => ({ x: this.pointer.clientX, y: this.pointer.clientY });
Expand Down Expand Up @@ -69,10 +78,10 @@ export class SidebarPointerSensor {
x: event.clientX - this.pointer.clientX,
y: event.clientY - this.pointer.clientY,
};
if (Math.hypot(offset.x, offset.y) <= this.props.options.distance) {
if (Math.hypot(offset.x, offset.y) <= this.distance) {
this.props.onPending(
this.props.active,
{ distance: this.props.options.distance },
{ distance: this.distance },
this.coordinates(),
offset,
);
Expand Down
Loading