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