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