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