@octopusdeploy/design-system-components
Version:
The design systems component library.
216 lines (215 loc) • 14.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Dialog = Dialog;
const jsx_runtime_1 = require("react/jsx-runtime");
const css_1 = require("@emotion/css");
const design_system_icons_1 = require("@octopusdeploy/design-system-icons");
const design_system_tokens_1 = require("@octopusdeploy/design-system-tokens");
const react_1 = require("react");
const Button_1 = require("../Button");
const ErrorSummary_1 = require("../ErrorSummary");
const DialogBase_1 = require("./DialogBase");
const DialogMainPanel_1 = require("./DialogMainPanel");
const DialogStepNavigationFooter_1 = require("./DialogStepNavigationFooter");
const dialogBreakpoints_styles_1 = require("./dialogBreakpoints.styles");
const useDialogSteps_1 = require("./useDialogSteps");
function isMultiStepProps(props) {
return "steps" in props && Array.isArray(props.steps);
}
/**
* A standard modal dialog with a title, optional dismiss button, scrollable content area,
* optional error panel, primary/secondary footer actions, optional animated side panel,
* and optional multi-step navigation.
*
* @remarks Do not implement directly in Octopus Server code. Use Workflows instead. See https://docs.octopushq.com/docs/frontend/workflows
*/
function Dialog(props) {
const { width, open, onClose, onClosed, title, errors, showDismissButton = true, sidePanel, defaultSidePanelOpen = true } = props;
const multiStep = isMultiStepProps(props);
const { currentStepIndex, totalSteps, isFirstStep, isLastStep, isNavigating, goToNext, goToPrevious, reset } = (0, useDialogSteps_1.useDialogSteps)(multiStep ? props.steps.map((s) => ({ onBeforeNext: s.onBeforeNext, onBeforeBack: s.onBeforeBack, onEnter: s.onEnter })) : []);
// extraLarge → undefined: clamps here so an `any`-typed caller can't bypass the type-level rejection of sidePanel at extraLarge.
const sidePanelWidth = (0, dialogBreakpoints_styles_1.toSidePanelWidth)(width);
// Determine initial side panel state (avoids flash during close animation).
const initialSidePanelContent = sidePanelWidth ? ((multiStep && props.steps.length > 0 ? props.steps[0].sidePanel : undefined) ?? sidePanel) : undefined;
const initialHasSidePanel = !!initialSidePanelContent;
// Determine content and side panel based on single vs multi-step.
const currentStep = multiStep ? props.steps[currentStepIndex] : undefined;
const content = currentStep ? currentStep.content : props.children;
const sidePanelContent = sidePanelWidth ? (currentStep?.sidePanel ?? sidePanel) : undefined;
const hasSidePanel = !!sidePanelContent;
const [sidePanelOpen, setSidePanelOpen] = (0, react_1.useState)(defaultSidePanelOpen && hasSidePanel);
(0, react_1.useEffect)(() => {
if (open) {
reset();
setSidePanelOpen(defaultSidePanelOpen && initialHasSidePanel);
}
}, [open, reset, defaultSidePanelOpen, initialHasSidePanel]);
const showDismiss = !hasSidePanel && showDismissButton;
const progressTracker = multiStep ? currentStep?.progressTracker : props.progressTracker;
// Build the footer actions
const actionsEl = multiStep ? ((0, jsx_runtime_1.jsx)(DialogStepNavigationFooter_1.DialogStepNavigationFooter, { totalSteps: totalSteps, currentStepIndex: currentStepIndex, isFirstStep: isFirstStep, isLastStep: isLastStep, isNavigating: isNavigating, primaryImportance: "primary", completeButtonLabel: props.completeButtonLabel, backButtonDisabled: props.backButtonDisabled, nextButtonDisabled: props.nextButtonDisabled, onClose: onClose, onBack: () => {
void goToPrevious();
}, onNext: () => {
void goToNext();
}, onComplete: props.onComplete })) : ((0, jsx_runtime_1.jsxs)("footer", { className: dialogStyles.actions, children: [showDismissButton && (0, jsx_runtime_1.jsx)(Button_1.Button, { importance: "quiet", label: "Cancel", onClick: onClose }), props.secondaryAction && (0, jsx_runtime_1.jsx)(Button_1.Button, { ...props.secondaryAction, importance: props.secondaryAction.importance ?? "secondary" }), (0, jsx_runtime_1.jsx)(Button_1.Button, { ...props.primaryAction, importance: props.primaryAction.importance ?? "primary" })] }));
return ((0, jsx_runtime_1.jsx)(DialogBase_1.DialogBase, { open: open, onClose: onClose, onClosed: onClosed, accessibleName: title, autoFocusFirstElement: false, closeOnBackdropClick: showDismissButton, children: (0, jsx_runtime_1.jsx)("div", { className: (0, css_1.cx)(dialogStyles.outer, sidePanelWidth && sidePanelOpen ? outerSidePanelOpenStyles[sidePanelWidth] : null), children: (0, jsx_runtime_1.jsxs)("div", { className: (0, css_1.cx)(dialogStyles.container, sidePanelWidth ? containerStyles[sidePanelWidth] : null, sidePanelWidth && sidePanelOpen ? containerSidePanelOpenStyles[sidePanelWidth] : null), children: [(0, jsx_runtime_1.jsx)(DialogMainPanel_1.DialogMainPanel, { isLoading: props.isLoading, width: width, hasSidePanel: hasSidePanel, title: title, headerAction: hasSidePanel ? { type: "toggleSidePanel", onOpen: () => setSidePanelOpen(true), isOpen: sidePanelOpen } : showDismiss ? { type: "dismiss", onClose } : undefined, progressTracker: progressTracker, errorPanel: errors && errors.length > 0 ? errors.map((err, index) => (0, jsx_runtime_1.jsx)(ErrorSummary_1.ErrorSummary, { error: err }, index)) : undefined, footer: actionsEl, children: content }), sidePanelWidth && ((0, jsx_runtime_1.jsx)("aside", { className: (0, css_1.cx)(dialogStyles.sidePanelContainer, sidePanelWrapperStyles[sidePanelWidth]), "aria-hidden": !sidePanelOpen, ...(!sidePanelOpen && { inert: "" }), children: (0, jsx_runtime_1.jsxs)("div", { className: dialogStyles.sidePanel, children: [hasSidePanel && ((0, jsx_runtime_1.jsx)("div", { className: (0, css_1.cx)(toggleButtonStyles[sidePanelWidth], dialogStyles.toggleInSidePanel, !sidePanelOpen && dialogStyles.toggleInSidePanelClosed, !sidePanelOpen && dialogStyles.toggleHidden), children: (0, jsx_runtime_1.jsx)(Button_1.Button, { importance: "ghost", accessibleName: "Close side panel", onClick: () => {
setSidePanelOpen(false);
}, size: "small", icon: (0, jsx_runtime_1.jsx)(design_system_icons_1.ChevronsLeftIcon, { size: 24 }) }) })), (0, jsx_runtime_1.jsx)("div", { className: (0, css_1.cx)(dialogStyles.sidePanelContent, { [dialogStyles.sidePanelContentClosed]: !sidePanelOpen }), children: sidePanelContent })] }) }))] }) }) }));
}
//TODO replace with tokens
const borderRadiusTransitionDurationMs = 150;
// When opening: animate the border radius to 0 over 150ms with no delay,
// so the corners are square before the panel finishes sliding in.
const outerSidePanelOpenProps = {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
transition: `border-top-right-radius ${borderRadiusTransitionDurationMs}ms ease 0ms, border-bottom-right-radius ${borderRadiusTransitionDurationMs}ms ease 0ms`,
};
const outerSidePanelOpenStyles = {
small: (0, css_1.css)({ [`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.small})`]: outerSidePanelOpenProps }),
medium: (0, css_1.css)({ [`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.medium})`]: outerSidePanelOpenProps }),
large: (0, css_1.css)({ [`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.large})`]: outerSidePanelOpenProps }),
};
const containerStyles = {
small: (0, css_1.css)({
[`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.small})`]: {
gridTemplateColumns: `${design_system_tokens_1.dialog.mainPanel.width.small} 0px`,
},
}),
medium: (0, css_1.css)({
[`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.medium})`]: {
gridTemplateColumns: `${design_system_tokens_1.dialog.mainPanel.width.medium} 0px`,
},
}),
large: (0, css_1.css)({
[`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.large})`]: {
gridTemplateColumns: `${design_system_tokens_1.dialog.mainPanel.width.large} 0px`,
},
}),
};
const containerSidePanelOpenStyles = {
small: (0, css_1.css)({
[`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.small})`]: {
gridTemplateColumns: `${design_system_tokens_1.dialog.mainPanel.width.small} ${design_system_tokens_1.dialog.sidePanel.width}`,
},
}),
medium: (0, css_1.css)({
[`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.medium})`]: {
gridTemplateColumns: `${design_system_tokens_1.dialog.mainPanel.width.medium} ${design_system_tokens_1.dialog.sidePanel.width}`,
},
}),
large: (0, css_1.css)({
[`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.large})`]: {
gridTemplateColumns: `${design_system_tokens_1.dialog.mainPanel.width.large} ${design_system_tokens_1.dialog.sidePanel.width}`,
},
}),
};
const sidePanelWrapperStyles = {
small: (0, css_1.css)({ [`@media (max-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.small})`]: { display: "none" } }),
medium: (0, css_1.css)({ [`@media (max-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.medium})`]: { display: "none" } }),
large: (0, css_1.css)({ [`@media (max-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.large})`]: { display: "none" } }),
};
const toggleButtonBaseProps = { display: "none", flexShrink: 0 };
const toggleButtonStyles = {
small: (0, css_1.css)({ ...toggleButtonBaseProps, [`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.small})`]: { display: "block" } }),
medium: (0, css_1.css)({ ...toggleButtonBaseProps, [`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.medium})`]: { display: "block" } }),
large: (0, css_1.css)({ ...toggleButtonBaseProps, [`@media (min-width: ${dialogBreakpoints_styles_1.dialogSidePanelBreakpoint.large})`]: { display: "block" } }),
};
const dialogStyles = {
outer: (0, css_1.css)({
overflow: "hidden",
borderRadius: design_system_tokens_1.borderRadius.large,
maxHeight: design_system_tokens_1.dialog.maxHeight,
display: "flex",
flexDirection: "column",
transition: `border-top-right-radius ${borderRadiusTransitionDurationMs}ms ease 150ms, border-bottom-right-radius ${borderRadiusTransitionDurationMs}ms ease 150ms`,
}),
container: (0, css_1.css)({
display: "grid",
minHeight: 0,
transition: `grid-template-columns ${design_system_tokens_1.dialog.sidePanel.slideDuration} ease-in-out ${design_system_tokens_1.dialog.sidePanel.fadeDuration}`,
}),
actions: (0, css_1.css)({
flexShrink: 0,
display: "flex",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
gap: design_system_tokens_1.space[8],
padding: `${design_system_tokens_1.space[8]} ${design_system_tokens_1.space[24]} ${design_system_tokens_1.space[24]} ${design_system_tokens_1.space[24]}`,
}),
sidePanelContainer: (0, css_1.css)({
background: design_system_tokens_1.themeTokens.color.dialog.background.secondary.default,
display: "flex",
minWidth: 0,
overflow: "hidden",
position: "relative",
borderBottomRightRadius: design_system_tokens_1.borderRadius.large,
borderTopRightRadius: design_system_tokens_1.borderRadius.large,
}),
sidePanel: (0, css_1.css)({
display: "flex",
flexDirection: "column",
alignItems: "center",
boxSizing: "border-box",
padding: `${design_system_tokens_1.space[72]} ${design_system_tokens_1.space[32]}`,
position: "relative",
width: design_system_tokens_1.dialog.sidePanel.width,
flex: "0 0 auto",
}),
sidePanelContent: (0, css_1.css)({
font: design_system_tokens_1.text.body.regular.large,
color: design_system_tokens_1.themeTokens.color.text.primary,
transition: `opacity ${design_system_tokens_1.dialog.sidePanel.fadeDuration} ease ${design_system_tokens_1.dialog.sidePanel.slideDuration}`,
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: design_system_tokens_1.space[24],
width: "100%",
"& img": {
display: "block",
width: "100%",
height: "auto",
},
// This will handle some basic typography styles for the content to prevent inconsistent styling.
// TODO: should replace with a more robust typography system/layout system.
"& p": {
margin: 0,
font: design_system_tokens_1.text.body.regular.large,
textBox: "trim-both text",
},
"& h3": {
font: design_system_tokens_1.text.heading.small,
margin: 0,
textBox: "trim-both text",
},
"& ul": {
marginTop: 0,
marginBottom: 0,
font: design_system_tokens_1.text.body.regular.large,
textBox: "trim-both text",
},
"& p + h3, & p + div": {
marginTop: design_system_tokens_1.space[12],
},
}),
sidePanelContentClosed: (0, css_1.css)({
opacity: 0,
pointerEvents: "none",
transition: `opacity ${design_system_tokens_1.dialog.sidePanel.fadeDuration} ease 0ms`,
}),
toggleInSidePanel: (0, css_1.css)({
position: "absolute",
top: design_system_tokens_1.space[16],
right: design_system_tokens_1.space[16],
zIndex: 1,
transition: `opacity ${design_system_tokens_1.dialog.sidePanel.fadeDuration} ease ${design_system_tokens_1.dialog.sidePanel.slideDuration}`,
}),
toggleInSidePanelClosed: (0, css_1.css)({
transition: `opacity ${design_system_tokens_1.dialog.sidePanel.fadeDuration} ease 0ms`,
}),
toggleHidden: (0, css_1.css)({
opacity: 0,
pointerEvents: "none",
}),
};