Improve scrollbar-mouse interaction (#667)

* fix scrollbar detection on high devicePixelRatio devices

* don't create a new element on pointerdown over a scrollbar

* Return scrollbars from renderScene and use it in isOverScrollBars

* remove unneeded setState

* show default cursor when hovering or dragging a scrollbar

* disable scrollbars when in multielement mode

Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
lissitz 2020-03-01 21:43:35 +01:00 committed by GitHub
parent 8e0206cc1e
commit e920c078b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 45 deletions

View File

@ -112,6 +112,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
import { normalizeScroll } from "./scene/data";
import { getCenter, getDistance } from "./gesture";
import { ScrollBars } from "./scene/types";
import { createUndoAction, createRedoAction } from "./actions/actionHistory";
let { elements } = createScene();
@ -214,6 +215,8 @@ let cursorX = 0;
let cursorY = 0;
let isHoldingSpace: boolean = false;
let isPanning: boolean = false;
let isDraggingScrollBar: boolean = false;
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
interface LayerUIProps {
actionManager: ActionManager;
@ -1159,12 +1162,9 @@ export class App extends React.Component<any, AppState> {
isOverHorizontalScrollBar,
isOverVerticalScrollBar,
} = isOverScrollBars(
elements,
event.clientX / window.devicePixelRatio,
event.clientY / window.devicePixelRatio,
canvasWidth / window.devicePixelRatio,
canvasHeight / window.devicePixelRatio,
this.state,
currentScrollBars,
event.clientX,
event.clientY,
);
const { x, y } = viewportCoordsToSceneCoords(
@ -1172,6 +1172,60 @@ export class App extends React.Component<any, AppState> {
this.state,
this.canvas,
);
let lastX = x;
let lastY = y;
if (
(isOverHorizontalScrollBar || isOverVerticalScrollBar) &&
!this.state.multiElement
) {
isDraggingScrollBar = true;
lastX = event.clientX;
lastY = event.clientY;
const onPointerMove = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (isOverHorizontalScrollBar) {
const x = event.clientX;
const dx = x - lastX;
this.setState({
scrollX: normalizeScroll(
this.state.scrollX - dx / this.state.zoom,
),
});
lastX = x;
return;
}
if (isOverVerticalScrollBar) {
const y = event.clientY;
const dy = y - lastY;
this.setState({
scrollY: normalizeScroll(
this.state.scrollY - dy / this.state.zoom,
),
});
lastY = y;
}
};
const onPointerUp = () => {
isDraggingScrollBar = false;
setCursorForShape(this.state.elementType);
lastPointerUp = null;
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp);
};
lastPointerUp = onPointerUp;
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
return;
}
const originX = x;
const originY = y;
@ -1373,14 +1427,6 @@ export class App extends React.Component<any, AppState> {
this.setState({ multiElement: null, draggingElement: element });
}
let lastX = x;
let lastY = y;
if (isOverHorizontalScrollBar || isOverVerticalScrollBar) {
lastX = event.clientX;
lastY = event.clientY;
}
let resizeArrowFn:
| ((
element: ExcalidrawElement,
@ -2115,10 +2161,27 @@ export class App extends React.Component<any, AppState> {
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
}
if (isHoldingSpace || isPanning) {
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
return;
}
const hasDeselectedButton = Boolean(event.buttons);
const {
isOverHorizontalScrollBar,
isOverVerticalScrollBar,
} = isOverScrollBars(
currentScrollBars,
event.clientX,
event.clientY,
);
const isOverScrollBar =
isOverVerticalScrollBar || isOverHorizontalScrollBar;
if (!this.state.draggingElement && !this.state.multiElement) {
if (isOverScrollBar) {
resetCursor();
} else {
setCursorForShape(this.state.elementType);
}
}
const { x, y } = viewportCoordsToSceneCoords(
event,
@ -2138,6 +2201,7 @@ export class App extends React.Component<any, AppState> {
return;
}
const hasDeselectedButton = Boolean(event.buttons);
if (
hasDeselectedButton ||
this.state.elementType !== "selection"
@ -2146,7 +2210,7 @@ export class App extends React.Component<any, AppState> {
}
const selectedElements = getSelectedElements(elements);
if (selectedElements.length === 1) {
if (selectedElements.length === 1 && !isOverScrollBar) {
const resizeElement = getElementWithResizeHandler(
elements,
{ x, y },
@ -2166,7 +2230,8 @@ export class App extends React.Component<any, AppState> {
y,
this.state.zoom,
);
document.documentElement.style.cursor = hitElement ? "move" : "";
document.documentElement.style.cursor =
hitElement && !isOverScrollBar ? "move" : "";
}}
onPointerUp={this.removePointer}
onPointerLeave={this.removePointer}
@ -2279,7 +2344,7 @@ export class App extends React.Component<any, AppState> {
}, 300);
componentDidUpdate() {
const atLeastOneVisibleElement = renderScene(
const { atLeastOneVisibleElement, scrollBars } = renderScene(
elements,
this.state.selectionElement,
this.rc!,
@ -2294,6 +2359,9 @@ export class App extends React.Component<any, AppState> {
renderOptimizations: true,
},
);
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside: scrolledOutside });

View File

@ -36,9 +36,9 @@ export function renderScene(
renderSelection?: boolean;
renderOptimizations?: boolean;
} = {},
): boolean {
) {
if (!canvas) {
return false;
return { atLeastOneVisibleElement: false };
}
const context = canvas.getContext("2d")!;
@ -196,9 +196,10 @@ export function renderScene(
}
});
context.restore();
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
}
return visibleElements.length > 0;
return { atLeastOneVisibleElement: visibleElements.length > 0 };
}
function isVisibleElement(

View File

@ -1,6 +1,7 @@
import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element";
import { FlooredNumber } from "../types";
import { ScrollBars } from "./types";
const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6;
@ -19,7 +20,7 @@ export function getScrollBars(
scrollY: FlooredNumber;
zoom: number;
},
) {
): ScrollBars {
// This is the bounding box of all the elements
const [
elementsMinX,
@ -83,28 +84,7 @@ export function getScrollBars(
};
}
export function isOverScrollBars(
elements: readonly ExcalidrawElement[],
x: number,
y: number,
viewportWidth: number,
viewportHeight: number,
{
scrollX,
scrollY,
zoom,
}: {
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
) {
const scrollBars = getScrollBars(elements, viewportWidth, viewportHeight, {
scrollX,
scrollY,
zoom,
});
export function isOverScrollBars(scrollBars: ScrollBars, x: number, y: number) {
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal,
scrollBars.vertical,

View File

@ -19,3 +19,18 @@ export interface Scene {
}
export type ExportType = "png" | "clipboard" | "backend" | "svg";
export type ScrollBars = {
horizontal: {
x: number;
y: number;
width: number;
height: number;
} | null;
vertical: {
x: number;
y: number;
width: number;
height: number;
} | null;
};