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