fix: Disable zoom on overscroll
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -417,6 +417,7 @@ import {
|
|||||||
scrollToElements,
|
scrollToElements,
|
||||||
constrainScrollState,
|
constrainScrollState,
|
||||||
animateToConstraints,
|
animateToConstraints,
|
||||||
|
isViewportOverscrolled,
|
||||||
} from "../scroll";
|
} from "../scroll";
|
||||||
import {
|
import {
|
||||||
setEraserCursor,
|
setEraserCursor,
|
||||||
@@ -5647,6 +5648,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// while rubberband-overscrolled past the scroll constraints, suppress
|
||||||
|
// zooming until the viewport has snapped back inside the box
|
||||||
|
if (isViewportOverscrolled(this.state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const initialScale = gesture.initialScale;
|
const initialScale = gesture.initialScale;
|
||||||
if (initialScale) {
|
if (initialScale) {
|
||||||
this.translateCanvas((state) => ({
|
this.translateCanvas((state) => ({
|
||||||
@@ -6897,7 +6904,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? 1
|
? 1
|
||||||
: distance / gesture.initialDistance;
|
: distance / gesture.initialDistance;
|
||||||
|
|
||||||
const nextZoom = scaleFactor
|
// while rubberband-overscrolled past the scroll constraints, pin the zoom
|
||||||
|
// (still allowing the pan below) until the viewport has snapped back
|
||||||
|
const nextZoom = isViewportOverscrolled(this.state)
|
||||||
|
? this.state.zoom.value
|
||||||
|
: scaleFactor
|
||||||
? getNormalizedZoom(initialScale * scaleFactor)
|
? getNormalizedZoom(initialScale * scaleFactor)
|
||||||
: this.state.zoom.value;
|
: this.state.zoom.value;
|
||||||
|
|
||||||
@@ -12791,6 +12802,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const { deltaX, deltaY } = event;
|
const { deltaX, deltaY } = event;
|
||||||
// note that event.ctrlKey is necessary to handle pinch zooming
|
// note that event.ctrlKey is necessary to handle pinch zooming
|
||||||
if (event.metaKey || event.ctrlKey) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
|
// while rubberband-overscrolled past the scroll constraints, suppress
|
||||||
|
// zooming until the viewport has snapped back inside the box
|
||||||
|
if (isViewportOverscrolled(this.state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sign = Math.sign(deltaY);
|
const sign = Math.sign(deltaY);
|
||||||
const MAX_STEP = ZOOM_STEP * 100;
|
const MAX_STEP = ZOOM_STEP * 100;
|
||||||
const absDelta = Math.abs(deltaY);
|
const absDelta = Math.abs(deltaY);
|
||||||
|
|||||||
@@ -142,6 +142,31 @@ export const constrainScrollState = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the viewport currently sits outside the hard constraint bounds, i.e.
|
||||||
|
* the user is rubberband-overscrolled (panned past an edge or zoomed out below
|
||||||
|
* the fit zoom). Always `false` when there are no constraints. Used to suppress
|
||||||
|
* zooming until the viewport has settled back inside the box.
|
||||||
|
*/
|
||||||
|
export const isViewportOverscrolled = (
|
||||||
|
state: Pick<
|
||||||
|
AppState,
|
||||||
|
"scrollX" | "scrollY" | "zoom" | "width" | "height" | "scrollConstraints"
|
||||||
|
>,
|
||||||
|
): boolean => {
|
||||||
|
if (!state.scrollConstraints) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = constrainScrollState(state); // hard clamp (tolerance 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
target.scrollX !== state.scrollX ||
|
||||||
|
target.scrollY !== state.scrollY ||
|
||||||
|
target.zoom.value !== state.zoom.value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rubberband snap-back: animates the viewport from its current (possibly
|
* Rubberband snap-back: animates the viewport from its current (possibly
|
||||||
* overscrolled) position back inside the constraint box via the shared
|
* overscrolled) position back inside the constraint box via the shared
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
animateToConstraints,
|
animateToConstraints,
|
||||||
constrainScrollState,
|
constrainScrollState,
|
||||||
getMinZoomForConstraints,
|
getMinZoomForConstraints,
|
||||||
|
isViewportOverscrolled,
|
||||||
SCROLL_TO_CONTENT_ANIMATION_KEY,
|
SCROLL_TO_CONTENT_ANIMATION_KEY,
|
||||||
} from "../scroll";
|
} from "../scroll";
|
||||||
import { AnimationController } from "../renderer/animation";
|
import { AnimationController } from "../renderer/animation";
|
||||||
@@ -271,6 +272,47 @@ describe("rubberband tolerance (pure)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isViewportOverscrolled (pure)", () => {
|
||||||
|
const box: ScrollConstraints = { x: 0, y: 0, width: 1000, height: 1000 };
|
||||||
|
|
||||||
|
it("is false when there are no constraints", () => {
|
||||||
|
expect(
|
||||||
|
isViewportOverscrolled(
|
||||||
|
makeState({ scrollX: 9999, scrollConstraints: null }),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is false when the viewport is within the hard bounds", () => {
|
||||||
|
expect(
|
||||||
|
isViewportOverscrolled(
|
||||||
|
makeState({ scrollX: -100, scrollY: -100, scrollConstraints: box }),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is true when panned past an edge (rubberband overscroll)", () => {
|
||||||
|
// scrollX 30 is past the hard max of 0
|
||||||
|
expect(
|
||||||
|
isViewportOverscrolled(
|
||||||
|
makeState({ scrollX: 30, scrollConstraints: box }),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is true when zoomed out below the fit zoom", () => {
|
||||||
|
// fit zoom for this box is 0.2; 0.1 is below it
|
||||||
|
expect(
|
||||||
|
isViewportOverscrolled(
|
||||||
|
makeState({
|
||||||
|
zoom: { value: getNormalizedZoom(0.1) },
|
||||||
|
scrollConstraints: box,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("animateToConstraints (rubberband snap-back)", () => {
|
describe("animateToConstraints (rubberband snap-back)", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
AnimationController.reset();
|
AnimationController.reset();
|
||||||
|
|||||||
Reference in New Issue
Block a user