diff --git a/src/constants/themes/types.ts b/src/constants/themes/types.ts
index 78efc7cea..aee29d6e4 100644
--- a/src/constants/themes/types.ts
+++ b/src/constants/themes/types.ts
@@ -11,7 +11,9 @@ type BoxShadows =
| typeof primaryTheme['BOX_SHADOWS']
| typeof secondaryTheme['BOX_SHADOWS'];
-type Colors = typeof primaryTheme['COLORS'] | typeof secondaryTheme['COLORS'];
+export type Colors =
+ | typeof primaryTheme['COLORS']
+ | typeof secondaryTheme['COLORS'];
type Fonts = typeof primaryTheme['FONTS'] | typeof secondaryTheme['FONTS'];
diff --git a/src/shared-components/dialogModal/__snapshots__/test.tsx.snap b/src/shared-components/dialogModal/__snapshots__/test.tsx.snap
new file mode 100644
index 000000000..1a9aa883a
--- /dev/null
+++ b/src/shared-components/dialogModal/__snapshots__/test.tsx.snap
@@ -0,0 +1,121 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders dialog modal with custom color 1`] = `
+.emotion-2 {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 999999;
+ background-color: rgba(58,55,75,0.7);
+ -webkit-transition: opacity 250ms cubic-bezier(0.075,0.82,0.165,1);
+ transition: opacity 250ms cubic-bezier(0.075,0.82,0.165,1);
+}
+
+.emotion-2.entering,
+.emotion-2.exiting,
+.emotion-2.exited {
+ opacity: 0;
+}
+
+.emotion-2.entered {
+ opacity: 1;
+}
+
+@media (min-width:768px) {
+ .emotion-2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-flow: row nowrap;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ }
+}
+
+.emotion-0 {
+ width: 100%;
+ margin: 0 auto;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-top-left-radius: 32px;
+ border-top-right-radius: 32px;
+ box-shadow: 0px -8px 24px rgba(52,51,82,0.05);
+ background: #F8F8FA;
+ padding: 3.5rem 1.5rem 2rem;
+ overflow-y: auto;
+ max-height: 700px;
+ -webkit-transition: -webkit-transform 250ms cubic-bezier(0.075,0.82,0.165,1);
+ -webkit-transition: transform 250ms cubic-bezier(0.075,0.82,0.165,1);
+ transition: transform 250ms cubic-bezier(0.075,0.82,0.165,1);
+}
+
+.emotion-0.entered {
+ -webkit-transform: translateY(0%);
+ -ms-transform: translateY(0%);
+ transform: translateY(0%);
+}
+
+.emotion-0.entering,
+.emotion-0.exiting,
+.emotion-0.exited {
+ -webkit-transform: translateY(100%);
+ -ms-transform: translateY(100%);
+ transform: translateY(100%);
+}
+
+.emotion-0 p {
+ margin-bottom: 1.5rem;
+}
+
+.emotion-0 p:last-of-type {
+ margin-bottom: 2rem;
+}
+
+@media (min-width:768px) {
+ .emotion-0 {
+ position: relative;
+ width: 456px;
+ border-radius: 8px;
+ padding: 3.5rem;
+ }
+
+ .emotion-0.entering,
+ .emotion-0.exiting,
+ .emotion-0.exited {
+ -webkit-transform: translateY(40%);
+ -ms-transform: translateY(40%);
+ transform: translateY(40%);
+ }
+}
+
+
+
+
+
+ Dialog Modal Children Content
+
+
+
+
+`;
diff --git a/src/shared-components/dialogModal/index.tsx b/src/shared-components/dialogModal/index.tsx
index f55c1535f..5daa7b898 100644
--- a/src/shared-components/dialogModal/index.tsx
+++ b/src/shared-components/dialogModal/index.tsx
@@ -3,6 +3,7 @@ import React, { useRef, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Transition } from 'react-transition-group';
import { FocusScope } from '@react-aria/focus';
+import { useTheme } from 'emotion-theming';
import { CrossIcon } from '../../icons';
import {
@@ -11,8 +12,13 @@ import {
ModalTitle,
CrossIconContainer,
} from './style';
+import { Colors, primaryTheme, secondaryTheme } from '../../constants';
export interface DialogModalProps {
+ /**
+ * DialogModal background color. Defaults to the current theme's `white` if not specified.
+ */
+ backgroundColor?: Colors['background'];
/**
* Dialog Modal content.
* Must contain at least 1 button and is responsible for closing the modal.
@@ -41,11 +47,14 @@ const getDomNode = () =>
* Dialog Modals should always contain at least 1 button and the logic should close the modal at some point.
*/
export const DialogModal = ({
+ backgroundColor,
children,
onCloseIconClick,
title = '',
...rest
}: DialogModalProps) => {
+ const theme = useTheme();
+ const backgroundColorWithTheme = backgroundColor || theme.COLORS.white;
const [isClosing, setIsClosing] = useState(false);
const domNode = useRef(getDomNode());
@@ -87,11 +96,13 @@ export const DialogModal = ({
{onCloseIconClick && (
`
width: 100%;
margin: 0 auto;
position: absolute;
@@ -41,7 +43,7 @@ export const ModalContainer = styled.div`
border-top-left-radius: ${({ theme }) => theme.BORDER_RADIUS.large};
border-top-right-radius: ${({ theme }) => theme.BORDER_RADIUS.large};
box-shadow: ${({ theme }) => theme.BOX_SHADOWS.modal};
- background: ${({ theme }) => theme.COLORS.white};
+ background: ${({ backgroundColor }) => backgroundColor};
padding: ${SPACER.x4large} ${SPACER.large} ${SPACER.xlarge};
overflow-y: auto;
max-height: 700px;
@@ -84,7 +86,9 @@ export const ModalTitle = styled(Typography.Title)`
margin-bottom: ${SPACER.small};
`;
-export const CrossIconContainer = styled.div`
+export const CrossIconContainer = styled.div<{
+ backgroundColor: Colors['background'] | Colors['white'];
+}>`
position: absolute;
top: 8px;
right: 12px;
@@ -92,7 +96,7 @@ export const CrossIconContainer = styled.div`
width: 40px;
height: 40px;
border-radius: 50%;
- background: ${({ theme }) => theme.COLORS.white};
+ background: ${({ backgroundColor }) => backgroundColor};
display: flex;
flex-flow: row nowrap;
justify-content: center;
diff --git a/src/shared-components/dialogModal/test.tsx b/src/shared-components/dialogModal/test.tsx
index b790d885d..e5b512ed5 100644
--- a/src/shared-components/dialogModal/test.tsx
+++ b/src/shared-components/dialogModal/test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { primaryTheme } from 'src/constants/themes';
import { render } from 'src/tests/testingLibraryHelpers';
import { DialogModal } from './index';
@@ -17,4 +18,15 @@ describe('', () => {
getAllByText(modalTitle);
getAllByText(modalBody);
});
+
+ it('renders dialog modal with custom color', () => {
+ const { container } = render(
+
+ {modalBody}
+ ,
+ { withPortalContainer: true },
+ );
+
+ expect(container.firstElementChild).toMatchSnapshot();
+ });
});
diff --git a/src/tests/testingLibraryHelpers.tsx b/src/tests/testingLibraryHelpers.tsx
index 7a3a1867f..e5b6beace 100644
--- a/src/tests/testingLibraryHelpers.tsx
+++ b/src/tests/testingLibraryHelpers.tsx
@@ -6,8 +6,24 @@ import { primaryTheme, ThemeType } from '../constants';
interface RenderOptions extends ReactTestingLibrary.RenderOptions {
theme?: ThemeType;
+ withPortalContainer?: boolean;
}
+/**
+ * Many of our pages rely on components that make use of portals.
+ *
+ * Unit tests for components that do that **do not include such a portal
+ * container in their unit test** will have test failures on CI due to
+ * serialization order being off.
+ */
+const usePortalContainer = () => {
+ const portalContainer = document.createElement('div');
+ portalContainer.setAttribute('id', 'reactPortalSection');
+ document.body.appendChild(portalContainer);
+
+ return portalContainer;
+};
+
// We customize @testing-library methods to bake-in theming and keep unit tests DRY.
// We do not use ReactTestingLibrary.render(Component, { wrapper }) option because
// `@testing-library/react` is (somehow) overwriting React with its own API when used.
@@ -17,11 +33,17 @@ const customRender = (
Component: React.ReactElement,
options: RenderOptions = {},
): ReactTestingLibrary.RenderResult => {
- const { theme = primaryTheme, ...rest } = options;
+ const {
+ theme = primaryTheme,
+ container,
+ withPortalContainer,
+ ...rest
+ } = options;
return ReactTestingLibrary.render(
{Component},
{
+ container: withPortalContainer ? usePortalContainer() : container,
...rest,
},
);
diff --git a/stories/dialogModal/index.stories.tsx b/stories/dialogModal/index.stories.tsx
index a651f45c1..524acf33b 100644
--- a/stories/dialogModal/index.stories.tsx
+++ b/stories/dialogModal/index.stories.tsx
@@ -10,7 +10,8 @@ import {
Story,
Title,
} from '@storybook/addon-docs/blocks';
-import type { Meta } from '@storybook/react';
+import { Meta } from '@storybook/react';
+import { useTheme } from 'emotion-theming';
import { ANIMATION } from 'src/constants';
import { modalStoryDecoratorForChromatic } from 'stories/utils';
@@ -83,6 +84,86 @@ export const DefaultOpened = () => {
);
};
+export const WithColor = () => {
+ const [withCloseIcon, setWithCloseIcon] = useState(false);
+ const theme = useTheme();
+
+ return (
+
+
+
+ {withCloseIcon && (
+ setWithCloseIcon(false)}
+ >
+
+ This will remove the cleanser and moisturizer from your free trial,
+ too. Just the custom bottle will be sent your way!
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+WithColor.id = `${DIALOG_MODAL_STORY_ID_PREFIX}with-color`;
+WithColor.parameters = {
+ chromatic: { disable: true },
+};
+
+export const WithColorOpened = () => {
+ const [withCloseIcon, setWithCloseIcon] = useState(true);
+ const theme = useTheme();
+
+ return (
+
+
+
+ {withCloseIcon && (
+ setWithCloseIcon(false)}
+ >
+
+ This will remove the cleanser and moisturizer from your free trial,
+ too. Just the custom bottle will be sent your way!
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+WithColorOpened.id = `${DIALOG_MODAL_STORY_ID_PREFIX}with-color`;
+WithColorOpened.decorators = [modalStoryDecoratorForChromatic];
+
DefaultOpened.storyName = 'Default (Opened)';
DefaultOpened.decorators = [modalStoryDecoratorForChromatic];
@@ -188,8 +269,13 @@ export default {
- With Close Icon
+ With Color
+
+
+ With Close Icon