diff --git a/client/src/layouts/Layout.jsx b/client/src/layouts/Layout.jsx index 2825fe9..399462b 100644 --- a/client/src/layouts/Layout.jsx +++ b/client/src/layouts/Layout.jsx @@ -1,7 +1,13 @@ +import { ErrorBoundary } from 'react-error-boundary'; import { Outlet, useOutletContext } from 'react-router-dom'; import PullToRefresh from 'react-simple-pull-to-refresh'; -import React, { useState } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; +import React, { useEffect, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + setPageMainHeight, + setPageMainScrollTop, +} from '@stores/storeSliceSettings'; import { useFetchStatusesQuery, @@ -17,6 +23,9 @@ import PageHeader from '@components/PageHeader'; import PullToRefreshMessage from '@components/PullToRefreshMessage'; function Layout() { + const dispatch = useDispatch(); + const refPageMain = useRef(); + const { data: services, error, @@ -24,6 +33,44 @@ function Layout() { refetch, } = useFetchStatusesQuery(); + const setRefPageMainHeight = () => { + const refPageMainHeight = refPageMain.current?.clientHeight; + dispatch(setPageMainHeight(refPageMainHeight)); + }; + + const setRefPageMainScrollTop = () => { + const refPageMainScrollTop = refPageMain.current?.scrollTop; + dispatch(setPageMainScrollTop(refPageMainScrollTop)); + }; + + const setRefPageMeasurements = () => { + setRefPageMainHeight(); + setRefPageMainScrollTop(); + }; + + useEffect(() => { + setRefPageMeasurements(); + }, [ services ]); // Using `services` instead of `refPageMain` as `refPageMain` doesn't calculate the correct value on app init + + const scrollTo = (top) => { + refPageMain.current.scrollTo({ + behavior: 'instant', + top, + }); + }; + + useEffect(() => { + if (refPageMain.current) { + refPageMain.current.addEventListener('scroll', setRefPageMainScrollTop); + window.addEventListener('resize', setRefPageMeasurements); + + return function cleanup() { + refPageMain.current.removeEventListener('scroll', setRefPageMainScrollTop); + window.removeEventListener('resize', setRefPageMeasurements); + }; + } + }, [ services ]); // Using `services` instead of `refPageMain` as `refPageMain` doesn't calculate the correct value on app init + const [ menuOpen, setMenuOpen ] = useState(false); const classes = () => { @@ -70,12 +117,15 @@ function Layout() { setMenuOpen={setMenuOpen} /> -
+
@@ -98,6 +148,10 @@ function Layout() { export default Layout; +export function useScrollTo() { + return useOutletContext(); +} + export function useServices() { return useOutletContext(); } diff --git a/client/src/stores/store.js b/client/src/stores/store.js index 1a46190..8dfc306 100644 --- a/client/src/stores/store.js +++ b/client/src/stores/store.js @@ -4,11 +4,13 @@ import { configureStore } from '@reduxjs/toolkit/'; import { statusApi } from '@api/statusApi'; +import settingsReducer from './storeSliceSettings'; import statusReducer from './storeSliceStatus'; export const store = configureStore({ reducer: { [statusApi.reducerPath]: statusApi.reducer, + settings: settingsReducer, status: statusReducer, devTools: PUBLIC_ENV === 'development', }, diff --git a/client/src/stores/storeSliceSettings.js b/client/src/stores/storeSliceSettings.js new file mode 100644 index 0000000..7b4745d --- /dev/null +++ b/client/src/stores/storeSliceSettings.js @@ -0,0 +1,29 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + pageMainHeight: 0, + pageMainScrollTop: 0, + prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches, +}; + +export const storeSliceSettings = createSlice({ + name: 'settings', + initialState, + reducers: { + setPageMainHeight: (state, action) => { + const { payload } = action; + state.pageMainHeight = payload; + }, + setPageMainScrollTop: (state, action) => { + const { payload } = action; + state.pageMainScrollTop = payload; + }, + }, +}); + +export const { + setPageMainHeight, + setPageMainScrollTop, +} = storeSliceSettings.actions; + +export default storeSliceSettings.reducer; diff --git a/client/src/views/ViewHome.jsx b/client/src/views/ViewHome.jsx index 9a62075..da52f12 100644 --- a/client/src/views/ViewHome.jsx +++ b/client/src/views/ViewHome.jsx @@ -1,7 +1,11 @@ import { NavLink } from 'react-router-dom'; import React, { createRef, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; -import { useServices } from '@layouts/Layout'; +import { + useScrollTo, + useServices, +} from '@layouts/Layout'; import Icon from '@components/Icon'; import IconCircleMinus from '@components/icons/IconCircleMinus'; @@ -15,6 +19,7 @@ import buildPageTitle from '@utils/buildPageTitle'; import { havingTroubleFetchingData } from '@constants/textContent'; function ViewHome() { + const { scrollTo } = useScrollTo(); const { services } = useServices(); if (!services.length) { @@ -29,16 +34,31 @@ function ViewHome() { return service.lineStatuses[0].reason; }; - const ref = useRef(services.map(() => createRef())); + const { + pageMainHeight, + pageMainScrollTop, + } = useSelector((state) => state.settings); + + const refServices = useRef(services.map(() => createRef())); const [ activeService, setActiveService ] = useState(null); const [ statusReasonVisibility, setStatusReasonVisibility ] = useState({}); useEffect(() => { - if (activeService !== null) { - ref.current[activeService].current.scrollIntoView({ - behavior: 'instant', - block: 'center', - }); + if (activeService === null) return; + + const activeServiceHeight = refServices.current[activeService].current.offsetHeight; + const activeServiceTop = refServices.current[activeService].current.offsetTop; + const activeServiceBottom = activeServiceHeight + activeServiceTop; + + if (activeServiceHeight <= pageMainHeight) { // The active service fits on the page + if (activeServiceTop <= pageMainScrollTop) { // The top of the active service is above the top of the page + scrollTo(activeServiceTop); + } + else if (activeServiceBottom > (pageMainHeight + pageMainScrollTop)) { // The bottom of the active service is below the bottom of the page + scrollTo(activeServiceBottom - pageMainHeight); + } + } else { // The active service doesn't fit on the page so we should show as much as possible + scrollTo(activeServiceTop); } }, [ activeService ]); @@ -86,7 +106,7 @@ function ViewHome() {