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 } from '~/ui/components/Sidebar/SidebarHead'; 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 padding-bottom: 24px; 108 overflow-y: scroll; 109 overflow-x: hidden; 110 -webkit-overflow-scrolling: touch; 111 112 /* width */ 113 ::-webkit-scrollbar { 114 width: 6px; 115 } 116 117 /* Track */ 118 ::-webkit-scrollbar-track { 119 background: transparent; 120 cursor: pointer; 121 } 122 123 /* Handle */ 124 ::-webkit-scrollbar-thumb { 125 background: ${theme.background.tertiary}; 126 } 127 128 /* Handle on hover */ 129 ::-webkit-scrollbar-thumb:hover { 130 background: ${theme.background.quaternary}; 131 } 132 133 @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) { 134 overflow-y: auto; 135 } 136`; 137 138const STYLES_CENTER_WRAPPER = css` 139 max-width: 1200px; 140 margin: auto; 141`; 142 143const STYLES_HIDDEN = css` 144 display: none; 145`; 146 147type ScrollContainerProps = React.PropsWithChildren<{ 148 className?: string; 149 scrollPosition?: number; 150 scrollHandler?: () => void; 151}>; 152 153class ScrollContainer extends React.Component<ScrollContainerProps> { 154 scrollRef = React.createRef<HTMLDivElement>(); 155 156 componentDidMount() { 157 if (this.props.scrollPosition && this.scrollRef.current) { 158 this.scrollRef.current.scrollTop = this.props.scrollPosition; 159 } 160 } 161 162 public getScrollTop = () => { 163 return this.scrollRef.current?.scrollTop ?? 0; 164 }; 165 166 public getScrollRef = () => { 167 return this.scrollRef; 168 }; 169 170 render() { 171 return ( 172 <div 173 css={STYLES_SCROLL_CONTAINER} 174 className={this.props.className} 175 ref={this.scrollRef} 176 onScroll={this.props.scrollHandler}> 177 {this.props.children} 178 </div> 179 ); 180 } 181} 182 183type Props = React.PropsWithChildren<{ 184 onContentScroll?: (scrollTop: number) => void; 185 isMobileMenuVisible: boolean; 186 tocVisible: boolean; 187 header: React.ReactNode; 188 sidebarScrollPosition: number; 189 sidebar: React.ReactNode; 190 sidebarActiveGroup: string; 191 sidebarRight: React.ReactElement; 192}>; 193 194export default class DocumentationNestedScrollLayout extends React.Component<Props> { 195 static defaultProps = { 196 sidebarScrollPosition: 0, 197 }; 198 199 sidebarRef = React.createRef<ScrollContainer>(); 200 contentRef = React.createRef<ScrollContainer>(); 201 sidebarRightRef = React.createRef<ScrollContainer>(); 202 203 public getSidebarScrollTop = () => { 204 return this.sidebarRef.current?.getScrollTop() ?? 0; 205 }; 206 207 public getContentScrollTop = () => { 208 return this.contentRef.current?.getScrollTop() ?? 0; 209 }; 210 211 render() { 212 const { 213 header, 214 sidebar, 215 sidebarActiveGroup, 216 sidebarRight, 217 sidebarScrollPosition, 218 isMobileMenuVisible, 219 tocVisible, 220 children, 221 } = this.props; 222 223 return ( 224 <div css={STYLES_CONTAINER}> 225 <div css={STYLES_HEADER}>{header}</div> 226 <div css={STYLES_CONTENT}> 227 <div css={[STYLES_SIDEBAR, STYLES_LEFT]}> 228 <SidebarHead sidebarActiveGroup={sidebarActiveGroup} /> 229 <ScrollContainer ref={this.sidebarRef} scrollPosition={sidebarScrollPosition}> 230 {sidebar} 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