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