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