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) || RoutesUtils.isArchivePath(pathname)) && ( 129 <meta name="robots" content="noindex" /> 130 ) 131 ) : ( 132 <link rel="canonical" href={getCanonicalUrl(pathname)} /> 133 )} 134 </Head> 135 <div css={STYLES_DOCUMENT}> 136 {props.title && ( 137 <PageTitle 138 title={props.title} 139 sourceCodeUrl={props.sourceCodeUrl} 140 packageName={props.packageName} 141 iconUrl={props.iconUrl} 142 /> 143 )} 144 {props.description && ( 145 <P theme="secondary" data-description="true"> 146 {props.description} 147 </P> 148 )} 149 {props.title && <Separator />} 150 {props.children} 151 {props.title && ( 152 <Footer 153 title={props.title} 154 sourceCodeUrl={props.sourceCodeUrl} 155 packageName={props.packageName} 156 /> 157 )} 158 </div> 159 </DocumentationNestedScrollLayout> 160 ); 161} 162