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