fix: Disable zoom on overscroll
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -417,6 +417,7 @@ import {
|
||||
scrollToElements,
|
||||
constrainScrollState,
|
||||
animateToConstraints,
|
||||
isViewportOverscrolled,
|
||||
} from "../scroll";
|
||||
import {
|
||||
setEraserCursor,
|
||||
@@ -5647,6 +5648,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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;
|
||||
if (initialScale) {
|
||||
this.translateCanvas((state) => ({
|
||||
@@ -6897,7 +6904,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? 1
|
||||
: 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)
|
||||
: this.state.zoom.value;
|
||||
|
||||
@@ -12791,6 +12802,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const { deltaX, deltaY } = event;
|
||||
// note that event.ctrlKey is necessary to handle pinch zooming
|
||||
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 MAX_STEP = ZOOM_STEP * 100;
|
||||
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
|
||||
* overscrolled) position back inside the constraint box via the shared
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
animateToConstraints,
|
||||
constrainScrollState,
|
||||
getMinZoomForConstraints,
|
||||
isViewportOverscrolled,
|
||||
SCROLL_TO_CONTENT_ANIMATION_KEY,
|
||||
} from "../scroll";
|
||||
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)", () => {
|
||||
afterEach(() => {
|
||||
AnimationController.reset();
|
||||
|
||||
Reference in New Issue
Block a user