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