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  overflow-y: scroll;
108  overflow-x: hidden;
109  -webkit-overflow-scrolling: touch;
110
111  /* width */
112  ::-webkit-scrollbar {
113    width: 6px;
114  }
115
116  /* Track */
117  ::-webkit-scrollbar-track {
118    background: transparent;
119    cursor: pointer;
120  }
121
122  /* Handle */
123  ::-webkit-scrollbar-thumb {
124    background: ${theme.background.tertiary};
125  }
126
127  /* Handle on hover */
128  ::-webkit-scrollbar-thumb:hover {
129    background: ${theme.background.quaternary};
130  }
131
132  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
133    overflow-y: auto;
134  }
135`;
136
137const STYLES_CENTER_WRAPPER = css`
138  max-width: 1200px;
139  margin: auto;
140`;
141
142const STYLES_HIDDEN = css`
143  display: none;
144`;
145
146type ScrollContainerProps = React.PropsWithChildren<{
147  className?: string;
148  scrollPosition?: number;
149  scrollHandler?: () => void;
150}>;
151
152class ScrollContainer extends React.Component<ScrollContainerProps> {
153  scrollRef = React.createRef<HTMLDivElement>();
154
155  componentDidMount() {
156    if (this.props.scrollPosition && this.scrollRef.current) {
157      this.scrollRef.current.scrollTop = this.props.scrollPosition;
158    }
159  }
160
161  public getScrollTop = () => {
162    return this.scrollRef.current?.scrollTop ?? 0;
163  };
164
165  public getScrollRef = () => {
166    return this.scrollRef;
167  };
168
169  render() {
170    return (
171      <div
172        css={STYLES_SCROLL_CONTAINER}
173        className={this.props.className}
174        ref={this.scrollRef}
175        onScroll={this.props.scrollHandler}>
176        {this.props.children}
177      </div>
178    );
179  }
180}
181
182type Props = React.PropsWithChildren<{
183  onContentScroll?: (scrollTop: number) => void;
184  isMobileMenuVisible: boolean;
185  tocVisible: boolean;
186  header: React.ReactNode;
187  sidebarScrollPosition: number;
188  sidebar: React.ReactNode;
189  sidebarActiveGroup: string;
190  sidebarRight: React.ReactElement;
191}>;
192
193export default class DocumentationNestedScrollLayout extends React.Component<Props> {
194  static defaultProps = {
195    sidebarScrollPosition: 0,
196  };
197
198  sidebarRef = React.createRef<ScrollContainer>();
199  contentRef = React.createRef<ScrollContainer>();
200  sidebarRightRef = React.createRef<ScrollContainer>();
201
202  public getSidebarScrollTop = () => {
203    return this.sidebarRef.current?.getScrollTop() ?? 0;
204  };
205
206  public getContentScrollTop = () => {
207    return this.contentRef.current?.getScrollTop() ?? 0;
208  };
209
210  render() {
211    const {
212      header,
213      sidebar,
214      sidebarActiveGroup,
215      sidebarRight,
216      sidebarScrollPosition,
217      isMobileMenuVisible,
218      tocVisible,
219      children,
220    } = this.props;
221
222    return (
223      <div css={STYLES_CONTAINER}>
224        <div css={STYLES_HEADER}>{header}</div>
225        <div css={STYLES_CONTENT}>
226          <div css={[STYLES_SIDEBAR, STYLES_LEFT]}>
227            <SidebarHead sidebarActiveGroup={sidebarActiveGroup} />
228            <ScrollContainer ref={this.sidebarRef} scrollPosition={sidebarScrollPosition}>
229              {sidebar}
230            </ScrollContainer>
231          </div>
232          <div css={[STYLES_CENTER, isMobileMenuVisible && STYLES_HIDDEN]}>
233            <ScrollContainer ref={this.contentRef} scrollHandler={this.scrollHandler}>
234              <div css={STYLES_CENTER_WRAPPER}>{children}</div>
235            </ScrollContainer>
236          </div>
237          {tocVisible && (
238            <div css={[STYLES_SIDEBAR, STYLES_RIGHT]}>
239              <ScrollContainer ref={this.sidebarRightRef}>
240                {React.cloneElement(sidebarRight, {
241                  selfRef: this.sidebarRightRef,
242                  contentRef: this.contentRef,
243                })}
244              </ScrollContainer>
245            </div>
246          )}
247        </div>
248      </div>
249    );
250  }
251
252  private scrollHandler = () => {
253    this.props.onContentScroll && this.props.onContentScroll(this.getContentScrollTop());
254  };
255}
256