import React from 'react'; import {browserHistory, RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import Button from 'app/components/button'; import {IconClose, IconMenu} from 'app/icons'; import {t} from 'app/locale'; import {fadeIn, slideInLeft} from 'app/styles/animations'; import space from 'app/styles/space'; import SettingsBreadcrumb from './settingsBreadcrumb'; import SettingsHeader from './settingsHeader'; import SettingsSearch from './settingsSearch'; type Props = { renderNavigation?: () => React.ReactNode; children: React.ReactNode; } & RouteComponentProps<{}, {}>; type State = { /** * This is used when the screen is small enough that the navigation should * be hidden. On large screens this state will end up unused. */ navVisible: boolean; /** * Offset mobile settings navigation by the height of main navigation, * settings breadcrumbs and optional warnings. */ navOffsetTop: number; }; class SettingsLayout extends React.Component { state: State = { navVisible: false, navOffsetTop: 0, }; componentDidMount() { // Close the navigation when navigating. this.unlisten = browserHistory.listen(() => this.toggleNav(false)); } componentWillUnmount() { this.unlisten(); } unlisten!: () => void; headerRef = React.createRef(); toggleNav(navVisible: boolean) { // when the navigation is opened, body should be scroll-locked this.toggleBodyScrollLock(navVisible); this.setState({ navOffsetTop: this.headerRef.current?.getBoundingClientRect().bottom ?? 0, navVisible, }); } toggleBodyScrollLock(lock: boolean) { const bodyElement = document.getElementsByTagName('body')[0]; if (window.scrollTo) { window.scrollTo(0, 0); } bodyElement.classList[lock ? 'add' : 'remove']('scroll-lock'); } render() { const {params, routes, route, renderNavigation, children} = this.props; const {navVisible, navOffsetTop} = this.state; // We want child's view's props const childProps = children && React.isValidElement(children) ? children.props : this.props; const childRoutes = childProps.routes || routes || []; const childRoute = childProps.route || route || {}; const shouldRenderNavigation = typeof renderNavigation === 'function'; return ( {shouldRenderNavigation && ( : } onClick={() => this.toggleNav(!navVisible)} /> )} {shouldRenderNavigation && ( {renderNavigation!()} )} this.toggleNav(false)} /> {children} ); } } const SettingsColumn = styled('div')` display: flex; flex-direction: column; flex: 1; /* so this stretches vertically so that footer is fixed at bottom */ min-width: 0; /* fixes problem when child content stretches beyond layout width */ footer { margin-top: 0; } `; const HeaderContent = styled('div')` display: flex; align-items: center; justify-content: space-between; `; const NavMenuToggle = styled(Button)` display: none; margin: -${space(1)} ${space(1)} -${space(1)} -${space(1)}; padding: ${space(1)}; color: ${p => p.theme.subText}; &:hover, &:focus, &:active { color: ${p => p.theme.textColor}; } @media (max-width: ${p => p.theme.breakpoints[0]}) { display: block; } `; const StyledSettingsBreadcrumb = styled(SettingsBreadcrumb)` flex: 1; `; const MaxWidthContainer = styled('div')` display: flex; max-width: ${p => p.theme.settings.containerWidth}; flex: 1; `; const SidebarWrapper = styled('div')<{isVisible: boolean; offsetTop: number}>` flex-shrink: 0; width: ${p => p.theme.settings.sidebarWidth}; background: ${p => p.theme.background}; border-right: 1px solid ${p => p.theme.border}; @media (max-width: ${p => p.theme.breakpoints[0]}) { display: ${p => (p.isVisible ? 'block' : 'none')}; position: fixed; top: ${p => p.offsetTop}px; bottom: 0; overflow-y: auto; animation: ${slideInLeft} 100ms ease-in-out; z-index: ${p => p.theme.zIndex.settingsSidebarNav}; box-shadow: ${p => p.theme.dropShadowHeavy}; } `; const NavMask = styled('div')<{isVisible: boolean}>` display: none; @media (max-width: ${p => p.theme.breakpoints[0]}) { display: ${p => (p.isVisible ? 'block' : 'none')}; background: rgba(0, 0, 0, 0.35); height: 100%; width: 100%; position: absolute; z-index: ${p => p.theme.zIndex.settingsSidebarNavMask}; animation: ${fadeIn} 250ms ease-in-out; } `; /** * Note: `overflow: hidden` will cause some buttons in `SettingsPageHeader` to be cut off because it has negative margin. * Will also cut off tooltips. */ const Content = styled('div')` flex: 1; padding: ${space(4)}; min-width: 0; /* keep children from stretching container */ `; export default SettingsLayout;