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