UNPKG

aws-northstar

Version:
256 lines (252 loc) 13.2 kB
/** ******************************************************************************************************************* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. * ******************************************************************************************************************** */ import React, { createContext, useContext, useState, useLayoutEffect, useRef, useCallback, useEffect, useMemo, } from 'react'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import clsx from 'clsx'; import useLocalStorage from 'react-use-localstorage'; import MenuIcon from '@material-ui/icons/Menu'; import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; import IconButton from '@material-ui/core/IconButton'; import Box from '../../layouts/Box'; import Sidebar, { SidebarType } from './components/Sidebar'; import SplitPanel from './components/SplitPanel'; import Stack from '../../layouts/Stack'; import Overlay from '../../components/Overlay'; import Flashbar from '../../components/Flashbar'; import useScrollPosition from '../../hooks/useScrollPosition'; import LoadingIndicator from '../../components/LoadingIndicator'; import { LOCAL_STORAGE_KEY_SIDE_NAV_OPEN, LOCAL_STORAGE_KEY_HELP_PANEL_OPEN } from './constants'; const useStyles = makeStyles((theme) => ({ root: { margin: '0', overflow: 'hidden', }, flashbarContainer: { margin: theme.spacing(-4, -4, 2, -4), }, breadcrumbsContainer: { display: 'none', [theme.breakpoints.up('sm')]: { display: 'block', }, }, main: { display: 'flex', height: ({ headerHeightInPx }) => `calc(100vh - ${headerHeightInPx}px)`, }, contentArea: ({ hasSideNavigation, isSideNavigationOpen, hasHelpPanel, isHelpPanelOpen }) => ({ marginTop: 0, marginBottom: 0, height: '100%', position: 'relative', flexGrow: 1, boxSizing: 'border-box', overflow: 'auto', marginLeft: hasSideNavigation && !isSideNavigationOpen ? -WIDTH_SIDEBAR : 0, marginRight: hasHelpPanel && !isHelpPanelOpen ? -WIDTH_HELP_PANEL : 0, }), content: { flexGrow: 1, overflow: 'auto', }, notifications: ({ mainContentScrollPosition }) => ({ position: 'absolute', top: mainContentScrollPosition.y || 0, left: mainContentScrollPosition.x || 0, right: 0, zIndex: theme.zIndex.modal, transition: 'all 0.5s linear', }), contentPadding: { [theme.breakpoints.up('sm')]: { padding: `${theme.spacing(2)}px ${theme.spacing(4)}px`, }, }, mainContent: ({ notificationsBoxHeight }) => ({ marginTop: notificationsBoxHeight, '&:focus': { outline: 'none', }, }), menu: { [theme.breakpoints.down('xs')]: { position: 'absolute', }, [theme.breakpoints.up('sm')]: { position: 'absolute', top: '20px', paddingRight: 0, }, }, menuBar: { display: 'flex', boxShadow: '0 2px 1px -1px rgba(0,28,36,.3)', }, menuBarIcon: { padding: theme.spacing(2), }, menuBarNavIcon: { flexShrink: 1, borderRight: `1px solid ${theme.palette.grey['400']}`, }, menuBarInfoIcon: { flexShrink: 1, borderLeft: `1px solid ${theme.palette.grey['400']}`, }, })); const WIDTH_SIDEBAR = 285; const WIDTH_HELP_PANEL = 285; const initialState = { openHelpPanel: () => { }, setHelpPanelContent: () => { }, openSplitPanel: () => { }, setSplitPanelContent: () => { }, setDefaultSplitPanelHeight: (height) => { }, addNotification: () => { }, dismissNotifications: () => { }, }; const AppLayoutContext = createContext(initialState); /** * Basic layout for application, with place holder for header, navigation area, content area, breadcrumbs and tools/help panel. * It should be placed as the top most component in main content area. There should not be any spacing around it, it consumes * 100% of the width and height of the main content area, providing its own scrolling behavior. * By default it comes with a padding inside the content region. It can be removed by setting prop paddingContentArea == false. */ const AppLayout = ({ children, header, navigation, helpPanel, splitPanel, breadcrumbs, paddingContentArea = true, maxNotifications = 2, inProgress = false, notifications: notificationsProp, headerHeightInPx = 65, }) => { const [helpPanelContent, setHelpPanelContent] = useState(helpPanel); const [splitPanelContent, setSplitPanelContent] = useState(splitPanel); const [notifications, setNotifications] = useState([]); const [isSideNavigationOpen, setIsSideNavigationOpen] = useLocalStorage(LOCAL_STORAGE_KEY_SIDE_NAV_OPEN, 'false'); const [isHelpPanelOpen, setIsHelpPanelOpen] = useLocalStorage(LOCAL_STORAGE_KEY_HELP_PANEL_OPEN, 'false'); const [isSplitPanelOpen, setIsSplitPanelOpen] = useState(!!splitPanel); const [defaultSplitPanelHeight, setDefaultSplitPanelHeight] = useState(); const notificationsBoxRef = useRef(null); const mainContentRef = useRef(null); const [notificationsBoxHeight, setNotificationsBoxHeight] = useState(0); const theme = useTheme(); const fullMode = useMediaQuery(theme.breakpoints.up('sm')); const [mainContentScrollPosition, setMainContentScrollPosition] = useState({ x: 0, y: 0, }); useLayoutEffect(() => { var _a; setNotificationsBoxHeight(((_a = notificationsBoxRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0); }, [notificationsBoxRef, notifications]); useEffect(() => { if (notificationsProp) { setNotifications((prevNotifications) => [ ...prevNotifications, ...notificationsProp.filter((np) => !prevNotifications.find((pn) => pn.id === np.id)), ]); } }, [notificationsProp]); const handleDismissNotification = useCallback((id) => { if (id) { setNotifications((prevNotifications) => { var _a; const notification = prevNotifications.find((n) => n.id === id); (_a = notification === null || notification === void 0 ? void 0 : notification.originalOnDismiss) === null || _a === void 0 ? void 0 : _a.call(notification); return prevNotifications.filter((n) => n.id !== id); }); } else { setNotifications((prevNotifications) => { prevNotifications.forEach((n) => { var _a; return (_a = n.originalOnDismiss) === null || _a === void 0 ? void 0 : _a.call(n); }); return []; }); } }, [setNotifications]); const handleAddNotification = useCallback((newNotification) => { setNotifications((prevNotifications) => { const allNotifications = [ Object.assign(Object.assign({}, newNotification), { originalOnDismiss: newNotification.onDismiss, onDismiss: () => handleDismissNotification(newNotification.id) }), ...prevNotifications, ]; if (allNotifications.length > maxNotifications) { allNotifications.slice(maxNotifications).forEach((n) => { var _a; return (_a = n.originalOnDismiss) === null || _a === void 0 ? void 0 : _a.call(n); }); } return allNotifications.slice(0, maxNotifications); }); }, [handleDismissNotification, setNotifications, maxNotifications]); const watchScroll = useMemo(() => { return notifications.length > 0; }, [notifications]); const { handleScroll } = useScrollPosition((position) => { setMainContentScrollPosition(position); }, mainContentRef, 200); const classes = useStyles({ hasSideNavigation: !!navigation, hasHelpPanel: !!helpPanelContent, isSideNavigationOpen: isSideNavigationOpen === 'true', isHelpPanelOpen: isHelpPanelOpen === 'true', inProgress, notificationsBoxHeight, mainContentScrollPosition, headerHeightInPx, }); const renderNavigationIcon = useCallback((rootClassname) => { return (React.createElement(IconButton, { color: "inherit", "aria-label": "open navigation drawer", "data-testid": "open-nav-drawer", onClick: () => setIsSideNavigationOpen('true'), classes: { root: rootClassname, label: classes.menu, } }, React.createElement(MenuIcon, null))); }, [classes, setIsSideNavigationOpen]); const renderInfoIcon = useCallback((rootClassname) => { return (React.createElement(IconButton, { color: "inherit", "aria-label": "open help panel drawer", "data-testid": "open-helppanel-drawer", onClick: () => setIsHelpPanelOpen('true'), classes: { root: rootClassname, label: classes.menu, } }, React.createElement(InfoOutlinedIcon, null))); }, [classes, setIsHelpPanelOpen]); const openHelpPanel = useCallback((open = true) => { setIsHelpPanelOpen(open.toString()); }, [setIsHelpPanelOpen]); const openSplitPanel = useCallback((open = true) => { setIsSplitPanelOpen(open); }, [setIsSplitPanelOpen]); return (React.createElement(Box, { className: classes.root }, React.createElement(AppLayoutContext.Provider, { value: { openHelpPanel, setHelpPanelContent, openSplitPanel, setSplitPanelContent, setDefaultSplitPanelHeight, addNotification: handleAddNotification, dismissNotifications: handleDismissNotification, } }, header, !fullMode && (navigation || helpPanelContent) && (React.createElement(Box, { className: classes.menuBar }, navigation && (React.createElement(Box, { className: classes.menuBarNavIcon }, renderNavigationIcon(classes.menuBarIcon))), React.createElement(Box, { width: "100%" }), helpPanelContent && (React.createElement(Box, { className: classes.menuBarInfoIcon }, renderInfoIcon(classes.menuBarIcon))))), React.createElement(Box, { className: classes.main }, navigation && (React.createElement(Sidebar, { sidebarWidth: WIDTH_SIDEBAR, isSidebarOpen: isSideNavigationOpen, setIsSidebarOpen: setIsSideNavigationOpen, type: SidebarType.SIDE_NAVIGATION, displayIcon: fullMode, renderIcon: renderNavigationIcon }, navigation)), React.createElement(Box, { display: "flex", flexDirection: "column", className: classes.contentArea }, React.createElement("div", { ref: mainContentRef, className: classes.content, onScroll: (watchScroll && handleScroll) || undefined }, notifications && notifications.length > 0 && (React.createElement("div", { ref: notificationsBoxRef, className: classes.notifications }, React.createElement(Flashbar, { items: notifications, maxItemsDisplayed: maxNotifications }))), React.createElement(Box, { tabIndex: 0, position: "relative", className: clsx(classes.mainContent, { [classes.contentPadding]: !!paddingContentArea, }) }, React.createElement(Stack, { spacing: "s" }, breadcrumbs && React.createElement(Box, { className: classes.breadcrumbsContainer }, breadcrumbs), React.createElement("main", null, children)), inProgress && (React.createElement(Overlay, null, React.createElement(LoadingIndicator, { size: "large" }))))), React.createElement(SplitPanel, { isSplitPanelOpen: isSplitPanelOpen, setIsSplitPanelOpen: setIsSplitPanelOpen, defaultSplitPanelHeight: defaultSplitPanelHeight, fullMode: fullMode }, splitPanelContent)), helpPanelContent && (React.createElement(Sidebar, { sidebarWidth: WIDTH_HELP_PANEL, isSidebarOpen: isHelpPanelOpen, setIsSidebarOpen: setIsHelpPanelOpen, type: SidebarType.HELP_PANEL, displayIcon: fullMode, renderIcon: renderInfoIcon }, helpPanelContent)))))); }; export const useAppLayoutContext = () => useContext(AppLayoutContext); export default AppLayout;