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