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%); + } +} + +
+
+`; 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