123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- import * as React from 'react';
- import Icon from '../Icon/Icon';
- // Please keep in sync with $menu-transition-duration variable in `client/scss/settings/_variables.scss`
- export const SIDEBAR_TRANSITION_DURATION = 150;
- export interface Strings {
- DASHBOARD: string;
- EDIT_YOUR_ACCOUNT: string;
- SEARCH: string;
- TOGGLE_SIDEBAR: string;
- MAIN_MENU: string;
- }
- export interface ModuleRenderContext {
- key: number;
- slim: boolean;
- expandingOrCollapsing: boolean;
- onAccountExpand: () => void;
- onSearchClick: () => void;
- currentPath: string;
- strings: Strings;
- navigate(url: string): Promise<void>;
- }
- export interface ModuleDefinition {
- render(context: ModuleRenderContext): React.ReactFragment;
- }
- export interface SidebarProps {
- modules: ModuleDefinition[];
- currentPath: string;
- strings: Strings;
- collapsedOnLoad: boolean;
- navigate(url: string): Promise<void>;
- onExpandCollapse?(collapsed: boolean);
- }
- export const Sidebar: React.FunctionComponent<SidebarProps> = ({
- modules,
- currentPath,
- collapsedOnLoad = false,
- strings,
- navigate,
- onExpandCollapse,
- }) => {
- // 'collapsed' is a persistent state that is controlled by the arrow icon at the top
- // It records the user's general preference for a collapsed/uncollapsed menu
- // This is just a hint though, and we may still collapse the menu if the screen is too small
- const [collapsed, setCollapsed] = React.useState(collapsedOnLoad);
- // Call onExpandCollapse(true) if menu is initialised in collapsed state
- React.useEffect(() => {
- if (collapsed && onExpandCollapse) {
- onExpandCollapse(true);
- }
- }, []);
- // 'visibleOnMobile' indicates whether the sidebar is currently visible on mobile
- // On mobile, the sidebar is completely hidden by default and must be opened manually
- const [visibleOnMobile, setVisibleOnMobile] = React.useState(false);
- // Tracks whether the screen is below 800 pixels. In this state, the menu is completely hidden.
- // State is used here in case the user changes their browser size
- const checkWindowSizeIsMobile = () => window.innerWidth < 800;
- const [isMobile, setIsMobile] = React.useState(checkWindowSizeIsMobile());
- React.useEffect(() => {
- function handleResize() {
- if (checkWindowSizeIsMobile()) {
- setIsMobile(true);
- } else {
- setIsMobile(false);
- // Close the menu as this state is not used in desktop
- setVisibleOnMobile(false);
- }
- }
- window.addEventListener('resize', handleResize);
- handleResize();
- return () => window.removeEventListener('resize', handleResize);
- }, []);
- // Whether or not to display the menu with slim layout.
- // Separate from 'collapsed' as the menu can still be displayed with an expanded
- // layout while in 'collapsed' mode if the user is 'peeking' into it (see above)
- const slim = collapsed && !isMobile;
- // 'expandingOrCollapsing' is set to true whilst the the menu is transitioning between slim and expanded layouts
- const [expandingOrCollapsing, setExpandingOrCollapsing] =
- React.useState(false);
- React.useEffect(() => {
- setExpandingOrCollapsing(true);
- const finishTimeout = setTimeout(() => {
- setExpandingOrCollapsing(false);
- }, SIDEBAR_TRANSITION_DURATION);
- return () => {
- clearTimeout(finishTimeout);
- };
- }, [slim]);
- const onClickCollapseToggle = () => {
- setCollapsed(!collapsed);
- if (onExpandCollapse) {
- onExpandCollapse(!collapsed);
- }
- };
- const onClickOpenCloseToggle = () => {
- setVisibleOnMobile(!visibleOnMobile);
- setExpandingOrCollapsing(true);
- const finishTimeout = setTimeout(() => {
- setExpandingOrCollapsing(false);
- }, SIDEBAR_TRANSITION_DURATION);
- return () => {
- clearTimeout(finishTimeout);
- };
- };
- const [focused, setFocused] = React.useState(false);
- const onBlurHandler = () => {
- if (focused) {
- setFocused(false);
- setCollapsed(true);
- }
- };
- const onFocusHandler = () => {
- if (focused) {
- setCollapsed(false);
- setFocused(true);
- }
- };
- const onSearchClick = () => {
- if (slim) {
- onClickCollapseToggle();
- }
- };
- const onAccountExpand = () => {
- if (slim) {
- onClickCollapseToggle();
- }
- };
- // Render modules
- const renderedModules = modules.map((module, index) =>
- module.render({
- key: index,
- slim,
- expandingOrCollapsing,
- onAccountExpand,
- onSearchClick,
- currentPath,
- strings,
- navigate,
- }),
- );
- return (
- <>
- <div
- className={
- 'sidebar' +
- (slim ? ' sidebar--slim' : '') +
- (isMobile ? ' sidebar--mobile' : '') +
- (isMobile && !visibleOnMobile ? ' sidebar--hidden' : '')
- }
- >
- <div
- className="sidebar__inner"
- onFocus={onFocusHandler}
- onBlur={onBlurHandler}
- >
- <div
- className={`${
- slim ? 'w-justify-center' : 'w-justify-end'
- } w-flex w-items-center`}
- >
- <button
- onClick={onClickCollapseToggle}
- aria-label={strings.TOGGLE_SIDEBAR}
- aria-expanded={slim ? 'false' : 'true'}
- type="button"
- className={`
- ${!slim ? 'w-mr-4' : ''}
- button
- sidebar__collapse-toggle
- w-flex
- w-justify-center
- w-items-center
- sm:w-mt-4
- hover:w-bg-primary-200
- hover:text-white
- hover:opacity-100`}
- >
- <Icon
- name="expand-right"
- className={`w-transition motion-reduce:w-transition-none
- ${!collapsed ? '-w-rotate-180' : ''}
- `}
- />
- </button>
- </div>
- {renderedModules}
- </div>
- </div>
- <button
- onClick={onClickOpenCloseToggle}
- aria-label={strings.TOGGLE_SIDEBAR}
- aria-expanded={visibleOnMobile ? 'true' : 'false'}
- className={
- 'button sidebar-nav-toggle' +
- (isMobile ? ' sidebar-nav-toggle--mobile' : '') +
- (visibleOnMobile ? ' sidebar-nav-toggle--open' : '')
- }
- type="button"
- >
- {visibleOnMobile ? <Icon name="cross" /> : <Icon name="bars" />}
- </button>
- </>
- );
- };
|