StandardAppLayout.tsx 5 KB
Newer Older
1
import React, { ReactNode, useEffect } from 'react';
Andrey Azov's avatar
Andrey Azov committed
2
3
4
import classNames from 'classnames';
import noop from 'lodash/noop';

5
6
7
import { BreakpointWidth } from 'src/global/globalConfig';
import usePrevious from 'src/shared/hooks/usePrevious';

Andrey Azov's avatar
Andrey Azov committed
8
9
10
11
12
13
14
15
16
17
import { ReactComponent as Chevron } from 'static/img/shared/chevron-right.svg';
import { ReactComponent as CloseIcon } from 'static/img/shared/close.svg';

import styles from './StandardAppLayout.scss';

enum SidebarModeToggleAction {
  OPEN = 'open',
  CLOSE = 'close'
}

18
19
20
21
22
export enum SidebarBehaviourType {
  PUSH = 'push',
  SLIDEOVER = 'slideover'
}

Andrey Azov's avatar
Andrey Azov committed
23
24
25
26
27
28
29
30
31
type SidebarModeToggleProps = {
  onClick: () => void;
  showAction: SidebarModeToggleAction;
};

type StandardAppLayoutProps = {
  mainContent: ReactNode;
  sidebarContent: ReactNode;
  sidebarToolstripContent?: ReactNode;
32
  sidebarNavigation: ReactNode;
Andrey Azov's avatar
Andrey Azov committed
33
  topbarContent: ReactNode;
34
  sidebarBehaviour: SidebarBehaviourType;
Andrey Azov's avatar
Andrey Azov committed
35
36
37
38
39
  isSidebarOpen: boolean;
  onSidebarToggle: () => void;
  isDrawerOpen: boolean;
  drawerContent?: ReactNode;
  onDrawerClose: () => void;
40
  viewportWidth: BreakpointWidth;
Andrey Azov's avatar
Andrey Azov committed
41
42
43
};

const StandardAppLayout = (props: StandardAppLayoutProps) => {
44
45
46
47
48
49
50
51
52
  // TODO: is there any way to run this smarter?
  // Ideally, it should run only once per app life cycle to check whether user is on small screen and close the sidebar if they are
  useEffect(() => {
    if (props.viewportWidth < BreakpointWidth.DESKTOP && props.isSidebarOpen) {
      props.onSidebarToggle();
    }
  }, []);

  const mainClassNames = classNames(
Andrey Azov's avatar
Andrey Azov committed
53
    styles.main,
54
55
56
57
58
59
60
61
62
63
    {
      [styles.mainDefault]:
        props.isSidebarOpen &&
        props.sidebarBehaviour === SidebarBehaviourType.PUSH
    },
    {
      [styles.mainFullWidth]:
        !props.isSidebarOpen ||
        props.sidebarBehaviour === SidebarBehaviourType.SLIDEOVER
    }
Andrey Azov's avatar
Andrey Azov committed
64
65
  );

66
67
68
  const shouldShowSidebarNavigation =
    props.viewportWidth > BreakpointWidth.LAPTOP || props.isSidebarOpen;

69
70
71
72
  const topbarClassnames = classNames(
    styles.topbar,
    { [styles.topbar_withSidebarNavigation]: shouldShowSidebarNavigation },
    { [styles.topbar_withoutSidebarNavigation]: !shouldShowSidebarNavigation }
Andrey Azov's avatar
Andrey Azov committed
73
74
  );

75
76
  const sidebarWrapperClassnames = useSidebarWrapperClassNames(props);

Andrey Azov's avatar
Andrey Azov committed
77
78
  return (
    <div className={styles.standardAppLayout}>
79
      <div className={topbarClassnames}>
Andrey Azov's avatar
Andrey Azov committed
80
        {props.topbarContent}
81
        {shouldShowSidebarNavigation && props.sidebarNavigation}
Andrey Azov's avatar
Andrey Azov committed
82
83
      </div>
      <div className={styles.mainWrapper}>
84
        <div className={mainClassNames}>{props.mainContent}</div>
Andrey Azov's avatar
Andrey Azov committed
85
86
        <div className={sidebarWrapperClassnames}>
          {props.isDrawerOpen && <DrawerWindow onClick={props.onDrawerClose} />}
87
          <div className={styles.sidebarToolstrip}>
Andrey Azov's avatar
Andrey Azov committed
88
89
            <SidebarModeToggle
              onClick={
90
91
92
                props.isDrawerOpen
                  ? () => props.onDrawerClose()
                  : () => props.onSidebarToggle()
Andrey Azov's avatar
Andrey Azov committed
93
94
95
96
97
98
99
              }
              showAction={
                props.isSidebarOpen
                  ? SidebarModeToggleAction.CLOSE
                  : SidebarModeToggleAction.OPEN
              }
            />
100
            <div className={styles.sidebarToolstripContent}>
Andrey Azov's avatar
Andrey Azov committed
101
102
103
              {props.sidebarToolstripContent}
            </div>
          </div>
104
          <div className={styles.sidebar}>{props.sidebarContent}</div>
Andrey Azov's avatar
Andrey Azov committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
          <div className={styles.drawer}>
            <CloseIcon
              className={styles.drawerClose}
              onClick={props.onDrawerClose}
            />
            {props.drawerContent || null}
          </div>
        </div>
      </div>
    </div>
  );
};

StandardAppLayout.defaultProps = {
  isDrawerOpen: false,
120
  sidebarBehaviour: SidebarBehaviourType.PUSH,
Andrey Azov's avatar
Andrey Azov committed
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
  onDrawerClose: noop
};

const SidebarModeToggle = (props: SidebarModeToggleProps) => {
  const chevronClasses = classNames(styles.sidebarModeToggleChevron, {
    [styles.sidebarModeToggleChevronOpen]:
      props.showAction === SidebarModeToggleAction.OPEN
  });

  return (
    <div className={styles.sidebarModeToggle}>
      <Chevron className={chevronClasses} onClick={props.onClick} />
    </div>
  );
};

// left-most transparent part of the drawer allowing the user to see what element is behind the drawer;
// when clicked, will close the drawer
const DrawerWindow = (props: { onClick: () => void }) => {
  return <div className={styles.drawerWindow} onClick={props.onClick} />;
};

143
144
145
146
147
148
149
150
const useSidebarWrapperClassNames = (props: StandardAppLayoutProps) => {
  const previousSidebarOpen = usePrevious(props.isSidebarOpen);
  // do not use transition for opening and closing of the sidebar
  const isInstantaneous =
    !props.isSidebarOpen || // <-- sidebar about to close
    (props.isSidebarOpen && !previousSidebarOpen); // <-- sidebar about to open

  return classNames(
151
152
153
    styles.sidebarWrapper,
    { [styles.sidebarWrapperOpen]: props.isSidebarOpen },
    { [styles.sidebarWrapperClosed]: !props.isSidebarOpen },
154
    {
155
      [styles.sidebarWrapperDrawerOpen]: props.isDrawerOpen ?? false
156
157
158
159
160
    },
    { [styles.instantaneous]: isInstantaneous }
  );
};

Andrey Azov's avatar
Andrey Azov committed
161
export default StandardAppLayout;