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 { breakpoints, theme } from '@expo/styleguide';
6import * as React from 'react';
7
8import { SidebarHead } from '~/ui/components/Sidebar/SidebarHead';
9
10const STYLES_CONTAINER = css`
11  width: 100%;
12  height: 100vh;
13  overflow: hidden;
14  margin: 0 auto 0 auto;
15  border-right: 1px solid ${theme.border.default};
16  background: ${theme.background.default};
17
18  display: flex;
19  align-items: center;
20  justify-content: space-between;
21  flex-direction: column;
22
23  @media screen and (max-width: 1440px) {
24    border-left: 0px;
25    border-right: 0px;
26  }
27
28  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
29    display: block;
30    height: auto;
31  }
32`;
33
34const STYLES_HEADER = css`
35  flex-shrink: 0;
36  width: 100%;
37
38  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
39    position: sticky;
40    top: -57px;
41    z-index: 3;
42    max-height: 100vh;
43  }
44`;
45
46const STYLES_CONTENT = css`
47  display: flex;
48  align-items: flex-start;
49  margin: 0 auto;
50  justify-content: space-between;
51  width: 100%;
52  height: 100%;
53  min-height: 25%;
54
55  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
56    height: auto;
57  }
58`;
59
60const STYLES_SIDEBAR = css`
61  display: flex;
62  flex-direction: column;
63  flex-shrink: 0;
64  max-width: 280px;
65  height: 100%;
66  overflow: hidden;
67  transition: 200ms ease max-width;
68  background: ${theme.background.default};
69
70  @media screen and (max-width: 1200px) {
71    max-width: 280px;
72  }
73
74  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
75    display: none;
76  }
77`;
78
79const STYLES_LEFT = css`
80  border-right: 1px solid ${theme.border.default};
81`;
82
83const STYLES_RIGHT = css`
84  border-left: 1px solid ${theme.border.default};
85  background-color: ${theme.background.default};
86`;
87
88const STYLES_CENTER = css`
89  background: ${theme.background.default};
90  min-width: 5%;
91  width: 100%;
92  height: 100%;
93  overflow: hidden;
94  display: flex;
95
96  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
97    height: auto;
98    overflow: auto;
99  }
100`;
101
102// NOTE(jim):
103// All the other components tame the UI. this one allows a container to scroll.
104const STYLES_SCROLL_CONTAINER = css`
105  height: 100%;
106  width: 100%;
107  padding-bottom: 24px;
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.background.tertiary};
126  }
127
128  /* Handle on hover */
129  ::-webkit-scrollbar-thumb:hover {
130    background: ${theme.background.quaternary};
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            </ScrollContainer>
232          </div>
233          <div css={[STYLES_CENTER, isMobileMenuVisible && STYLES_HIDDEN]}>
234            <ScrollContainer ref={this.contentRef} scrollHandler={this.scrollHandler}>
235              <div css={STYLES_CENTER_WRAPPER}>{children}</div>
236            </ScrollContainer>
237          </div>
238          {tocVisible && (
239            <div css={[STYLES_SIDEBAR, STYLES_RIGHT]}>
240              <ScrollContainer ref={this.sidebarRightRef}>
241                {React.cloneElement(sidebarRight, {
242                  selfRef: this.sidebarRightRef,
243                  contentRef: this.contentRef,
244                })}
245              </ScrollContainer>
246            </div>
247          )}
248        </div>
249      </div>
250    );
251  }
252
253  private scrollHandler = () => {
254    this.props.onContentScroll && this.props.onContentScroll(this.getContentScrollTop());
255  };
256}
257