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