start removing account
[ProtonMail-WebClient.git] / packages / components / containers / layout / PrivateMainSettingsArea.tsx
blobc8d6124e396a18763d677ff0c841043631039b2c
1 import type { ReactNode } from 'react';
2 import { Children, cloneElement, isValidElement, useEffect, useRef } from 'react';
3 import { useLocation } from 'react-router';
5 import SettingsPageTitle from '@proton/components/containers/account/SettingsPageTitle';
6 import SettingsParagraph from '@proton/components/containers/account/SettingsParagraph';
7 import clsx from '@proton/utils/clsx';
9 import createScrollIntoView from '../../helpers/createScrollIntoView';
10 import useAppTitle from '../../hooks/useAppTitle';
11 import ErrorBoundary from '../app/ErrorBoundary';
12 import PrivateMainArea from './PrivateMainArea';
13 import SubSettingsSection from './SubSettingsSection';
14 import { getIsSubsectionAvailable } from './helper';
15 import type { SettingsAreaConfig } from './interface';
17 interface PrivateMainSettingsAreaBaseProps {
18     breadcrumbs?: ReactNode;
19     title?: string;
20     noTitle?: boolean;
21     description?: ReactNode;
22     children?: ReactNode;
25 export const PrivateMainSettingsAreaBase = ({
26     breadcrumbs,
27     title,
28     noTitle,
29     description,
30     children,
31 }: PrivateMainSettingsAreaBaseProps) => {
32     const location = useLocation();
34     const mainAreaRef = useRef<HTMLDivElement>(null);
36     useAppTitle(title);
38     useEffect(() => {
39         if (mainAreaRef.current) {
40             mainAreaRef.current.scrollTop = 0;
41         }
42     }, [location.pathname]);
44     useEffect(() => {
45         const { hash } = location;
47         if (!hash) {
48             return;
49         }
51         if (!mainAreaRef.current) {
52             return;
53         }
54         const mainArea = mainAreaRef.current;
55         let el: Element | null | undefined;
56         try {
57             el = mainArea.querySelector(hash);
58         } catch (e) {}
59         if (!el) {
60             return;
61         }
63         const abortScroll = createScrollIntoView(el, mainArea, true);
64         let removeListeners: () => void;
66         const abort = () => {
67             abortScroll();
68             removeListeners?.();
69         };
71         const options = {
72             passive: true,
73             capture: true,
74         };
76         // Abort on any user interaction such as scrolling, touching, or keyboard interaction
77         window.addEventListener('wheel', abort, options);
78         window.addEventListener('keydown', abort, options);
79         window.addEventListener('mousedown', abort, options);
80         window.addEventListener('touchstart', abort, options);
81         // Automatically abort after some time where it's assumed to have successfully scrolled into place.
82         const timeoutId = window.setTimeout(abort, 15000);
84         removeListeners = () => {
85             window.removeEventListener('wheel', abort, options);
86             window.removeEventListener('keydown', abort, options);
87             window.removeEventListener('mousedown', abort, options);
88             window.removeEventListener('touchstart', abort, options);
89             window.clearTimeout(timeoutId);
90         };
92         return () => {
93             abort();
94         };
95         // Listen to location instead of location.hash since it's possible to click the same #section multiple times and end up with a new entry in history
96     }, [location]);
98     const wrappedSections = Children.toArray(children).map((child) => {
99         if (!isValidElement<{ observer: IntersectionObserver; className: string }>(child)) {
100             return null;
101         }
103         return cloneElement(child);
104     });
106     return (
107         <PrivateMainArea ref={mainAreaRef}>
108             <div className="container-section-sticky">
109                 {breadcrumbs && <div className="mt-6 md:mt-0">{breadcrumbs}</div>}
110                 {!noTitle && (
111                     <SettingsPageTitle className={clsx('mt-14', description ? 'mb-5' : 'mb-14')}>
112                         {title}
113                     </SettingsPageTitle>
114                 )}
115                 {description && <SettingsParagraph className="mb-6">{description}</SettingsParagraph>}
116                 <ErrorBoundary>{wrappedSections}</ErrorBoundary>
117             </div>
118         </PrivateMainArea>
119     );
122 interface PrivateMainSettingsAreaProps {
123     children: ReactNode;
124     config: SettingsAreaConfig;
127 const PrivateMainSettingsArea = ({ children, config }: PrivateMainSettingsAreaProps) => {
128     const { text, title, description, subsections } = config;
130     const wrappedSections = Children.toArray(children).map((child, i) => {
131         if (!isValidElement<{ observer: IntersectionObserver; className: string }>(child)) {
132             return null;
133         }
134         const subsectionConfig = subsections?.[i];
135         if (!subsectionConfig) {
136             throw new Error('Missing subsection');
137         }
138         if (!getIsSubsectionAvailable(subsectionConfig)) {
139             return null;
140         }
142         return (
143             <SubSettingsSection
144                 key={subsectionConfig.id}
145                 id={subsectionConfig.id}
146                 title={subsectionConfig.text}
147                 beta={subsectionConfig.beta}
148                 className="container-section-sticky-section"
149             >
150                 {child}
151             </SubSettingsSection>
152         );
153     });
155     return (
156         <PrivateMainSettingsAreaBase title={title || text} description={description}>
157             {wrappedSections}
158         </PrivateMainSettingsAreaBase>
159     );
162 export default PrivateMainSettingsArea;