1586106d6SBartłomiej Klocek// NOTE(jim):
2586106d6SBartłomiej Klocek// GETTING NESTED SCROLL RIGHT IS DELICATE BUSINESS. THEREFORE THIS COMPONENT
3586106d6SBartłomiej Klocek// IS THE ONLY PLACE WHERE SCROLL CODE SHOULD BE HANDLED. THANKS.
440836478SBartosz Kaszubowskiimport { css } from '@emotion/react';
5*f4b1168bSBartosz Kaszubowskiimport { theme } from '@expo/styleguide';
6*f4b1168bSBartosz Kaszubowskiimport { breakpoints } from '@expo/styleguide-base';
7586106d6SBartłomiej Klocekimport * as React from 'react';
8586106d6SBartłomiej Klocek
9b4bb5e4dSBartosz Kaszubowskiimport { SidebarHead, SidebarFooter } from '~/ui/components/Sidebar';
10586106d6SBartłomiej Klocek
11586106d6SBartłomiej Klocekconst STYLES_CONTAINER = css`
12586106d6SBartłomiej Klocek  width: 100%;
13586106d6SBartłomiej Klocek  height: 100vh;
14586106d6SBartłomiej Klocek  overflow: hidden;
15586106d6SBartłomiej Klocek  margin: 0 auto 0 auto;
168bf05203SJon Samp  border-right: 1px solid ${theme.border.default};
178bf05203SJon Samp  background: ${theme.background.default};
18586106d6SBartłomiej Klocek
19586106d6SBartłomiej Klocek  display: flex;
20586106d6SBartłomiej Klocek  align-items: center;
21586106d6SBartłomiej Klocek  justify-content: space-between;
22586106d6SBartłomiej Klocek  flex-direction: column;
23586106d6SBartłomiej Klocek
24586106d6SBartłomiej Klocek  @media screen and (max-width: 1440px) {
25586106d6SBartłomiej Klocek    border-left: 0px;
26586106d6SBartłomiej Klocek    border-right: 0px;
27586106d6SBartłomiej Klocek  }
28586106d6SBartłomiej Klocek
29d04fd43fSBartosz Kaszubowski  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
30586106d6SBartłomiej Klocek    display: block;
31586106d6SBartłomiej Klocek    height: auto;
32586106d6SBartłomiej Klocek  }
33586106d6SBartłomiej Klocek`;
34586106d6SBartłomiej Klocek
35586106d6SBartłomiej Klocekconst STYLES_HEADER = css`
36586106d6SBartłomiej Klocek  flex-shrink: 0;
37586106d6SBartłomiej Klocek  width: 100%;
38586106d6SBartłomiej Klocek
39d04fd43fSBartosz Kaszubowski  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
40586106d6SBartłomiej Klocek    position: sticky;
41586106d6SBartłomiej Klocek    top: -57px;
42586106d6SBartłomiej Klocek    z-index: 3;
43d04fd43fSBartosz Kaszubowski    max-height: 100vh;
44586106d6SBartłomiej Klocek  }
45586106d6SBartłomiej Klocek`;
46586106d6SBartłomiej Klocek
47586106d6SBartłomiej Klocekconst STYLES_CONTENT = css`
48586106d6SBartłomiej Klocek  display: flex;
49586106d6SBartłomiej Klocek  align-items: flex-start;
50586106d6SBartłomiej Klocek  margin: 0 auto;
51586106d6SBartłomiej Klocek  justify-content: space-between;
52586106d6SBartłomiej Klocek  width: 100%;
53586106d6SBartłomiej Klocek  height: 100%;
54586106d6SBartłomiej Klocek  min-height: 25%;
55586106d6SBartłomiej Klocek
56d04fd43fSBartosz Kaszubowski  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
57586106d6SBartłomiej Klocek    height: auto;
58586106d6SBartłomiej Klocek  }
59586106d6SBartłomiej Klocek`;
60586106d6SBartłomiej Klocek
61586106d6SBartłomiej Klocekconst STYLES_SIDEBAR = css`
62d04fd43fSBartosz Kaszubowski  display: flex;
63d04fd43fSBartosz Kaszubowski  flex-direction: column;
64586106d6SBartłomiej Klocek  flex-shrink: 0;
65586106d6SBartłomiej Klocek  max-width: 280px;
66586106d6SBartłomiej Klocek  height: 100%;
67586106d6SBartłomiej Klocek  overflow: hidden;
68586106d6SBartłomiej Klocek  transition: 200ms ease max-width;
69d04fd43fSBartosz Kaszubowski  background: ${theme.background.default};
70586106d6SBartłomiej Klocek
71586106d6SBartłomiej Klocek  @media screen and (max-width: 1200px) {
72586106d6SBartłomiej Klocek    max-width: 280px;
73586106d6SBartłomiej Klocek  }
74586106d6SBartłomiej Klocek
75d04fd43fSBartosz Kaszubowski  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
76586106d6SBartłomiej Klocek    display: none;
77586106d6SBartłomiej Klocek  }
78586106d6SBartłomiej Klocek`;
79586106d6SBartłomiej Klocek
80586106d6SBartłomiej Klocekconst STYLES_LEFT = css`
818bf05203SJon Samp  border-right: 1px solid ${theme.border.default};
82586106d6SBartłomiej Klocek`;
83586106d6SBartłomiej Klocek
84586106d6SBartłomiej Klocekconst STYLES_RIGHT = css`
858bf05203SJon Samp  border-left: 1px solid ${theme.border.default};
868bf05203SJon Samp  background-color: ${theme.background.default};
87586106d6SBartłomiej Klocek`;
88586106d6SBartłomiej Klocek
89586106d6SBartłomiej Klocekconst STYLES_CENTER = css`
908bf05203SJon Samp  background: ${theme.background.default};
91586106d6SBartłomiej Klocek  min-width: 5%;
92586106d6SBartłomiej Klocek  width: 100%;
93586106d6SBartłomiej Klocek  height: 100%;
94586106d6SBartłomiej Klocek  overflow: hidden;
95586106d6SBartłomiej Klocek  display: flex;
96586106d6SBartłomiej Klocek
97d04fd43fSBartosz Kaszubowski  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
98586106d6SBartłomiej Klocek    height: auto;
99586106d6SBartłomiej Klocek    overflow: auto;
100586106d6SBartłomiej Klocek  }
101586106d6SBartłomiej Klocek`;
102586106d6SBartłomiej Klocek
103586106d6SBartłomiej Klocek// NOTE(jim):
104586106d6SBartłomiej Klocek// All the other components tame the UI. this one allows a container to scroll.
105586106d6SBartłomiej Klocekconst STYLES_SCROLL_CONTAINER = css`
106586106d6SBartłomiej Klocek  height: 100%;
107586106d6SBartłomiej Klocek  width: 100%;
108586106d6SBartłomiej Klocek  overflow-y: scroll;
109586106d6SBartłomiej Klocek  overflow-x: hidden;
110586106d6SBartłomiej Klocek  -webkit-overflow-scrolling: touch;
111586106d6SBartłomiej Klocek
112586106d6SBartłomiej Klocek  /* width */
113586106d6SBartłomiej Klocek  ::-webkit-scrollbar {
114586106d6SBartłomiej Klocek    width: 6px;
115586106d6SBartłomiej Klocek  }
116586106d6SBartłomiej Klocek
117586106d6SBartłomiej Klocek  /* Track */
118586106d6SBartłomiej Klocek  ::-webkit-scrollbar-track {
1198bf05203SJon Samp    background: transparent;
120586106d6SBartłomiej Klocek    cursor: pointer;
121586106d6SBartłomiej Klocek  }
122586106d6SBartłomiej Klocek
123586106d6SBartłomiej Klocek  /* Handle */
124586106d6SBartłomiej Klocek  ::-webkit-scrollbar-thumb {
125be43ea08SBartosz Kaszubowski    background: ${theme.palette.gray5};
126586106d6SBartłomiej Klocek  }
127586106d6SBartłomiej Klocek
128586106d6SBartłomiej Klocek  /* Handle on hover */
129586106d6SBartłomiej Klocek  ::-webkit-scrollbar-thumb:hover {
130be43ea08SBartosz Kaszubowski    background: ${theme.palette.gray6};
131586106d6SBartłomiej Klocek  }
132586106d6SBartłomiej Klocek
133d04fd43fSBartosz Kaszubowski  @media screen and (max-width: ${(breakpoints.medium + breakpoints.large) / 2}px) {
134586106d6SBartłomiej Klocek    overflow-y: auto;
135586106d6SBartłomiej Klocek  }
136586106d6SBartłomiej Klocek`;
137586106d6SBartłomiej Klocek
138586106d6SBartłomiej Klocekconst STYLES_CENTER_WRAPPER = css`
139586106d6SBartłomiej Klocek  max-width: 1200px;
140586106d6SBartłomiej Klocek  margin: auto;
141586106d6SBartłomiej Klocek`;
142586106d6SBartłomiej Klocek
143d04fd43fSBartosz Kaszubowskiconst STYLES_HIDDEN = css`
144d04fd43fSBartosz Kaszubowski  display: none;
145d04fd43fSBartosz Kaszubowski`;
146d04fd43fSBartosz Kaszubowski
147b8b69ebeSBartosz Kaszubowskitype ScrollContainerProps = React.PropsWithChildren<{
148d04fd43fSBartosz Kaszubowski  className?: string;
149586106d6SBartłomiej Klocek  scrollPosition?: number;
150586106d6SBartłomiej Klocek  scrollHandler?: () => void;
151b8b69ebeSBartosz Kaszubowski}>;
152586106d6SBartłomiej Klocek
153586106d6SBartłomiej Klocekclass ScrollContainer extends React.Component<ScrollContainerProps> {
154586106d6SBartłomiej Klocek  scrollRef = React.createRef<HTMLDivElement>();
155586106d6SBartłomiej Klocek
156586106d6SBartłomiej Klocek  componentDidMount() {
157586106d6SBartłomiej Klocek    if (this.props.scrollPosition && this.scrollRef.current) {
158586106d6SBartłomiej Klocek      this.scrollRef.current.scrollTop = this.props.scrollPosition;
159586106d6SBartłomiej Klocek    }
160586106d6SBartłomiej Klocek  }
161586106d6SBartłomiej Klocek
162586106d6SBartłomiej Klocek  public getScrollTop = () => {
163586106d6SBartłomiej Klocek    return this.scrollRef.current?.scrollTop ?? 0;
164586106d6SBartłomiej Klocek  };
165586106d6SBartłomiej Klocek
166586106d6SBartłomiej Klocek  public getScrollRef = () => {
167586106d6SBartłomiej Klocek    return this.scrollRef;
168586106d6SBartłomiej Klocek  };
169586106d6SBartłomiej Klocek
170586106d6SBartłomiej Klocek  render() {
171586106d6SBartłomiej Klocek    return (
172d04fd43fSBartosz Kaszubowski      <div
173d04fd43fSBartosz Kaszubowski        css={STYLES_SCROLL_CONTAINER}
174d04fd43fSBartosz Kaszubowski        className={this.props.className}
175d04fd43fSBartosz Kaszubowski        ref={this.scrollRef}
176d04fd43fSBartosz Kaszubowski        onScroll={this.props.scrollHandler}>
177586106d6SBartłomiej Klocek        {this.props.children}
178586106d6SBartłomiej Klocek      </div>
179586106d6SBartłomiej Klocek    );
180586106d6SBartłomiej Klocek  }
181586106d6SBartłomiej Klocek}
182586106d6SBartłomiej Klocek
183b8b69ebeSBartosz Kaszubowskitype Props = React.PropsWithChildren<{
184586106d6SBartłomiej Klocek  onContentScroll?: (scrollTop: number) => void;
185d04fd43fSBartosz Kaszubowski  isMobileMenuVisible: boolean;
186586106d6SBartłomiej Klocek  tocVisible: boolean;
187586106d6SBartłomiej Klocek  header: React.ReactNode;
188586106d6SBartłomiej Klocek  sidebarScrollPosition: number;
189586106d6SBartłomiej Klocek  sidebar: React.ReactNode;
190d04fd43fSBartosz Kaszubowski  sidebarActiveGroup: string;
191586106d6SBartłomiej Klocek  sidebarRight: React.ReactElement;
192b8b69ebeSBartosz Kaszubowski}>;
193586106d6SBartłomiej Klocek
194586106d6SBartłomiej Klocekexport default class DocumentationNestedScrollLayout extends React.Component<Props> {
195586106d6SBartłomiej Klocek  static defaultProps = {
196586106d6SBartłomiej Klocek    sidebarScrollPosition: 0,
197586106d6SBartłomiej Klocek  };
198586106d6SBartłomiej Klocek
199586106d6SBartłomiej Klocek  sidebarRef = React.createRef<ScrollContainer>();
200586106d6SBartłomiej Klocek  contentRef = React.createRef<ScrollContainer>();
201586106d6SBartłomiej Klocek  sidebarRightRef = React.createRef<ScrollContainer>();
202586106d6SBartłomiej Klocek
203586106d6SBartłomiej Klocek  public getSidebarScrollTop = () => {
204586106d6SBartłomiej Klocek    return this.sidebarRef.current?.getScrollTop() ?? 0;
205586106d6SBartłomiej Klocek  };
206586106d6SBartłomiej Klocek
207586106d6SBartłomiej Klocek  public getContentScrollTop = () => {
208586106d6SBartłomiej Klocek    return this.contentRef.current?.getScrollTop() ?? 0;
209586106d6SBartłomiej Klocek  };
210586106d6SBartłomiej Klocek
211586106d6SBartłomiej Klocek  render() {
212d04fd43fSBartosz Kaszubowski    const {
213d04fd43fSBartosz Kaszubowski      header,
214d04fd43fSBartosz Kaszubowski      sidebar,
215d04fd43fSBartosz Kaszubowski      sidebarActiveGroup,
216d04fd43fSBartosz Kaszubowski      sidebarRight,
217d04fd43fSBartosz Kaszubowski      sidebarScrollPosition,
218d04fd43fSBartosz Kaszubowski      isMobileMenuVisible,
219d04fd43fSBartosz Kaszubowski      tocVisible,
220d04fd43fSBartosz Kaszubowski      children,
221d04fd43fSBartosz Kaszubowski    } = this.props;
222586106d6SBartłomiej Klocek
223586106d6SBartłomiej Klocek    return (
224586106d6SBartłomiej Klocek      <div css={STYLES_CONTAINER}>
225d04fd43fSBartosz Kaszubowski        <div css={STYLES_HEADER}>{header}</div>
226586106d6SBartłomiej Klocek        <div css={STYLES_CONTENT}>
227586106d6SBartłomiej Klocek          <div css={[STYLES_SIDEBAR, STYLES_LEFT]}>
228d04fd43fSBartosz Kaszubowski            <SidebarHead sidebarActiveGroup={sidebarActiveGroup} />
229586106d6SBartłomiej Klocek            <ScrollContainer ref={this.sidebarRef} scrollPosition={sidebarScrollPosition}>
230d04fd43fSBartosz Kaszubowski              {sidebar}
231b4bb5e4dSBartosz Kaszubowski              <SidebarFooter />
232586106d6SBartłomiej Klocek            </ScrollContainer>
233586106d6SBartłomiej Klocek          </div>
234d04fd43fSBartosz Kaszubowski          <div css={[STYLES_CENTER, isMobileMenuVisible && STYLES_HIDDEN]}>
235586106d6SBartłomiej Klocek            <ScrollContainer ref={this.contentRef} scrollHandler={this.scrollHandler}>
236d04fd43fSBartosz Kaszubowski              <div css={STYLES_CENTER_WRAPPER}>{children}</div>
237586106d6SBartłomiej Klocek            </ScrollContainer>
238586106d6SBartłomiej Klocek          </div>
239d04fd43fSBartosz Kaszubowski          {tocVisible && (
240586106d6SBartłomiej Klocek            <div css={[STYLES_SIDEBAR, STYLES_RIGHT]}>
241586106d6SBartłomiej Klocek              <ScrollContainer ref={this.sidebarRightRef}>
242d04fd43fSBartosz Kaszubowski                {React.cloneElement(sidebarRight, {
243586106d6SBartłomiej Klocek                  selfRef: this.sidebarRightRef,
244586106d6SBartłomiej Klocek                  contentRef: this.contentRef,
245586106d6SBartłomiej Klocek                })}
246586106d6SBartłomiej Klocek              </ScrollContainer>
247586106d6SBartłomiej Klocek            </div>
248586106d6SBartłomiej Klocek          )}
249586106d6SBartłomiej Klocek        </div>
250586106d6SBartłomiej Klocek      </div>
251586106d6SBartłomiej Klocek    );
252586106d6SBartłomiej Klocek  }
253586106d6SBartłomiej Klocek
254586106d6SBartłomiej Klocek  private scrollHandler = () => {
255586106d6SBartłomiej Klocek    this.props.onContentScroll && this.props.onContentScroll(this.getContentScrollTop());
256586106d6SBartłomiej Klocek  };
257586106d6SBartłomiej Klocek}
258