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