1import { css } from '@emotion/react';
2import { theme } from '@expo/styleguide';
3import { breakpoints } from '@expo/styleguide-base';
4import { useRouter } from 'next/compat/router';
5import { useEffect, useState, createRef } from 'react';
6
7import * as RoutesUtils from '~/common/routes';
8import * as Utilities from '~/common/utilities';
9import * as WindowUtils from '~/common/window';
10import DocumentationNestedScrollLayout from '~/components/DocumentationNestedScrollLayout';
11import DocumentationSidebarRight, {
12  SidebarRightComponentType,
13} from '~/components/DocumentationSidebarRight';
14import Head from '~/components/Head';
15import { usePageApiVersion } from '~/providers/page-api-version';
16import { Footer } from '~/ui/components/Footer';
17import { Header } from '~/ui/components/Header';
18import { PageTitle } from '~/ui/components/PageTitle';
19import { Separator } from '~/ui/components/Separator';
20import { Sidebar } from '~/ui/components/Sidebar';
21import { P } from '~/ui/components/Text';
22
23const STYLES_DOCUMENT = css`
24  background: ${theme.background.default};
25  margin: 0 auto;
26  padding: 40px 56px;
27
28  @media screen and (max-width: ${breakpoints.medium + 124}px) {
29    padding: 20px 16px 48px 16px;
30  }
31`;
32
33type Props = React.PropsWithChildren<{
34  title?: string;
35  description?: string;
36  sourceCodeUrl?: string;
37  tocVisible: boolean;
38  packageName?: string;
39  iconUrl?: string;
40  /** If the page should not show up in the Algolia Docsearch results */
41  hideFromSearch?: boolean;
42}>;
43
44const getCanonicalUrl = (path: string) => {
45  if (RoutesUtils.isReferencePath(path)) {
46    return `https://docs.expo.dev${Utilities.replaceVersionInUrl(path, 'latest')}`;
47  } else {
48    return `https://docs.expo.dev${path}`;
49  }
50};
51
52export default function DocumentationPage(props: Props) {
53  const { version } = usePageApiVersion();
54  const router = useRouter();
55  const pathname = router?.pathname ?? '/';
56
57  const layoutRef = createRef<DocumentationNestedScrollLayout>();
58  const sidebarRightRef = createRef<SidebarRightComponentType>();
59
60  const [isMobileMenuVisible, setMobileMenuVisible] = useState(false);
61
62  const routes = RoutesUtils.getRoutes(pathname, version);
63  const sidebarActiveGroup = RoutesUtils.getPageSection(pathname);
64  const sidebarScrollPosition = process.browser ? window.__sidebarScroll : 0;
65
66  useEffect(() => {
67    router?.events.on('routeChangeStart', url => {
68      if (layoutRef.current) {
69        if (
70          RoutesUtils.getPageSection(pathname) !== RoutesUtils.getPageSection(url) ||
71          pathname === '/'
72        ) {
73          window.__sidebarScroll = 0;
74        } else {
75          window.__sidebarScroll = layoutRef.current.getSidebarScrollTop();
76        }
77      }
78    });
79    window.addEventListener('resize', handleResize);
80    return () => window.removeEventListener('resize', handleResize);
81  });
82
83  const handleResize = () => {
84    if (WindowUtils.getViewportSize().width >= breakpoints.medium + 124) {
85      setMobileMenuVisible(false);
86      window.scrollTo(0, 0);
87    }
88  };
89
90  const handleContentScroll = (contentScrollPosition: number) => {
91    window.requestAnimationFrame(() => {
92      if (sidebarRightRef && sidebarRightRef.current) {
93        sidebarRightRef.current.handleContentScroll(contentScrollPosition);
94      }
95    });
96  };
97
98  const sidebarElement = <Sidebar routes={routes} />;
99  const sidebarRightElement = <DocumentationSidebarRight ref={sidebarRightRef} />;
100  const headerElement = (
101    <Header
102      sidebar={sidebarElement}
103      sidebarActiveGroup={sidebarActiveGroup}
104      isMobileMenuVisible={isMobileMenuVisible}
105      setMobileMenuVisible={newState => setMobileMenuVisible(newState)}
106    />
107  );
108
109  return (
110    <DocumentationNestedScrollLayout
111      ref={layoutRef}
112      header={headerElement}
113      sidebar={sidebarElement}
114      sidebarRight={sidebarRightElement}
115      sidebarActiveGroup={sidebarActiveGroup}
116      tocVisible={props.tocVisible}
117      isMobileMenuVisible={isMobileMenuVisible}
118      onContentScroll={handleContentScroll}
119      sidebarScrollPosition={sidebarScrollPosition}>
120      <Head title={props.title} description={props.description}>
121        {props.hideFromSearch !== true && (
122          <meta
123            name="docsearch:version"
124            content={RoutesUtils.isReferencePath(pathname) ? version : 'none'}
125          />
126        )}
127        {(version === 'unversioned' ||
128          RoutesUtils.isPreviewPath(pathname) ||
129          RoutesUtils.isArchivePath(pathname)) && <meta name="robots" content="noindex" />}
130        {version !== 'latest' && version !== 'unversioned' && (
131          <link rel="canonical" href={getCanonicalUrl(pathname)} />
132        )}
133      </Head>
134      <div css={STYLES_DOCUMENT}>
135        {props.title && (
136          <PageTitle
137            title={props.title}
138            sourceCodeUrl={props.sourceCodeUrl}
139            packageName={props.packageName}
140            iconUrl={props.iconUrl}
141          />
142        )}
143        {props.description && (
144          <P theme="secondary" data-description="true">
145            {props.description}
146          </P>
147        )}
148        {props.title && <Separator />}
149        {props.children}
150        {props.title && (
151          <Footer
152            title={props.title}
153            sourceCodeUrl={props.sourceCodeUrl}
154            packageName={props.packageName}
155          />
156        )}
157      </div>
158    </DocumentationNestedScrollLayout>
159  );
160}
161