UNPKG

@octopusdeploy/design-system-components

Version:
264 lines (263 loc) • 16.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.GlobalLayout = GlobalLayout; const jsx_runtime_1 = require("react/jsx-runtime"); const css_1 = require("@emotion/css"); const material_1 = require("@mui/material"); const design_system_icons_1 = require("@octopusdeploy/design-system-icons"); const design_system_tokens_1 = require("@octopusdeploy/design-system-tokens"); const react_1 = __importStar(require("react")); const hooks_1 = require("../../hooks"); const breakpoints_1 = require("../../hooks/breakpoints"); const routing_1 = require("../../routing"); const utils_1 = require("../../utils"); const ScrollArea_1 = require("../ScrollArea"); const AreaNavList_1 = require("./AreaNavList"); const SideNavBar_1 = require("./SideNavBar"); const TopNavBar_1 = require("./TopNavBar"); // These z-indexes are local to GlobalLayout through use of isolation css const zIndexGlobalLayoutContent = 1; const zIndexAreaNavPanel = 2; function GlobalLayout({ topNavBar, sideNavBar, areaNavPanel, onSideBarClickAway, children, scrollAreaRef, helpPanel }) { const isLargerThanIpad = (0, breakpoints_1.useIsLargerThanIpadResolution)(); const [isSideDrawerExpanded, setIsSideDrawerExpanded] = (0, react_1.useState)(false); const [isAreaNavPanelExpanded, setIsAreaNavPanelExpanded] = (0, react_1.useState)(getDefaultAreaNavPanelExpandState(areaNavPanel.displayMode)); const [hoveredSideNavLink, setHoveredSideNavLink] = (0, react_1.useState)(); const [isAreaNavPanelHovered, setIsAreaNavPanelHovered] = (0, react_1.useState)(false); (0, react_1.useLayoutEffect)(() => { setIsAreaNavPanelExpanded(getDefaultAreaNavPanelExpandState(areaNavPanel.displayMode)); }, [areaNavPanel.displayMode]); const isUrlActive = (0, routing_1.useIsUrlActive)(); const expandSideDrawer = () => setIsSideDrawerExpanded(true); const collapseSideDrawer = () => setIsSideDrawerExpanded(false); const handleSideNavBarLinkClicked = (0, react_1.useCallback)((event, item) => { const isActive = isUrlActive(item.href, item.showLinkAsActive ?? "if path partially matches"); if (areaNavPanel.displayMode === "inline" && areaNavPanel.items.length > 0 && isActive) { // If the user is clicking tab because they can't see the AreaNavPanel, toggle // the AreaNavPanel to let them peek the menu contents. setIsAreaNavPanelExpanded((isExpanded) => !isExpanded); event?.preventDefault(); } else { // We're navigating to the Deploy area from somewhere else, so we always want to show the nav panel even if // it was collapsed in the other area. Expand it then fall through to the regular route/link onClick. setIsAreaNavPanelExpanded(true); } sideNavBar.onLinkClicked?.(item); setIsSideDrawerExpanded(false); }, [areaNavPanel, isUrlActive, sideNavBar]); const handleAreaNavPanelLinkClicked = (0, react_1.useCallback)((item) => { areaNavPanel.onLinkClicked?.(item); collapseSideDrawer(); }, [areaNavPanel]); (0, react_1.useEffect)(() => { if (!isLargerThanIpad || areaNavPanel.displayMode !== "overlay") return; const hasActiveHoveredSideNavLink = hoveredSideNavLink && isUrlActive(hoveredSideNavLink.href, hoveredSideNavLink.showLinkAsActive ?? "if path partially matches"); if (!isAreaNavPanelExpanded && hasActiveHoveredSideNavLink) { setIsAreaNavPanelExpanded(true); return; } if (!hasActiveHoveredSideNavLink && !isAreaNavPanelHovered) { const collapseTimeout = window.setTimeout(() => { setIsAreaNavPanelExpanded(false); }, 200); return () => { window.clearTimeout(collapseTimeout); }; } }, [areaNavPanel.displayMode, hoveredSideNavLink, isAreaNavPanelHovered, isLargerThanIpad, isUrlActive, isAreaNavPanelExpanded]); const mobileSideContainer = ((0, jsx_runtime_1.jsxs)(GlobalLayoutSideContainerDrawer, { isExpanded: isSideDrawerExpanded, onClose: collapseSideDrawer, onOpen: expandSideDrawer, children: [(0, jsx_runtime_1.jsx)(SideNavBar_1.SideNavBar, { logo: sideNavBar.logo, topItems: sideNavBar.topItems, bottomItems: sideNavBar.bottomItems, onLinkClicked: handleSideNavBarLinkClicked, onHoveredLinkChanged: setHoveredSideNavLink }), (0, jsx_runtime_1.jsx)(AreaNavPanel, { isCollapsible: false, listItems: areaNavPanel.items, title: areaNavPanel.title, badge: areaNavPanel.badge, onLinkClicked: handleAreaNavPanelLinkClicked, layoutHasTopNavBar: !!topNavBar })] })); const sideContainer = ((0, jsx_runtime_1.jsxs)(GlobalLayoutSideContainerFixed, { onClickAway: onSideBarClickAway, children: [(0, jsx_runtime_1.jsx)(SideNavBar_1.SideNavBar, { logo: sideNavBar.logo, topItems: sideNavBar.topItems, bottomItems: sideNavBar.bottomItems, onLinkClicked: handleSideNavBarLinkClicked, onHoveredLinkChanged: setHoveredSideNavLink }), (0, jsx_runtime_1.jsx)(AreaNavPanel, { isCollapsible: true, listItems: areaNavPanel.items, title: areaNavPanel.title, badge: areaNavPanel.badge, isExpanded: isAreaNavPanelExpanded, setIsExpanded: setIsAreaNavPanelExpanded, onLinkClicked: handleAreaNavPanelLinkClicked, displayMode: areaNavPanel.displayMode, onIsHoveredChanged: setIsAreaNavPanelHovered, layoutHasTopNavBar: !!topNavBar })] })); return ((0, jsx_runtime_1.jsxs)("div", { className: rootProps, children: [topNavBar && (0, jsx_runtime_1.jsx)(TopNavBar_1.TopNavBar, { ...topNavBar, onExpandMenu: expandSideDrawer }), (0, jsx_runtime_1.jsxs)("div", { className: mainContainerProps, children: [isLargerThanIpad ? sideContainer : mobileSideContainer, (0, jsx_runtime_1.jsx)(GlobalLayoutContent, { isAreaNavPanelExpandedInline: !!topNavBar && isAreaNavPanelExpanded && areaNavPanel.displayMode === "inline" && areaNavPanel.items.length > 0, isHelpPanelVisible: helpPanel.isExpanded, scrollAreaRef: scrollAreaRef, children: children }), isLargerThanIpad && helpPanel.isExpanded && (0, jsx_runtime_1.jsx)(GlobalLayoutHelpPanel, { children: helpPanel.content })] })] })); } function getDefaultAreaNavPanelExpandState(displayMode) { return displayMode === "inline"; } function GlobalLayoutSideContainerFixed({ onClickAway, children }) { const containerRef = (0, react_1.useRef)(null); (0, react_1.useEffect)(() => { const container = containerRef.current; if (container === null) return; const onPointerDown = (event) => { if ((0, utils_1.isHTMLElement)(event.target) && !container.contains(event.target) && onClickAway) { onClickAway(); } }; window.addEventListener("pointerdown", onPointerDown); return () => { window.removeEventListener("pointerdown", onPointerDown); }; }, [onClickAway]); return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, className: globalLayoutSideContainerFixedStyles, children: children })); } function GlobalLayoutSideContainerDrawer({ isExpanded, onOpen, onClose, children }) { return ((0, jsx_runtime_1.jsx)(material_1.SwipeableDrawer, { open: isExpanded, onClose: onClose, onOpen: onOpen, children: (0, jsx_runtime_1.jsx)("div", { className: globalLayoutSideContainerDrawerStyles, children: children }) })); } const globalLayoutSideContainerFixedStyles = (0, css_1.css)({ display: "flex", position: "relative", // Needed when AreaNavBar goes into overlay/absolute mode to allow scrolling. }); const globalLayoutSideContainerDrawerStyles = (0, css_1.css)({ display: "flex", backgroundColor: design_system_tokens_1.themeTokens.color.background.secondary.default, height: "100%", }); function AreaNavPanel(props) { const [containerElement, setContainerElement] = (0, react_1.useState)(null); (0, hooks_1.useOnIsHoveredChanged)(containerElement, props.isCollapsible ? props.onIsHoveredChanged : undefined); if (props.listItems.length === 0) { return null; } //if the overall layout has a top nav bar, we need some extra styles to account for it const layoutHasTopNavBarStyles = props.layoutHasTopNavBar ? areaNavPanelWithNavBarStyles : null; if (!props.isCollapsible) { return ((0, jsx_runtime_1.jsxs)("div", { "data-amplitude-unmask": true, className: (0, css_1.cx)(areaNavPanelStyles, areaNavPanelRelativeStyles, layoutHasTopNavBarStyles), children: [props.title && (0, jsx_runtime_1.jsx)("div", { className: areaNavPanelTitleStyles, children: props.title }), props.badge && (0, jsx_runtime_1.jsx)("div", { className: areaNavPanelBadgeStyles, children: props.badge }), (0, jsx_runtime_1.jsx)(AreaNavList_1.AreaNavList, { listItems: props.listItems, onLinkClicked: props.onLinkClicked, hasTitle: !!props.title })] })); } const { displayMode, isExpanded, setIsExpanded } = props; if (displayMode === "inline") { return ((0, jsx_runtime_1.jsxs)("div", { "data-amplitude-unmask": true, ref: setContainerElement, className: (0, css_1.cx)(areaNavPanelStyles, areaNavPanelRelativeStyles, layoutHasTopNavBarStyles), children: [(0, jsx_runtime_1.jsx)(AreaNavPanelCollapsedToggle, { isPanelExpanded: isExpanded, onToggle: () => setIsExpanded(!isExpanded) }), isExpanded && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [props.title && (0, jsx_runtime_1.jsx)("div", { className: areaNavPanelTitleStyles, children: props.title }), props.badge && (0, jsx_runtime_1.jsx)("div", { className: areaNavPanelBadgeStyles, children: props.badge }), (0, jsx_runtime_1.jsx)(AreaNavList_1.AreaNavList, { listItems: props.listItems, onLinkClicked: props.onLinkClicked, hasTitle: !!props.title })] }))] })); } return ((0, jsx_runtime_1.jsx)(material_1.Fade, { ref: setContainerElement, in: isExpanded, children: (0, jsx_runtime_1.jsxs)("div", { "data-amplitude-unmask": true, className: (0, css_1.cx)(areaNavPanelStyles, areaNavPanelAbsoluteStyles, layoutHasTopNavBarStyles), children: [props.title && (0, jsx_runtime_1.jsx)("div", { className: areaNavPanelTitleStyles, children: props.title }), props.badge && (0, jsx_runtime_1.jsx)("div", { className: areaNavPanelBadgeStyles, children: props.badge }), (0, jsx_runtime_1.jsx)(AreaNavList_1.AreaNavList, { listItems: props.listItems, onLinkClicked: props.onLinkClicked, hasTitle: !!props.title })] }) })); } const toggleAreaNavPanelId = "toggleAreaNavPanel"; const areaNavPanelStyles = (0, css_1.css)({ scrollbarWidth: "none", msOverflowStyle: "none", boxShadow: design_system_tokens_1.themeTokens.shadow.layoutPrimary, overflow: "hidden", "&::-webkit-scrollbar": { width: 0, height: 0, }, [`:hover #${toggleAreaNavPanelId}`]: { opacity: "0.4", visibility: "visible", }, }); const areaNavPanelWithNavBarStyles = (0, css_1.css)({ borderTopLeftRadius: design_system_tokens_1.borderRadius.medium, }); const areaNavPanelAbsoluteStyles = (0, css_1.css)({ position: "absolute", left: "5rem", height: "100%", boxShadow: design_system_tokens_1.themeTokens.shadow.large, zIndex: zIndexAreaNavPanel, // This needs to show above the global layout content // Need a solid background when we're in absolute mode since we're overlaying other content. backgroundColor: design_system_tokens_1.themeTokens.color.background.primary.default, }); const areaNavPanelRelativeStyles = (0, css_1.css)({ position: "relative", }); const areaNavPanelTitleStyles = (0, css_1.css)({ font: design_system_tokens_1.text.heading.large, padding: `${design_system_tokens_1.space[32]} ${design_system_tokens_1.space[16]} 0 ${design_system_tokens_1.space[16]}`, background: design_system_tokens_1.themeTokens.color.background.primary.transparent, }); const areaNavPanelBadgeStyles = (0, css_1.css)({ padding: `${design_system_tokens_1.space[16]} ${design_system_tokens_1.space[16]} 0 ${design_system_tokens_1.space[16]}`, background: design_system_tokens_1.themeTokens.color.background.primary.transparent, }); function GlobalLayoutContent({ isAreaNavPanelExpandedInline, isHelpPanelVisible, scrollAreaRef, children }) { return ((0, jsx_runtime_1.jsx)("main", { className: (0, css_1.cx)(globalLayoutContentStyles, { [globalLayoutContentWithoutAreaNavPanelStyles]: !isAreaNavPanelExpandedInline, [globalLayoutContentWithHelpPanelStyles]: isHelpPanelVisible, }), children: (0, jsx_runtime_1.jsx)(ScrollArea_1.ScrollArea, { scrollAreaRef: scrollAreaRef, stableGutter: true, children: children }) })); } function AreaNavPanelCollapsedToggle({ isPanelExpanded, onToggle }) { const handleToggle = react_1.default.useCallback((e) => { e.stopPropagation(); onToggle(); }, [onToggle]); return ((0, jsx_runtime_1.jsx)("div", { id: toggleAreaNavPanelId, className: toggleStyles, onClick: handleToggle, children: isPanelExpanded && (0, jsx_runtime_1.jsx)(design_system_icons_1.ChevronsLeftIcon, { size: 16 }) })); } const toggleStyles = (0, css_1.css)({ position: "absolute", opacity: "0", visibility: "hidden", transition: "opacity 300ms", transitionTimingFunction: "ease-in", right: design_system_tokens_1.space[16], top: design_system_tokens_1.space[12], cursor: "pointer", ".MuiIconButton-root": { color: design_system_tokens_1.themeTokens.color.icon.secondary, }, }); function GlobalLayoutHelpPanel({ children }) { return (0, jsx_runtime_1.jsx)("div", { className: globalLayoutHelpPanelStyles, children: children }); } const globalLayoutHelpPanelStyles = (0, css_1.css)({ position: "relative", display: "flex", boxShadow: design_system_tokens_1.themeTokens.shadow.layoutPrimary, background: design_system_tokens_1.themeTokens.color.background.primary.default, marginLeft: design_system_tokens_1.space[16], borderTopLeftRadius: design_system_tokens_1.borderRadius.medium, }); const globalLayoutContentStyles = (0, css_1.css)({ flex: 1, isolation: "isolate", boxShadow: design_system_tokens_1.themeTokens.shadow.layoutPrimary, background: design_system_tokens_1.themeTokens.color.background.primary.default, overflow: "clip", // Prevents content from overflowing the width of this container minWidth: 0, // There is some content in portal that shows a fullscreen dialog/overlay e.g. the new project dialog // Without this z-index, the help panel has higher natural stacking order and appears over this content zIndex: zIndexGlobalLayoutContent, }); const globalLayoutContentWithoutAreaNavPanelStyles = (0, css_1.css)({ borderTopLeftRadius: design_system_tokens_1.borderRadius.medium, }); const globalLayoutContentWithHelpPanelStyles = (0, css_1.css)({ borderTopRightRadius: design_system_tokens_1.borderRadius.medium, }); const rootProps = (0, css_1.css)({ flex: 1, display: "flex", flexDirection: "column", minHeight: 0, backgroundColor: design_system_tokens_1.themeTokens.color.background.secondary.default, }); const mainContainerProps = (0, css_1.css)({ flex: 1, display: "flex", minHeight: 0, isolation: "isolate", });