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 { theme } from '@expo/styleguide'; 6import { breakpoints } from '@expo/styleguide-base'; 7import * as React from 'react'; 8 9import { SidebarHead, SidebarFooter } from '~/ui/components/Sidebar'; 10 11const STYLES_CONTAINER = css` 12 width: 100%; 13 height: 100vh; 14 overflow: hidden; 15 margin: 0 auto 0 auto; 16 border-right: 1px solid ${theme.border.default}; 17 background: ${theme.background.default}; 18 19 display: flex; 20 align-items: center; 21 justify-content: space-between; 22 flex-direction: column; 23 24 @media screen and (max-width: 1440px) { 25 border-left: 0px; 26 border-right: 0px; 27 } 28 29 @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) { 30 display: block; 31 height: auto; 32 } 33`; 34 35const STYLES_HEADER = css` 36 flex-shrink: 0; 37 width: 100%; 38 39 @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) { 40 position: sticky; 41 top: -57px; 42 z-index: 3; 43 max-height: 100vh; 44 } 45`; 46 47const STYLES_CONTENT = css` 48 display: flex; 49 align-items: flex-start; 50 margin: 0 auto; 51 justify-content: space-between; 52 width: 100%; 53 height: 100%; 54 min-height: 25%; 55 56 @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) { 57 height: auto; 58 } 59`; 60 61const STYLES_SIDEBAR = css` 62 display: flex; 63 flex-direction: column; 64 flex-shrink: 0; 65 max-width: 280px; 66 height: 100%; 67 overflow: hidden; 68 transition: 200ms ease max-width; 69 background: ${theme.background.default}; 70 71 @media screen and (max-width: 1200px) { 72 max-width: 280px; 73 } 74 75 @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) { 76 display: none; 77 } 78`; 79 80const STYLES_LEFT = css` 81 border-right: 1px solid ${theme.border.default}; 82`; 83 84const STYLES_RIGHT = css` 85 border-left: 1px solid ${theme.border.default}; 86 background-color: ${theme.background.default}; 87`; 88 89const STYLES_CENTER = css` 90 background: ${theme.background.default}; 91 min-width: 5%; 92 width: 100%; 93 height: 100%; 94 overflow: hidden; 95 display: flex; 96 97 @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) { 98 height: auto; 99 overflow: auto; 100 } 101`; 102 103// NOTE(jim): 104// All the other components tame the UI. this one allows a container to scroll. 105const STYLES_SCROLL_CONTAINER = css` 106 height: 100%; 107 width: 100%; 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.palette.gray5}; 126 } 127 128 /* Handle on hover */ 129 ::-webkit-scrollbar-thumb:hover { 130 background: ${theme.palette.gray6}; 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 <SidebarFooter /> 232 </ScrollContainer> 233 </div> 234 <div css={[STYLES_CENTER, isMobileMenuVisible && STYLES_HIDDEN]}> 235 <ScrollContainer ref={this.contentRef} scrollHandler={this.scrollHandler}> 236 <div css={STYLES_CENTER_WRAPPER}>{children}</div> 237 </ScrollContainer> 238 </div> 239 {tocVisible && ( 240 <div css={[STYLES_SIDEBAR, STYLES_RIGHT]}> 241 <ScrollContainer ref={this.sidebarRightRef}> 242 {React.cloneElement(sidebarRight, { 243 selfRef: this.sidebarRightRef, 244 contentRef: this.contentRef, 245 })} 246 </ScrollContainer> 247 </div> 248 )} 249 </div> 250 </div> 251 ); 252 } 253 254 private scrollHandler = () => { 255 this.props.onContentScroll && this.props.onContentScroll(this.getContentScrollTop()); 256 }; 257} 258