diff --git a/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx b/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx index c3f47f107d..b8726a1a75 100644 --- a/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx +++ b/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx @@ -5,6 +5,12 @@ export type ShouldUpdateScrollFn = ( locationContext: MastodonLocation, ) => boolean; +/** + * ScrollBehavior will automatically scroll to the top on navigations + * or restore saved scroll positions, but on some location changes we + * need to prevent this. + */ + export const defaultShouldUpdateScroll: ShouldUpdateScrollFn = ( prevLocation, location, diff --git a/app/javascript/mastodon/containers/scroll_container/index.tsx b/app/javascript/mastodon/containers/scroll_container/index.tsx index a17b7bce5a..e7d2726715 100644 --- a/app/javascript/mastodon/containers/scroll_container/index.tsx +++ b/app/javascript/mastodon/containers/scroll_container/index.tsx @@ -5,15 +5,18 @@ import type { ShouldUpdateScrollFn } from './default_should_update_scroll'; import { ScrollBehaviorContext } from './scroll_context'; interface ScrollContainerProps { + /** + * This key must be static for the element & not change + * while the component is mounted. + */ scrollKey: string; shouldUpdateScroll?: ShouldUpdateScrollFn; children: React.ReactElement; } /** - * `ScrollContainer` is used to automatically scroll to the top of the page - * when pushing a new history state, and remembering the scroll position - * when going back. + * `ScrollContainer` is used to manage the scroll position of elements on the page + * that can be scrolled independently of the page body. * This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/ */ @@ -26,6 +29,9 @@ export const ScrollContainer: React.FC = ({ const containerRef = useRef(); + /** + * Register/unregister scrollable element with ScrollBehavior + */ useEffect(() => { if (!scrollBehaviorContext || !containerRef.current) { return; diff --git a/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx b/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx index 122909ef9a..a7eb780800 100644 --- a/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx +++ b/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx @@ -41,6 +41,14 @@ interface ScrollContextProps { } /** + * A top-level wrapper that provides the app with an instance of the + * ScrollBehavior object. scroll-behavior is a library for managing the + * scroll position of a single-page app in the same way the browser would + * normally do for a multi-page app. This means it'll scroll back to top + * when navigating to a new page, and will restore the scroll position + * when navigating e.g. using `history.back`. + * The library keeps a record of scroll positions in session storage. + * * This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/ */ @@ -51,16 +59,24 @@ export const ScrollContext: React.FC = ({ const location = useLocation(); const history = useHistory(); + /** + * Keep the current location in a mutable ref so that ScrollBehavior's + * `getCurrentLocation` can access it without having to recreate the + * whole ScrollBehavior object + */ const currentLocationRef = useRef(location); useEffect(() => { currentLocationRef.current = location; }, [location]); + /** + * Initialise ScrollBehavior object once – using state rather + * than a ref to simplify the types and ensure it's defined immediately. + */ const [scrollBehavior] = useState( (): ScrollBehaviorInstance => new ScrollBehavior({ - // eslint-disable-next-line @typescript-eslint/unbound-method - addNavigationListener: history.listen, + addNavigationListener: history.listen.bind(history), stateStorage: new SessionStorage(), getCurrentLocation: () => currentLocationRef.current as unknown as LocationBase, @@ -77,18 +93,28 @@ export const ScrollContext: React.FC = ({ }), ); - // Handle scroll update when location changes + /** + * Handle scroll update when location changes + */ const prevLocation = usePrevious(location) ?? null; useEffect(() => { scrollBehavior.updateScroll(prevLocation, location); }, [location, prevLocation, scrollBehavior]); + /** + * Stop Scrollbehavior on unmount + */ useEffect(() => { return () => { scrollBehavior.stop(); }; }, [scrollBehavior]); + /** + * Provide the app with a way to register separately scrollable + * elements to also be tracked by ScrollBehavior. (By default + * ScrollBehavior only handles scrolling on the main document body.) + */ const contextValue = useMemo( () => ({ registerElement: (key, element, shouldUpdateScroll) => { diff --git a/app/javascript/mastodon/containers/scroll_container/state_storage.ts b/app/javascript/mastodon/containers/scroll_container/state_storage.ts index 5d1ff4ab26..fe8a208aae 100644 --- a/app/javascript/mastodon/containers/scroll_container/state_storage.ts +++ b/app/javascript/mastodon/containers/scroll_container/state_storage.ts @@ -8,6 +8,10 @@ interface LocationBaseWithKey extends LocationBase { /** * This module is part of our port of https://github.com/ytase/react-router-scroll/ + * and handles storing scroll positions in SessionStorage. + * Stored positions (`[x, y]`) are keyed by the location key and an optional + * `scrollKey` that's used for to track separately scrollable elements other + * than the document body. */ export class SessionStorage {