@octopusdeploy/design-system-components
Version:
The design systems component library.
260 lines (259 loc) • 16.1 kB
JavaScript
;
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, { 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 })] }));
const sideContainer = ((0, jsx_runtime_1.jsxs)(GlobalLayoutSideContainerFixed, { onClickAway: onSideBarClickAway, children: [(0, jsx_runtime_1.jsx)(SideNavBar_1.SideNavBar, { 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 })] }));
return ((0, jsx_runtime_1.jsxs)("div", { className: rootProps, children: [(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: 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 (!props.isCollapsible) {
return ((0, jsx_runtime_1.jsxs)("div", { "data-amplitude-unmask": true, className: (0, css_1.cx)(areaNavPanelStyles, areaNavPanelRelativeStyles), 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), 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), 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,
borderTopLeftRadius: design_system_tokens_1.borderRadius.medium,
overflow: "hidden",
"&::-webkit-scrollbar": {
width: 0,
height: 0,
},
[`:hover #${toggleAreaNavPanelId}`]: {
opacity: "0.4",
visibility: "visible",
},
});
const areaNavPanelAbsoluteStyles = (0, css_1.css)({
position: "absolute",
left: "4.25rem",
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",
});