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) || RoutesUtils.isArchivePath(pathname)) && (
129            <meta name="robots" content="noindex" />
130          )
131        ) : (
132          <link rel="canonical" href={getCanonicalUrl(pathname)} />
133        )}
134      </Head>
135      <div css={STYLES_DOCUMENT}>
136        {props.title && (
137          <PageTitle
138            title={props.title}
139            sourceCodeUrl={props.sourceCodeUrl}
140            packageName={props.packageName}
141            iconUrl={props.iconUrl}
142          />
143        )}
144        {props.description && (
145          <P theme="secondary" data-description="true">
146            {props.description}
147          </P>
148        )}
149        {props.title && <Separator />}
150        {props.children}
151        {props.title && (
152          <Footer
153            title={props.title}
154            sourceCodeUrl={props.sourceCodeUrl}
155            packageName={props.packageName}
156          />
157        )}
158      </div>
159    </DocumentationNestedScrollLayout>
160  );
161}
162