1// NOTE(jim): 2// GETTING NESTED SCROLL RIGHT IS DELICATE BUSINESS. THEREFORE THIS COMPONENT 3// IS THE ONLY PLACE WHERE SCROLL CODE SHOULD BE HANDLED. THANKS. 4import { Global, css } from '@emotion/core'; 5import * as React from 'react'; 6 7import * as Constants from '~/constants/theme'; 8 9// NOTE(jim): Global styles if and only if this component is used. 10const STYLES_GLOBAL = css` 11 body { 12 background: ${Constants.colors.white}; 13 } 14 15 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 16 html { 17 /* width */ 18 ::-webkit-scrollbar { 19 width: 6px; 20 } 21 22 /* Track */ 23 ::-webkit-scrollbar-track { 24 background: ${Constants.expoColors.semantic.background}; 25 } 26 27 /* Handle */ 28 ::-webkit-scrollbar-thumb { 29 background: ${Constants.expoColors.gray[200]}; 30 border-radius: 10px; 31 } 32 33 /* Handle on hover */ 34 ::-webkit-scrollbar-thumb:hover { 35 background: ${Constants.expoColors.gray[300]}; 36 } 37 } 38 } 39`; 40 41const STYLES_CONTAINER = css` 42 width: 100%; 43 height: 100vh; 44 overflow: hidden; 45 margin: 0 auto 0 auto; 46 border-left: 1px solid ${Constants.expoColors.semantic.border}; 47 border-right: 1px solid ${Constants.expoColors.semantic.border}; 48 background: #f9f9f9; 49 50 display: flex; 51 align-items: center; 52 justify-content: space-between; 53 flex-direction: column; 54 55 @media screen and (max-width: 1440px) { 56 border-left: 0px; 57 border-right: 0px; 58 } 59 60 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 61 display: block; 62 height: auto; 63 } 64`; 65 66const STYLES_HEADER = css` 67 background: #fff; 68 flex-shrink: 0; 69 width: 100%; 70 71 @media screen and (min-width: ${Constants.breakpoints.mobile}) { 72 border-bottom: 1px solid ${Constants.expoColors.semantic.border}; 73 } 74 75 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 76 position: sticky; 77 top: -57px; 78 z-index: 3; 79 } 80`; 81 82const SHOW_SEARCH_AND_MENU = css` 83 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 84 top: 0px; 85 } 86`; 87 88const STYLES_CONTENT = css` 89 display: flex; 90 align-items: flex-start; 91 margin: 0 auto; 92 justify-content: space-between; 93 width: 100%; 94 height: 100%; 95 min-height: 25%; 96 97 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 98 height: auto; 99 } 100`; 101 102const STYLES_SIDEBAR = css` 103 flex-shrink: 0; 104 max-width: 280px; 105 height: 100%; 106 overflow: hidden; 107 transition: 200ms ease max-width; 108 background: ${Constants.expoColors.semantic.background}; 109 110 @media screen and (max-width: 1200px) { 111 max-width: 280px; 112 } 113 114 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 115 display: none; 116 } 117`; 118 119const STYLES_LEFT = css` 120 border-right: 1px solid ${Constants.expoColors.semantic.border}; 121`; 122 123const STYLES_RIGHT = css` 124 border-left: 1px solid ${Constants.expoColors.semantic.border}; 125 background-color: ${Constants.expoColors.white}; 126`; 127 128const STYLES_CENTER = css` 129 background: ${Constants.expoColors.white}; 130 min-width: 5%; 131 width: 100%; 132 height: 100%; 133 overflow: hidden; 134 display: flex; 135 136 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 137 height: auto; 138 overflow: auto; 139 } 140`; 141 142// NOTE(jim): 143// All the other components tame the UI. this one allows a container to scroll. 144const STYLES_SCROLL_CONTAINER = css` 145 height: 100%; 146 width: 100%; 147 padding-bottom: 36px; 148 overflow-y: scroll; 149 overflow-x: hidden; 150 -webkit-overflow-scrolling: touch; 151 152 /* width */ 153 ::-webkit-scrollbar { 154 width: 6px; 155 } 156 157 /* Track */ 158 ::-webkit-scrollbar-track { 159 background: ${Constants.expoColors.semantic.background}; 160 cursor: pointer; 161 } 162 163 /* Handle */ 164 ::-webkit-scrollbar-thumb { 165 background: ${Constants.expoColors.gray[200]}; 166 border-radius: 10px; 167 } 168 169 /* Handle on hover */ 170 ::-webkit-scrollbar-thumb:hover { 171 background: ${Constants.expoColors.gray[300]}; 172 } 173 174 @media screen and (max-width: ${Constants.breakpoints.mobile}) { 175 overflow-y: auto; 176 } 177`; 178 179const STYLES_CENTER_WRAPPER = css` 180 max-width: 1200px; 181 margin: auto; 182`; 183 184type ScrollContainerProps = { 185 scrollPosition?: number; 186 scrollHandler?: () => void; 187}; 188 189class ScrollContainer extends React.Component<ScrollContainerProps> { 190 scrollRef = React.createRef<HTMLDivElement>(); 191 192 componentDidMount() { 193 if (this.props.scrollPosition && this.scrollRef.current) { 194 this.scrollRef.current.scrollTop = this.props.scrollPosition; 195 } 196 } 197 198 public getScrollTop = () => { 199 return this.scrollRef.current?.scrollTop ?? 0; 200 }; 201 202 public getScrollRef = () => { 203 return this.scrollRef; 204 }; 205 206 render() { 207 return ( 208 <div css={STYLES_SCROLL_CONTAINER} ref={this.scrollRef} onScroll={this.props.scrollHandler}> 209 {this.props.children} 210 </div> 211 ); 212 } 213} 214 215type Props = { 216 onContentScroll?: (scrollTop: number) => void; 217 isMenuActive: boolean; 218 tocVisible: boolean; 219 isMobileSearchActive: boolean; 220 header: React.ReactNode; 221 sidebarScrollPosition: number; 222 sidebar: React.ReactNode; 223 sidebarRight: React.ReactElement; 224}; 225 226export default class DocumentationNestedScrollLayout extends React.Component<Props> { 227 static defaultProps = { 228 sidebarScrollPosition: 0, 229 }; 230 231 sidebarRef = React.createRef<ScrollContainer>(); 232 contentRef = React.createRef<ScrollContainer>(); 233 sidebarRightRef = React.createRef<ScrollContainer>(); 234 235 public getSidebarScrollTop = () => { 236 return this.sidebarRef.current?.getScrollTop() ?? 0; 237 }; 238 239 public getContentScrollTop = () => { 240 return this.contentRef.current?.getScrollTop() ?? 0; 241 }; 242 243 render() { 244 const { isMobileSearchActive, isMenuActive, sidebarScrollPosition } = this.props; 245 246 if (isMenuActive) { 247 window.scrollTo(0, 0); 248 } 249 250 return ( 251 <div css={STYLES_CONTAINER}> 252 <Global styles={STYLES_GLOBAL} /> 253 <div css={[STYLES_HEADER, (isMobileSearchActive || isMenuActive) && SHOW_SEARCH_AND_MENU]}> 254 {this.props.header} 255 </div> 256 <div css={STYLES_CONTENT}> 257 <div css={[STYLES_SIDEBAR, STYLES_LEFT]}> 258 <ScrollContainer ref={this.sidebarRef} scrollPosition={sidebarScrollPosition}> 259 {this.props.sidebar} 260 </ScrollContainer> 261 </div> 262 263 <div css={STYLES_CENTER}> 264 <ScrollContainer ref={this.contentRef} scrollHandler={this.scrollHandler}> 265 <div css={STYLES_CENTER_WRAPPER}>{this.props.children}</div> 266 </ScrollContainer> 267 </div> 268 269 {this.props.tocVisible && ( 270 <div css={[STYLES_SIDEBAR, STYLES_RIGHT]}> 271 <ScrollContainer ref={this.sidebarRightRef}> 272 {React.cloneElement(this.props.sidebarRight, { 273 selfRef: this.sidebarRightRef, 274 contentRef: this.contentRef, 275 })} 276 </ScrollContainer> 277 </div> 278 )} 279 </div> 280 </div> 281 ); 282 } 283 284 private scrollHandler = () => { 285 this.props.onContentScroll && this.props.onContentScroll(this.getContentScrollTop()); 286 }; 287} 288