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,16 +2257,14 @@ 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, nextZoom: getNormalizedZoom(value),
nextZoom: getNormalizedZoom(value), },
}, this.state,
this.state, ),
),
}),
); );
}; };
@ -2373,25 +2378,7 @@ class App extends React.Component<AppProps, AppState> {
state, state,
) => { ) => {
this.cancelInProgresAnimation?.(); this.cancelInProgresAnimation?.();
this.setState(state);
// When there are no scroll constraints, update the state directly
if (!this.state.scrollConstraints) {
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( scrollX,
nextState.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 constrainedScrollY = Math.min(
if ("scrollY" in nextState) { scrollConstraints.y,
constrainedScrollY = Math.min( Math.max(
scrollConstraints.y, scrollY,
Math.max( scrollConstraints.y - scrollConstraints.height + height / zoom.value,
nextState.scrollY, ),
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 ( constrainedZoom = {
"zoom" in nextState && value: getNormalizedZoom(Math.max(zoom.value, zoomLimit)),
typeof nextState.zoom === "object" &&
nextState.zoom !== null
) {
constrainedZoom = {
value: getNormalizedZoom(Math.max(nextState.zoom.value, zoomLimit)),
};
}
// Call function to adjust scroll position for centered view depending on the current zoom value
adjustScrollForCenteredView(constrainedZoom.value);
// Return the nextState with constrained scrollX, scrollY, and zoom values
return {
...nextState,
scrollX: constrainedScrollX,
scrollY: constrainedScrollY,
zoom: constrainedZoom,
}; };
adjustScrollForCenteredView(zoom.value);
// If any of the values have changed, set new state
if (
constrainedScrollX !== this.state.scrollX ||
constrainedScrollY !== this.state.scrollY ||
constrainedZoom.value !== this.state.zoom.value
) {
const constrainedState = {
scrollX: constrainedScrollX,
scrollY: constrainedScrollY,
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;