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