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