1import { css } from '@emotion/react';
2import { theme } from '@expo/styleguide';
3import { breakpoints } from '@expo/styleguide-base';
4import Router, { useRouter } from 'next/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 { pathname } = useRouter();
55
56  const layoutRef = createRef<DocumentationNestedScrollLayout>();
57  const sidebarRightRef = createRef<SidebarRightComponentType>();
58
59  const [isMobileMenuVisible, setMobileMenuVisible] = useState(false);
60
61  const routes = RoutesUtils.getRoutes(pathname, version);
62  const sidebarActiveGroup = RoutesUtils.getPageSection(pathname);
63  const sidebarScrollPosition = process.browser ? window.__sidebarScroll : 0;
64
65  useEffect(() => {
66    Router.events.on('routeChangeStart', url => {
67      if (layoutRef.current) {
68        if (
69          RoutesUtils.getPageSection(pathname) !== RoutesUtils.getPageSection(url) ||
70          pathname === '/'
71        ) {
72          window.__sidebarScroll = 0;
73        } else {
74          window.__sidebarScroll = layoutRef.current.getSidebarScrollTop();
75        }
76      }
77    });
78    window.addEventListener('resize', handleResize);
79    return () => window.removeEventListener('resize', handleResize);
80  });
81
82  const handleResize = () => {
83    if (WindowUtils.getViewportSize().width >= breakpoints.medium + 124) {
84      setMobileMenuVisible(false);
85      window.scrollTo(0, 0);
86    }
87  };
88
89  const handleContentScroll = (contentScrollPosition: number) => {
90    window.requestAnimationFrame(() => {
91      if (sidebarRightRef && sidebarRightRef.current) {
92        sidebarRightRef.current.handleContentScroll(contentScrollPosition);
93      }
94    });
95  };
96
97  const sidebarElement = <Sidebar routes={routes} />;
98  const sidebarRightElement = <DocumentationSidebarRight ref={sidebarRightRef} />;
99  const headerElement = (
100    <Header
101      sidebar={sidebarElement}
102      sidebarActiveGroup={sidebarActiveGroup}
103      isMobileMenuVisible={isMobileMenuVisible}
104      setMobileMenuVisible={newState => setMobileMenuVisible(newState)}
105    />
106  );
107
108  return (
109    <DocumentationNestedScrollLayout
110      ref={layoutRef}
111      header={headerElement}
112      sidebar={sidebarElement}
113      sidebarRight={sidebarRightElement}
114      sidebarActiveGroup={sidebarActiveGroup}
115      tocVisible={props.tocVisible}
116      isMobileMenuVisible={isMobileMenuVisible}
117      onContentScroll={handleContentScroll}
118      sidebarScrollPosition={sidebarScrollPosition}>
119      <Head title={props.title} description={props.description}>
120        {props.hideFromSearch !== true && (
121          <meta
122            name="docsearch:version"
123            content={RoutesUtils.isReferencePath(pathname) ? version : 'none'}
124          />
125        )}
126        {version === 'unversioned' ? (
127          (RoutesUtils.isPreviewPath(pathname) || RoutesUtils.isArchivePath(pathname)) && (
128            <meta name="robots" content="noindex" />
129          )
130        ) : (
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-text="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