Skip to content

Commit

Permalink
Make splitter respond to touch (#4794)
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin authored Oct 18, 2024
1 parent 524fa9e commit f47a252
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 25 deletions.
52 changes: 43 additions & 9 deletions packages/react-components/src/split-pane/sash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { mergeClasses } from "@fluentui/react-components";
import { useState, type ReactNode } from "react";
import style from "./split-pane.module.css";

export interface DraggingEvent {
readonly originalEvent: MouseEvent | TouchEvent;
readonly pageX: number;
readonly pageY: number;
}
export interface SashProps {
className?: string;
style: React.CSSProperties;
render: (dragging: boolean) => ReactNode;
onReset: () => void;
onDragStart: React.MouseEventHandler<HTMLDivElement>;
onDragging: React.MouseEventHandler<HTMLDivElement>;
onDragEnd: React.MouseEventHandler<HTMLDivElement>;
onDragStart: (evt: DraggingEvent) => void;
onDragging: (evt: DraggingEvent) => void;
onDragEnd: (evt: DraggingEvent) => void;
}

export const Sash = ({
Expand All @@ -23,32 +28,61 @@ export const Sash = ({
}: SashProps) => {
const [draging, setDrag] = useState(false);

const handleMouseMove = (e: any) => {
onDragging(e);
};
const handleMouseMove = (e: MouseEvent) => onDragging(createDraggingEventFromMouseEvent(e));

const handleMouseUp = (e: any) => {
const handleMouseUp = (e: MouseEvent) => {
setDrag(false);
onDragEnd(e);
onDragEnd(createDraggingEventFromMouseEvent(e));
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};

const handleTouchMove = (e: TouchEvent) => onDragging(createDraggingEventFromTouchEvent(e));
const handleTouchUp = (e: TouchEvent) => {
setDrag(false);
onDragEnd(createDraggingEventFromTouchEvent(e));
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handleTouchUp);
};

return (
<div
role="separator"
className={mergeClasses(style["sash"], className)}
onMouseDown={(e) => {
setDrag(true);
onDragStart(e);
onDragStart(createDraggingEventFromMouseEvent(e as any));

window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}}
onTouchStart={(e) => {
setDrag(true);
onDragStart(createDraggingEventFromTouchEvent(e as any));

window.addEventListener("touchmove", handleTouchMove);
window.addEventListener("touchend", handleTouchUp);
}}
onDoubleClick={onReset}
{...others}
>
{render(draging)}
</div>
);
};

function createDraggingEventFromMouseEvent(originalEvent: MouseEvent): DraggingEvent {
return {
originalEvent,
pageX: originalEvent.pageX,
pageY: originalEvent.pageY,
};
}
function createDraggingEventFromTouchEvent(originalEvent: TouchEvent): DraggingEvent {
const lastTouch = originalEvent.touches[originalEvent.touches.length - 1];
return {
originalEvent,
pageX: lastTouch.clientX,
pageY: lastTouch.clientY,
};
}
51 changes: 50 additions & 1 deletion packages/react-components/src/split-pane/split-pane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,17 @@ describe("sizes", () => {
if (pageY !== undefined) (evt as any).pageY = pageY;
return evt;
}
function mockTouchEvent(
target: HTMLElement,
type: keyof typeof createEvent,
{ touches }: { touches: Partial<TouchEvent["touches"][number]>[] },
) {
const evt = createEvent[type](target);
(evt as any).touches = touches;
return evt;
}

it("resize", async () => {
it("resize (mouse)", async () => {
render(
<SplitPane initialSizes={[undefined, undefined]}>
<Pane>Pane 1</Pane>
Expand All @@ -98,4 +107,44 @@ describe("sizes", () => {
expect(pane1).toHaveStyle({ width: "600px" });
expect(pane2).toHaveStyle({ width: "400px" });
});

it("resize (touch)", async () => {
render(
<SplitPane initialSizes={[undefined, undefined]}>
<Pane>Pane 1</Pane>
<Pane>Pane 2</Pane>
</SplitPane>,
);
const separator = await screen.getByRole("separator");
const pane1 = await screen.findByText("Pane 1");
const pane2 = await screen.findByText("Pane 2");

expect(pane1).toHaveStyle({ width: "500px" });
expect(pane2).toHaveStyle({ width: "500px" });

fireEvent(
separator,
mockTouchEvent(separator, "touchStart", { touches: [{ clientX: 500, clientY: 0 }] }),
);
fireEvent(
separator,
mockTouchEvent(separator, "touchMove", { touches: [{ clientX: 600, clientY: 0 }] }),
);

expect(pane1).toHaveStyle({ width: "600px" });
expect(pane2).toHaveStyle({ width: "400px" });

fireEvent(
separator,
mockTouchEvent(separator, "touchEnd", { touches: [{ clientX: 600, clientY: 0 }] }),
);
fireEvent(
separator,
mockTouchEvent(separator, "touchMove", { touches: [{ clientX: 700, clientY: 0 }] }),
);

// Should not update after we mouse up
expect(pane1).toHaveStyle({ width: "600px" });
expect(pane2).toHaveStyle({ width: "400px" });
});
});
22 changes: 7 additions & 15 deletions packages/react-components/src/split-pane/split-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import { mergeClasses } from "@fluentui/react-components";
import {
useCallback,
useMemo,
useRef,
useState,
type FunctionComponent,
type JSX,
type MouseEvent,
} from "react";
import { useCallback, useMemo, useRef, useState, type FunctionComponent, type JSX } from "react";
import { useControllableValue } from "../hooks.js";
import { Pane, type PaneProps } from "./pane.js";
import { SashContent } from "./sash-content.js";
import { Sash } from "./sash.js";
import { Sash, type DraggingEvent } from "./sash.js";
import style from "./split-pane.module.css";
import { useElDimensions } from "./use-el-dimensions.js";

Expand All @@ -23,8 +15,8 @@ export interface SplitPaneProps {
sizes?: (string | number | undefined)[];
sashRender?: (index: number, active: boolean) => React.ReactNode;
onChange?: (sizes: number[]) => void;
onDragStart?: (e: MouseEvent) => void;
onDragEnd?: (e: MouseEvent) => void;
onDragStart?: (e: DraggingEvent) => void;
onDragEnd?: (e: DraggingEvent) => void;
className?: string;
sashClassName?: string;
performanceMode?: boolean;
Expand Down Expand Up @@ -131,7 +123,7 @@ export const SplitPane: FunctionComponent<SplitPaneProps> = ({
);

const dragStart = useCallback(
(e: any) => {
(e: DraggingEvent) => {
document?.body?.classList?.add(style["split-disabled"]);
axis.current = { x: e.pageX, y: e.pageY };
cacheSizes.current = { sizes, sashPosSizes };
Expand All @@ -148,7 +140,7 @@ export const SplitPane: FunctionComponent<SplitPaneProps> = ({
}, [defaultSizes, updateSizes]);

const dragEnd = useCallback(
(e: any) => {
(e: DraggingEvent) => {
document?.body?.classList?.remove(style["split-disabled"]);
axis.current = { x: e.pageX, y: e.pageY };
cacheSizes.current = { sizes, sashPosSizes };
Expand All @@ -159,7 +151,7 @@ export const SplitPane: FunctionComponent<SplitPaneProps> = ({
);

const onDragging = useCallback(
(e: MouseEvent<HTMLDivElement>, i: number) => {
(e: DraggingEvent, i: number) => {
const curAxis = { x: e.pageX, y: e.pageY };
let distanceX = curAxis[splitAxis] - axis.current[splitAxis];

Expand Down

0 comments on commit f47a252

Please sign in to comment.