feat: limit scroll in componentDidUpdate

This commit is contained in:
Arnošt Pleskot 2023-07-04 16:49:00 +02:00
parent 209934c90a
commit 2998573e79
No known key found for this signature in database
2 changed files with 72 additions and 81 deletions

View File

@ -199,7 +199,11 @@ import {
isSomeElementSelected, isSomeElementSelected,
} from "../scene"; } from "../scene";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { RenderConfig, ScrollBars } from "../scene/types"; import {
RenderConfig,
ScrollBars,
ConstrainedScrollValues,
} from "../scene/types";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { findShapeByKey, SHAPES } from "../shapes"; import { findShapeByKey, SHAPES } from "../shapes";
import { import {
@ -1621,7 +1625,10 @@ class App extends React.Component<AppProps, AppState> {
), ),
); );
} }
this.renderScene();
const constraintedScroll = this.constrainScroll(prevState);
this.renderScene(constraintedScroll);
this.history.record(this.state, this.scene.getElementsIncludingDeleted()); this.history.record(this.state, this.scene.getElementsIncludingDeleted());
// Do not notify consumers if we're still loading the scene. Among other // Do not notify consumers if we're still loading the scene. Among other
@ -1637,7 +1644,7 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
private renderScene = () => { private renderScene = (constrainedScroll?: ConstrainedScrollValues) => {
const cursorButton: { const cursorButton: {
[id: string]: string | undefined; [id: string]: string | undefined;
} = {}; } = {};
@ -1708,10 +1715,10 @@ class App extends React.Component<AppProps, AppState> {
canvas: this.canvas!, canvas: this.canvas!,
renderConfig: { renderConfig: {
selectionColor, selectionColor,
scrollX: this.state.scrollX, scrollX: constrainedScroll?.scrollX ?? this.state.scrollX,
scrollY: this.state.scrollY, scrollY: constrainedScroll?.scrollY ?? this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor, viewBackgroundColor: this.state.viewBackgroundColor,
zoom: this.state.zoom, zoom: constrainedScroll?.zoom ?? this.state.zoom,
remotePointerViewportCoords: pointerViewportCoords, remotePointerViewportCoords: pointerViewportCoords,
remotePointerButton: cursorButton, remotePointerButton: cursorButton,
remoteSelectedElementIds, remoteSelectedElementIds,
@ -2250,8 +2257,7 @@ class App extends React.Component<AppProps, AppState> {
value: number, value: number,
) => { ) => {
this.setState( this.setState(
this.constrainScroll({ getStateForZoom(
...getStateForZoom(
{ {
viewportX: this.state.width / 2 + this.state.offsetLeft, viewportX: this.state.width / 2 + this.state.offsetLeft,
viewportY: this.state.height / 2 + this.state.offsetTop, viewportY: this.state.height / 2 + this.state.offsetTop,
@ -2259,7 +2265,6 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.state, this.state,
), ),
}),
); );
}; };
@ -2373,25 +2378,7 @@ class App extends React.Component<AppProps, AppState> {
state, state,
) => { ) => {
this.cancelInProgresAnimation?.(); this.cancelInProgresAnimation?.();
// When there are no scroll constraints, update the state directly
if (!this.state.scrollConstraints) {
this.setState(state); this.setState(state);
}
// If state is a function, we generate the new state and then apply scroll constraints
if (typeof state === "function") {
this.setState((prevState, props) => {
// Generate new state
const newState = state(prevState, props);
// Apply scroll constraints to the new state
return this.constrainScroll(newState);
});
} else {
// If state is not a function, apply scroll constraints directly before updating the state
this.setState(this.constrainScroll(state));
}
}; };
setToast = ( setToast = (
@ -7651,15 +7638,17 @@ class App extends React.Component<AppProps, AppState> {
* @param nextState - The next state of the application, or a subset of the application state. * @param nextState - The next state of the application, or a subset of the application state.
* @returns The modified next state with scrollX and scrollY constrained to the scroll constraints. * @returns The modified next state with scrollX and scrollY constrained to the scroll constraints.
*/ */
private constrainScroll = <K extends keyof AppState>( private constrainScroll = (prevState: AppState): ConstrainedScrollValues => {
nextState: AppState | Pick<AppState, K> | null,
) => {
const { scrollX, scrollY, scrollConstraints, width, height, zoom } = const { scrollX, scrollY, scrollConstraints, width, height, zoom } =
this.state; this.state;
// When no scroll constraints are set, return the nextState as is if (
if (!scrollConstraints || !nextState) { !scrollConstraints ||
return nextState; (this.state.zoom.value === prevState.zoom.value &&
this.state.scrollX === prevState.scrollX &&
this.state.scrollY === prevState.scrollY)
) {
return null;
} }
// Calculate maximum zoom for both X and Y axis based on width and height of viewport and scrollable area // Calculate maximum zoom for both X and Y axis based on width and height of viewport and scrollable area
@ -7691,49 +7680,46 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
// If scrollX is part of the nextState, constrain it within the scroll constraints // Constrain scrollX and scrollY within the scroll constraints
if ("scrollX" in nextState) {
constrainedScrollX = Math.min( constrainedScrollX = Math.min(
scrollConstraints.x, scrollConstraints.x,
Math.max( Math.max(
nextState.scrollX, scrollX,
scrollConstraints.x - scrollConstraints.width + width / zoom.value, scrollConstraints.x - scrollConstraints.width + width / zoom.value,
), ),
); );
}
// If scrollY is part of the nextState, constrain it within the scroll constraints
if ("scrollY" in nextState) {
constrainedScrollY = Math.min( constrainedScrollY = Math.min(
scrollConstraints.y, scrollConstraints.y,
Math.max( Math.max(
nextState.scrollY, scrollY,
scrollConstraints.y - scrollConstraints.height + height / zoom.value, scrollConstraints.y - scrollConstraints.height + height / zoom.value,
), ),
); );
}
// If zoom is part of the nextState, constrain it within the scroll constraints and adjust for centered view // Constrain zoom within the scroll constraints and adjust for centered view
if (
"zoom" in nextState &&
typeof nextState.zoom === "object" &&
nextState.zoom !== null
) {
constrainedZoom = { constrainedZoom = {
value: getNormalizedZoom(Math.max(nextState.zoom.value, zoomLimit)), value: getNormalizedZoom(Math.max(zoom.value, zoomLimit)),
}; };
}
// Call function to adjust scroll position for centered view depending on the current zoom value adjustScrollForCenteredView(zoom.value);
adjustScrollForCenteredView(constrainedZoom.value);
// Return the nextState with constrained scrollX, scrollY, and zoom values // If any of the values have changed, set new state
return { if (
...nextState, constrainedScrollX !== this.state.scrollX ||
constrainedScrollY !== this.state.scrollY ||
constrainedZoom.value !== this.state.zoom.value
) {
const constrainedState = {
scrollX: constrainedScrollX, scrollX: constrainedScrollX,
scrollY: constrainedScrollY, scrollY: constrainedScrollY,
zoom: constrainedZoom, zoom: constrainedZoom,
}; };
this.setState(constrainedState);
return constrainedState;
}
return null;
}; };
} }

View File

@ -60,3 +60,8 @@ export type ScrollBars = {
height: number; height: number;
} | null; } | null;
}; };
export type ConstrainedScrollValues = Pick<
AppState,
"scrollX" | "scrollY" | "zoom"
> | null;