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