1import { css } from '@emotion/react'; 2import { breakpoints, theme } from '@expo/styleguide'; 3import some from 'lodash/some'; 4import Router, { NextRouter } from 'next/router'; 5import * as React from 'react'; 6 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 { H1 } from '~/components/base/headings'; 15import { PageApiVersionContextType, usePageApiVersion } from '~/providers/page-api-version'; 16import navigation from '~/public/static/constants/navigation.json'; 17import { NavigationRoute } from '~/types/common'; 18import { Footer } from '~/ui/components/Footer'; 19import { Header } from '~/ui/components/Header'; 20import { Sidebar } from '~/ui/components/Sidebar'; 21 22const STYLES_DOCUMENT = css` 23 background: ${theme.background.default}; 24 margin: 0 auto; 25 padding: 40px 56px; 26 27 hr { 28 border: 0; 29 height: 0.01rem; 30 background-color: ${theme.border.default}; 31 } 32 33 @media screen and (max-width: ${breakpoints.medium + 124}px) { 34 padding: 20px 16px 48px 16px; 35 } 36`; 37 38type Props = React.PropsWithChildren<{ 39 router: NextRouter; 40 title?: string; 41 sourceCodeUrl?: string; 42 tocVisible: boolean; 43 /** If the page should not show up in the Algolia Docsearch results */ 44 hideFromSearch?: boolean; 45 version: PageApiVersionContextType['version']; 46}>; 47 48type State = { 49 isMobileMenuVisible: boolean; 50}; 51 52class DocumentationPageWithApiVersion extends React.Component<Props, State> { 53 state = { 54 isMobileMenuVisible: false, 55 }; 56 57 private layoutRef = React.createRef<DocumentationNestedScrollLayout>(); 58 private sidebarRightRef = React.createRef<SidebarRightComponentType>(); 59 60 componentDidMount() { 61 Router.events.on('routeChangeStart', url => { 62 if (this.layoutRef.current) { 63 if (this.getActiveTopLevelSection() !== this.getActiveTopLevelSection(url)) { 64 window.__sidebarScroll = 0; 65 } else { 66 window.__sidebarScroll = this.layoutRef.current.getSidebarScrollTop(); 67 } 68 } 69 }); 70 window.addEventListener('resize', this.handleResize); 71 } 72 73 componentWillUnmount() { 74 window.removeEventListener('resize', this.handleResize); 75 } 76 77 private handleResize = () => { 78 if (WindowUtils.getViewportSize().width >= breakpoints.medium + 124) { 79 this.setState({ isMobileMenuVisible: false }); 80 window.scrollTo(0, 0); 81 } 82 }; 83 84 private pathStartsWith = (name: string, path: string = this.props.router.pathname) => { 85 return path.startsWith(`/${name}`); 86 }; 87 88 private isArchivePath = () => { 89 return this.props.router.pathname.startsWith('/archive'); 90 }; 91 92 private isReferencePath = (path?: string) => { 93 return this.pathStartsWith('versions', path); 94 }; 95 96 private isGeneralPath = () => { 97 return some(navigation.generalDirectories, name => 98 this.props.router.pathname.startsWith(`/${name}`) 99 ); 100 }; 101 102 private isFeaturePreviewPath = (path?: string) => { 103 return navigation.featurePreview.some((name: string) => this.pathStartsWith(name, path)); 104 }; 105 106 private isPreviewPath = () => { 107 return some(navigation.previewDirectories, name => 108 this.props.router.pathname.startsWith(`/${name}`) 109 ); 110 }; 111 112 private isEasPath = (path?: string) => { 113 return navigation.easDirectories.some(name => this.pathStartsWith(name, path)); 114 }; 115 116 private getCanonicalUrl = () => { 117 if (this.isReferencePath()) { 118 return `https://docs.expo.dev${Utilities.replaceVersionInUrl( 119 this.props.router.pathname, 120 'latest' 121 )}`; 122 } else { 123 return `https://docs.expo.dev${this.props.router.pathname}`; 124 } 125 }; 126 127 private getAlgoliaTag = () => { 128 if (this.props.hideFromSearch === true) { 129 return null; 130 } 131 132 return this.isReferencePath() ? this.props.version : 'none'; 133 }; 134 135 private getRoutes = (): NavigationRoute[] => { 136 if (this.isReferencePath()) { 137 return navigation.reference[this.props.version] as NavigationRoute[]; 138 } else { 139 return navigation[this.getActiveTopLevelSection()] as NavigationRoute[]; 140 } 141 }; 142 143 private getActiveTopLevelSection = (path?: string) => { 144 if (this.isReferencePath(path)) { 145 return 'reference'; 146 } else if (this.isEasPath(path)) { 147 return 'eas'; 148 } else if (this.isGeneralPath()) { 149 return 'general'; 150 } else if (this.isFeaturePreviewPath(path)) { 151 return 'featurePreview'; 152 } else if (this.isPreviewPath()) { 153 return 'preview'; 154 } else if (this.isArchivePath()) { 155 return 'archive'; 156 } 157 158 return 'general'; 159 }; 160 161 render() { 162 const routes = this.getRoutes(); 163 const sidebarActiveGroup = this.getActiveTopLevelSection(); 164 const sidebarScrollPosition = process.browser ? window.__sidebarScroll : 0; 165 166 const sidebarElement = <Sidebar routes={routes} />; 167 const headerElement = ( 168 <Header 169 sidebar={sidebarElement} 170 sidebarActiveGroup={sidebarActiveGroup} 171 isMobileMenuVisible={this.state.isMobileMenuVisible} 172 setMobileMenuVisible={isMobileMenuVisible => this.setState({ isMobileMenuVisible })} 173 /> 174 ); 175 176 const handleContentScroll = (contentScrollPosition: number) => { 177 window.requestAnimationFrame(() => { 178 if (this.sidebarRightRef && this.sidebarRightRef.current) { 179 this.sidebarRightRef.current.handleContentScroll(contentScrollPosition); 180 } 181 }); 182 }; 183 184 const sidebarRight = <DocumentationSidebarRight ref={this.sidebarRightRef} />; 185 186 const algoliaTag = this.getAlgoliaTag(); 187 const title = this.props.title 188 ? `${this.props.title} - Expo Documentation` 189 : `Expo Documentation`; 190 191 const pageContent = ( 192 <> 193 {this.props.title && <H1>{this.props.title}</H1>} 194 {this.props.children} 195 {this.props.title && ( 196 <Footer title={this.props.title} sourceCodeUrl={this.props.sourceCodeUrl} /> 197 )} 198 </> 199 ); 200 201 return ( 202 <DocumentationNestedScrollLayout 203 ref={this.layoutRef} 204 header={headerElement} 205 sidebar={sidebarElement} 206 sidebarActiveGroup={sidebarActiveGroup} 207 sidebarRight={sidebarRight} 208 tocVisible={this.props.tocVisible} 209 isMobileMenuVisible={this.state.isMobileMenuVisible} 210 onContentScroll={handleContentScroll} 211 sidebarScrollPosition={sidebarScrollPosition}> 212 <Head title={this.props.title}> 213 {algoliaTag !== null && <meta name="docsearch:version" content={algoliaTag} />} 214 <meta property="og:title" content={title} /> 215 <meta property="og:type" content="website" /> 216 <meta property="og:image" content="https://docs.expo.dev/static/images/og.png" /> 217 <meta property="og:image:url" content="https://docs.expo.dev/static/images/og.png" /> 218 <meta 219 property="og:image:secure_url" 220 content="https://docs.expo.dev/static/images/og.png" 221 /> 222 <meta property="og:locale" content="en_US" /> 223 <meta property="og:site_name" content="Expo Documentation" /> 224 <meta 225 property="og:description" 226 content="Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React." 227 /> 228 229 <meta name="twitter:site" content="@expo" /> 230 <meta name="twitter:card" content="summary" /> 231 <meta property="twitter:title" content={title} /> 232 <meta 233 name="twitter:description" 234 content="Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React." 235 /> 236 <meta 237 property="twitter:image" 238 content="https://docs.expo.dev/static/images/twitter.png" 239 /> 240 241 {(this.props.version === 'unversioned' || 242 this.isPreviewPath() || 243 this.isArchivePath()) && <meta name="robots" content="noindex" />} 244 245 {this.props.version !== 'unversioned' && ( 246 <link rel="canonical" href={this.getCanonicalUrl()} /> 247 )} 248 </Head> 249 <div css={STYLES_DOCUMENT}>{pageContent}</div> 250 </DocumentationNestedScrollLayout> 251 ); 252 } 253} 254 255export default function DocumentationPage(props: Omit<Props, 'version'>) { 256 const { version } = usePageApiVersion(); 257 return <DocumentationPageWithApiVersion {...props} version={version} />; 258} 259