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