diff --git a/app/src/atoms/Modal/ModalHeader.tsx b/app/src/atoms/Modal/ModalHeader.tsx
new file mode 100644
index 00000000000..5eb6a701341
--- /dev/null
+++ b/app/src/atoms/Modal/ModalHeader.tsx
@@ -0,0 +1,77 @@
+import * as React from 'react'
+import { css } from 'styled-components'
+import {
+ Btn,
+ Icon,
+ TYPOGRAPHY,
+ Flex,
+ ALIGN_CENTER,
+ JUSTIFY_SPACE_BETWEEN,
+ SPACING,
+} from '@opentrons/components'
+
+import { StyledText } from '../text'
+import { Divider } from '../structure'
+import type { IconProps } from '@opentrons/components'
+
+export interface ModalHeaderProps {
+ onClose?: React.MouseEventHandler
+ title: React.ReactNode
+ icon?: IconProps
+ closeButton?: JSX.Element
+}
+
+const closeIconStyles = css`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 14px;
+ width: ${SPACING.spacingL};
+ height: ${SPACING.spacingL};
+ &:hover {
+ background-color: #16212d26;
+ }
+
+ &:active {
+ background-color: #16212d40;
+ }
+`
+
+export const ModalHeader = (props: ModalHeaderProps): JSX.Element => {
+ const { icon, onClose, title, closeButton } = props
+ return (
+ <>
+
+
+ {icon != null && }
+
+ {title}
+
+
+ {closeButton != null
+ ? { closeButton }
+ : onClose != null && (
+
+
+
+ )}
+
+
+ >
+ )
+}
diff --git a/app/src/atoms/Modal/ModalShell.tsx b/app/src/atoms/Modal/ModalShell.tsx
new file mode 100644
index 00000000000..072ea5a66b7
--- /dev/null
+++ b/app/src/atoms/Modal/ModalShell.tsx
@@ -0,0 +1,116 @@
+import * as React from 'react'
+
+import {
+ Box,
+ Flex,
+ StyleProps,
+ COLORS,
+ SPACING,
+ POSITION_FIXED,
+ POSITION_ABSOLUTE,
+ ALIGN_CENTER,
+ JUSTIFY_CENTER,
+ POSITION_RELATIVE,
+ OVERFLOW_AUTO,
+ POSITION_STICKY,
+ BORDERS,
+} from '@opentrons/components'
+
+const BASE_STYLE = {
+ position: POSITION_ABSOLUTE,
+ alignItems: ALIGN_CENTER,
+ justifyContent: JUSTIFY_CENTER,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+} as const
+
+const MODAL_STYLE = {
+ backgroundColor: COLORS.white,
+ position: POSITION_RELATIVE,
+ overflowY: OVERFLOW_AUTO,
+ maxHeight: '100%',
+ width: '100%',
+ margin: SPACING.spacing5,
+ borderRadius: BORDERS.radiusSoftCorners,
+ boxShadow: BORDERS.smallDropShadow,
+} as const
+
+const HEADER_STYLE = {
+ backgroundColor: COLORS.white,
+ position: POSITION_STICKY,
+ top: 0,
+} as const
+
+const FOOTER_STYLE = {
+ backgroundColor: COLORS.white,
+ position: POSITION_STICKY,
+ bottom: 0,
+ boxShadow: '0px 3px 6px 0px #0000003B',
+} as const
+
+export interface ModalShellProps extends StyleProps {
+ /** Optional close on outside click **/
+ onOutsideClick?: React.MouseEventHandler
+ /** Optional sticky header */
+ header?: React.ReactNode
+ /** Optional sticky footer */
+ footer?: React.ReactNode
+ /** Modal content */
+ children: React.ReactNode
+}
+
+/**
+ * A ModalShell is a layout component for building more specific modals.
+ *
+ * It includes:
+ * - An overlay
+ * - A content area, with `overflow-y: auto` and customizable with style props
+ * - An optional sticky header
+ * - An optional sticky footer
+ * - An optional onOutsideClick function
+ */
+export function ModalShell(props: ModalShellProps): JSX.Element {
+ const {
+ onOutsideClick,
+ zIndex = 10,
+ header,
+ footer,
+ children,
+ ...styleProps
+ } = props
+
+ return (
+ {
+ e.stopPropagation()
+ if (onOutsideClick != null) onOutsideClick(e)
+ }}
+ >
+
+ {
+ e.stopPropagation()
+ }}
+ >
+ {header != null ? {header} : null}
+ {children}
+ {footer != null ? {footer} : null}
+
+
+
+ )
+}
diff --git a/app/src/atoms/Modal/index.tsx b/app/src/atoms/Modal/index.tsx
index f45aea92816..87efd3fe127 100644
--- a/app/src/atoms/Modal/index.tsx
+++ b/app/src/atoms/Modal/index.tsx
@@ -3,6 +3,7 @@ import { css } from 'styled-components'
import {
Btn,
Icon,
+ Box,
BaseModal,
BaseModalProps,
TYPOGRAPHY,
@@ -14,17 +15,23 @@ import {
BORDERS,
} from '@opentrons/components'
+import { useFeatureFlag } from '../../redux/config'
import { StyledText } from '../text'
import { Divider } from '../structure'
-
+import { ModalShell } from './ModalShell'
+import { ModalHeader } from './ModalHeader'
import type { IconProps } from '@opentrons/components'
type ModalType = 'info' | 'warning' | 'error'
+export * from './ModalShell'
+export * from './ModalHeader'
+
export interface ModalProps extends BaseModalProps {
type?: ModalType
onClose?: React.MouseEventHandler
closeOnOutsideClick?: boolean
title?: React.ReactNode
+ footer?: React.ReactNode
children?: React.ReactNode
icon?: IconProps
}
@@ -54,7 +61,8 @@ export const Modal = (props: ModalProps): JSX.Element => {
children,
maxHeight,
} = props
- const header =
+ const liquidSetupEnabled = useFeatureFlag('enableLiquidSetup')
+ const defaultHeader =
title != null ? (
<>
{
>
) : null
+ const modalHeader = (
+
+ )
- return (
+ return liquidSetupEnabled ? (
+
+
+ {children}
+
+
+ ) : (
}
>
-
+
-
+
)
}