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