diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx
index 218c616501..a0cb1b68b1 100644
--- a/apps/admin-x-settings/src/App.tsx
+++ b/apps/admin-x-settings/src/App.tsx
@@ -8,6 +8,7 @@ import {DefaultHeaderTypes} from './unsplash/UnsplashTypes';
import {FetchKoenigLexical, OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
+import {ScrollSectionProvider} from './hooks/useScrollSection';
import {ErrorBoundary as SentryErrorBoundary} from '@sentry/react';
import {Toaster} from 'react-hot-toast';
import {UpgradeStatusType} from './utils/globalTypes';
@@ -51,22 +52,24 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, d
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx
index 3491ad0337..2c31a6766a 100644
--- a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx
+++ b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx
@@ -1,6 +1,6 @@
import NiceModal from '@ebay/nice-modal-react';
import React, {createContext, useCallback, useEffect, useState} from 'react';
-import {ScrollSectionProvider} from '../../hooks/useScrollSection';
+import {useScrollSectionContext} from '../../hooks/useScrollSection';
import type {ModalComponent, ModalName} from './routing/modals';
export type RouteParams = {[key: string]: string}
@@ -124,6 +124,7 @@ type RouteProviderProps = {
const RoutingProvider: React.FC = ({externalNavigate, children}) => {
const [route, setRoute] = useState(undefined);
const [loadingModal, setLoadingModal] = useState(false);
+ const {updateNavigatedSection, scrollToSection} = useScrollSectionContext();
useEffect(() => {
// Preload all the modals after initial render to avoid a delay when opening them
@@ -142,12 +143,14 @@ const RoutingProvider: React.FC = ({externalNavigate, childr
const newPath = options.route;
- if (newPath) {
+ if (newPath === route) {
+ scrollToSection(newPath.split('/')[0]);
+ } else if (newPath) {
window.location.hash = `/settings/${newPath}`;
} else {
window.location.hash = `/settings`;
}
- }, [externalNavigate]);
+ }, [externalNavigate, route, scrollToSection]);
useEffect(() => {
const handleHashChange = () => {
@@ -172,6 +175,12 @@ const RoutingProvider: React.FC = ({externalNavigate, childr
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
+ useEffect(() => {
+ if (route !== undefined) {
+ updateNavigatedSection(route.split('/')[0]);
+ }
+ }, [route, updateNavigatedSection]);
+
if (route === undefined) {
return null;
}
@@ -184,9 +193,7 @@ const RoutingProvider: React.FC = ({externalNavigate, childr
loadingModal
}}
>
-
- {children}
-
+ {children}
);
};
diff --git a/apps/admin-x-settings/src/hooks/useScrollSection.tsx b/apps/admin-x-settings/src/hooks/useScrollSection.tsx
index d378a34252..bd5b0dedf2 100644
--- a/apps/admin-x-settings/src/hooks/useScrollSection.tsx
+++ b/apps/admin-x-settings/src/hooks/useScrollSection.tsx
@@ -4,12 +4,16 @@ interface ScrollSectionContextData {
updateSection: (id: string, element: HTMLDivElement) => void;
updateNav: (id: string, element: HTMLLIElement) => void;
currentSection: string | null;
+ updateNavigatedSection: (id: string) => void;
+ scrollToSection: (id: string) => void;
}
const ScrollSectionContext = createContext({
updateSection: () => {},
updateNav: () => {},
- currentSection: null
+ currentSection: null,
+ updateNavigatedSection: () => {},
+ scrollToSection: () => {}
});
export const useScrollSectionContext = () => useContext(ScrollSectionContext);
@@ -63,51 +67,70 @@ const scrollSidebarNav = (navElement: HTMLLIElement, doneInitialScroll: boolean)
}
};
+const getIntersectingSections = (current: string[], entries: IntersectionObserverEntry[], sectionElements: Record) => {
+ const entriesWithId = entries.map(({isIntersecting, target}) => ({
+ isIntersecting,
+ id: Object.entries(sectionElements).find(([, element]) => element === target)?.[0]
+ })).filter(entry => entry.id) as {id: string; isIntersecting: boolean}[];
+
+ const newlyIntersectingIds = entriesWithId.filter(entry => !current.includes(entry.id) && entry.isIntersecting).map(entry => entry.id);
+ const unintersectingIds = entriesWithId.filter(entry => !entry.isIntersecting).map(entry => entry.id);
+
+ const newSections = current.filter(section => !unintersectingIds.includes(section)).concat(newlyIntersectingIds);
+
+ newSections.sort((first, second) => {
+ const firstElement = sectionElements[first];
+ const secondElement = sectionElements[second];
+
+ if (!firstElement || !secondElement) {
+ return 0;
+ }
+
+ return firstElement.getBoundingClientRect().top - secondElement.getBoundingClientRect().top;
+ });
+
+ return newSections;
+};
+
export const ScrollSectionProvider: React.FC<{
- navigatedSection: string;
children: ReactNode;
-}> = ({navigatedSection, children}) => {
+}> = ({children}) => {
+ const [navigatedSection, _setNavigatedSection] = useState(null);
const sectionElements = useRef>({});
+ const intersectionObserver = useRef(null);
const [intersectingSections, setIntersectingSections] = useState([]);
const [lastIntersectedSection, setLastIntersectedSection] = useState(null);
+ const [hasUpdatedNavigatedSection, setHasUpdatedNavigatedSection] = useState(false);
const [doneInitialScroll, setDoneInitialScroll] = useState(false);
const [, setDoneSidebarScroll] = useState(false);
+ const setNavigatedSection = useCallback((value: string) => {
+ _setNavigatedSection(value);
+ setHasUpdatedNavigatedSection(true);
+ }, []);
+
const navElements = useRef>({});
- const intersectionObserver = useMemo(() => new IntersectionObserver((entries) => {
- const entriesWithId = entries.map(({isIntersecting, target}) => ({
- isIntersecting,
- id: Object.entries(sectionElements.current).find(([, element]) => element === target)?.[0]
- })).filter(entry => entry.id) as {id: string; isIntersecting: boolean}[];
+ const setupIntersectionObserver = useCallback(() => {
+ const observer = new IntersectionObserver((entries) => {
+ setIntersectingSections((sections) => {
+ const newSections = getIntersectingSections(sections, entries, sectionElements.current);
- setIntersectingSections((sections) => {
- const newlyIntersectingIds = entriesWithId.filter(entry => !sections.includes(entry.id) && entry.isIntersecting).map(entry => entry.id);
- const unintersectingIds = entriesWithId.filter(entry => !entry.isIntersecting).map(entry => entry.id);
-
- const newSections = sections.filter(section => !unintersectingIds.includes(section)).concat(newlyIntersectingIds);
-
- newSections.sort((first, second) => {
- const firstElement = sectionElements.current[first];
- const secondElement = sectionElements.current[second];
-
- if (!firstElement || !secondElement) {
- return 0;
+ if (newSections.length) {
+ setLastIntersectedSection(newSections[0]);
}
- return firstElement.getBoundingClientRect().top - secondElement.getBoundingClientRect().top;
+ return newSections;
});
-
- if (newSections.length) {
- setLastIntersectedSection(newSections[0]);
- }
-
- return newSections;
+ }, {
+ rootMargin: `-${scrollMargin - 50}px 0px -40% 0px`
});
- }, {
- rootMargin: `-${scrollMargin - 50}px 0px -40% 0px`
- }), []);
+
+ Object.values(sectionElements.current).forEach(element => observer.observe(element));
+
+ return observer;
+ }, []);
const updateSection = useCallback((id: string, element: HTMLDivElement) => {
if (sectionElements.current[id] === element) {
@@ -115,11 +138,11 @@ export const ScrollSectionProvider: React.FC<{
}
if (sectionElements.current[id]) {
- intersectionObserver.unobserve(sectionElements.current[id]);
+ intersectionObserver.current?.unobserve(sectionElements.current[id]);
}
sectionElements.current[id] = element;
- intersectionObserver.observe(element);
+ intersectionObserver.current?.observe(element);
if (!doneInitialScroll && id === navigatedSection) {
scrollToSection(element, false);
@@ -131,6 +154,12 @@ export const ScrollSectionProvider: React.FC<{
navElements.current[id] = element;
}, []);
+ const scrollTo = useCallback((id: string) => {
+ if (sectionElements.current[id]) {
+ scrollToSection(sectionElements.current[id], true);
+ }
+ }, []);
+
const currentSection = useMemo(() => {
if (navigatedSection && intersectingSections.includes(navigatedSection)) {
return navigatedSection;
@@ -144,28 +173,40 @@ export const ScrollSectionProvider: React.FC<{
}, [intersectingSections, lastIntersectedSection, navigatedSection]);
useEffect(() => {
+ if (!hasUpdatedNavigatedSection) {
+ return;
+ }
+
if (navigatedSection && sectionElements.current[navigatedSection]) {
setDoneInitialScroll((done) => {
scrollToSection(sectionElements.current[navigatedSection], done);
return true;
});
+ } else {
+ // No navigated section means opening settings without a path
+ setDoneInitialScroll(true);
}
- }, [navigatedSection]);
+
+ // Wait for the initial scroll so that the intersecting sections are correct
+ setTimeout(() => setupIntersectionObserver());
+ }, [hasUpdatedNavigatedSection, navigatedSection, setupIntersectionObserver]);
useEffect(() => {
- if (currentSection && navElements.current[currentSection]) {
+ if (hasUpdatedNavigatedSection && currentSection && navElements.current[currentSection]) {
setDoneSidebarScroll((done) => {
scrollSidebarNav(navElements.current[currentSection], done);
return true;
});
}
- }, [currentSection]);
+ }, [hasUpdatedNavigatedSection, currentSection]);
return (
{children}