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