From c9ef2ac15c7091b6fc7f9b28a975ebfca447832f Mon Sep 17 00:00:00 2001 From: ludwiczakpawel Date: Thu, 27 Jan 2022 16:24:23 +0100 Subject: [PATCH] Modals fixes (#16310) * . * dark * simplify and add stories * update stories and docs * fix --- app/assets/stylesheets/components/modals.scss | 109 +++++++++-------- app/assets/stylesheets/config/_colors.css | 11 ++ app/assets/stylesheets/themes/dark.css | 11 ++ app/javascript/admin/adminModal.js | 2 +- app/javascript/crayons/Modal/Modal.jsx | 110 ++++++------------ .../Modal/__stories__/Modal.stories.jsx | 109 ++++++++++++++--- .../crayons/Modal/__stories__/modals.mdx | 47 +++++++- .../crayons/Modal/__tests__/Modal.test.jsx | 13 +-- .../components/EmailPreferencesForm.jsx | 2 +- .../onboarding/components/FollowTags.jsx | 2 +- .../onboarding/components/FollowUsers.jsx | 2 +- .../onboarding/components/IntroSlide.jsx | 25 ++-- .../onboarding/components/ProfileForm.jsx | 2 +- app/javascript/packs/base.jsx | 9 +- app/javascript/packs/flagUserModal.jsx | 2 +- app/views/liquids/_user_subscription.html.erb | 2 +- 16 files changed, 278 insertions(+), 180 deletions(-) diff --git a/app/assets/stylesheets/components/modals.scss b/app/assets/stylesheets/components/modals.scss index 53c635ea9..0f4fc5ec3 100644 --- a/app/assets/stylesheets/components/modals.scss +++ b/app/assets/stylesheets/components/modals.scss @@ -1,26 +1,22 @@ @import '../config/import'; .crayons-modal { - --modal-max-width: 768px; - --modal-body-padding: var(--su-3); - --modal-header-padding: var(--su-2) var(--su-2) var(--su-2) var(--su-3); - --modal-header-font-size: var(--fs-l); - --modal-body-font-size: var(--fs-base); + --modal-max-width: 640px; + --modal-padding: var(--su-3); + --modal-backdrop-pointer-events: none; + --modal-border: none; + --modal-header-border: 1px solid var(--divider); + --modal-header-padding: var(--su-2) var(--su-2) var(--su-2) + var(--modal-padding); + --modal-alignment: unset; + --modal-header-alignment: space-between; @media (min-width: $breakpoint-s) { - --modal-header-padding: var(--su-2) var(--su-2) var(--su-2) var(--su-4); - --modal-body-padding: var(--su-4); + --modal-padding: var(--su-4); } @media (min-width: $breakpoint-m) { - --modal-header-padding: var(--su-3) var(--su-3) var(--su-3) var(--su-6); - --modal-body-padding: var(--su-6); - --modal-header-font-size: var(--fs-xl); - --modal-body-font-size: var(--fs-l); - } - - @media (min-width: $breakpoint-l) { - --modal-body-padding: var(--su-8); + --modal-padding: var(--su-7); } position: fixed; @@ -34,82 +30,103 @@ z-index: var(--z-modal); pointer-events: none; - &__overlay { - background: rgba(0, 0, 0, 0.6); - opacity: 0.5; + &--bg-dismissible { + --modal-backdrop-pointer-events: auto; + } + + &--prompt { + --modal-header-border: none; + --modal-header-padding: var(--modal-padding) var(--su-8) 0 + var(--modal-padding); + --modal-body-margin: 0; + + @media (min-width: $breakpoint-m) { + --modal-body-margin: calc((var(--modal-padding) * -1) + var(--su-4)); + } + + .crayons-modal__dismiss { + position: absolute; + right: var(--su-2); + top: var(--su-2); + } + } + + &--centered { + --modal-header-padding: var(--su-7) var(--su-8) 0 var(--su-8); + --modal-alignment: center; + --modal-header-alignment: center; + } + + &--danger { + --modal-border: 4px solid var(--modal-danger-border-color); + } + + &__backdrop { + background: var(--modal-backdrop); position: absolute; left: 0; right: 0; top: 0; bottom: 0; pointer-events: auto; - - &.background-clickable { - pointer-events: none; - } } &__box { display: grid; grid-template-rows: auto 1fr; max-width: var(--modal-max-width); - background: var(--card-bg); - color: var(--card-color); + background: var(--modal-bg); z-index: var(--z-elevate); max-height: 100%; width: 100%; height: 100%; overflow: hidden; + position: relative; pointer-events: auto; border-radius: var(--radius-large-auto); + border: var(--modal-border); + text-align: var(--modal-alignment); box-shadow: var(--shadow-2); @media (min-width: $breakpoint-s) { - background: var(--card-bg); - color: var(--card-color); max-height: calc(100% - var(--su-6)); height: auto; width: 90%; } &__header { - border-bottom: 1px solid var(--base-a5); - display: flex; - align-items: center; - justify-content: space-between; + border-bottom: var(--modal-header-border); padding: var(--modal-header-padding); flex-shrink: 0; - - &__title { - font-size: var(--modal-header-font-size); - } + display: flex; + justify-content: var(--modal-header-alignment); + align-items: center; } &__body { - font-size: var(--modal-body-font-size); - padding: var(--modal-body-padding); + padding: var(--modal-padding); + margin-top: var(--modal-body-margin); max-height: 100%; overflow-y: auto; grid-auto-rows: min-content; } } - &--s { + &--small { @media (min-width: $breakpoint-s) { --modal-max-width: 480px; - --modal-body-padding: var(--su-6); - --modal-header-font-size: var(--fs-l); - --modal-body-font-size: var(--fs-base); } } - &--m { + &--large { @media (min-width: $breakpoint-m) { - --modal-max-width: 640px; - --modal-body-padding: var(--su-7); - --modal-header-padding: var(--su-3) var(--su-5); - --modal-header-font-size: var(--fs-xl); - --modal-body-font-size: var(--fs-l); + --modal-max-width: 768px; + } + } + + &--prompt { + @media (min-width: $breakpoint-s) { + --modal-max-width: 480px; } } } diff --git a/app/assets/stylesheets/config/_colors.css b/app/assets/stylesheets/config/_colors.css index 05dfe8679..a6e65d90d 100644 --- a/app/assets/stylesheets/config/_colors.css +++ b/app/assets/stylesheets/config/_colors.css @@ -276,6 +276,14 @@ --indicator-info-bg: var(--accent-brand); --indicator-info-color: rgb(var(--white)); + /*********************************************** + ** Modals ************************************** + ***********************************************/ + + --modal-bg: rgb(var(--white)); + --modal-backdrop: rgba(var(--black), 0.6); + --modal-danger-border-color: rgb(var(--red-500)); + /*********************************************** ** Colors ************************************** ***********************************************/ @@ -301,6 +309,9 @@ /* Focus */ --focus: var(--accent-brand); + /* Divider */ + --divider: rgb(var(--grey-200)); + /* Shadows */ --shadow-1: 0 10px 15px -3px rgba(var(--black), 0.1), 0 4px 6px -2px rgba(var(--black), 0.05), 0 0 0 1px rgba(var(--black), 0.1); diff --git a/app/assets/stylesheets/themes/dark.css b/app/assets/stylesheets/themes/dark.css index a218de70d..9eec08da6 100644 --- a/app/assets/stylesheets/themes/dark.css +++ b/app/assets/stylesheets/themes/dark.css @@ -284,6 +284,14 @@ --indicator-info-bg: rgb(var(--indigo-700)); --indicator-info-color: rgb(var(--indigo-100)); + /*********************************************** + ** Modals ************************************** + ***********************************************/ + + --modal-bg: rgb(var(--grey-900)); + --modal-backdrop: rgba(var(--black), 0.6); + --modal-danger-border-color: rgb(var(--red-400)); + /*********************************************** ** Colors ************************************** ***********************************************/ @@ -308,6 +316,9 @@ /* Focus */ --focus: var(--accent-brand-lighter); + /* Divider */ + --divider: rgb(var(--grey-700)); + /* Shadows */ --shadow-1: 0 10px 15px -3px rgba(var(--black), 0.5), 0 4px 6px -2px rgba(var(--black), 0.25), inset 0 0 0 1px rgba(var(--white), 0.1); diff --git a/app/javascript/admin/adminModal.js b/app/javascript/admin/adminModal.js index d29a1a620..87110f097 100644 --- a/app/javascript/admin/adminModal.js +++ b/app/javascript/admin/adminModal.js @@ -31,7 +31,7 @@ export const adminModal = function ({ rightCustomDataAttr = null, }) { return ` -
+

${title}

diff --git a/app/javascript/crayons/Modal/Modal.jsx b/app/javascript/crayons/Modal/Modal.jsx index 88e77ece7..30c2717e4 100644 --- a/app/javascript/crayons/Modal/Modal.jsx +++ b/app/javascript/crayons/Modal/Modal.jsx @@ -1,102 +1,59 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; +import classNames from 'classnames/bind'; import { FocusTrap } from '../../shared/components/focusTrap'; import { defaultChildrenPropTypes } from '../../common-prop-types'; -import { Button } from '@crayons'; +import { ButtonNew as Button } from '@crayons'; import CloseIcon from '@images/x.svg'; -function getAdditionalClassNames({ size, className }) { - let additionalClassNames = ''; - - if (size && size.length > 0 && size !== 'default') { - additionalClassNames += ` crayons-modal--${size}`; - } - - if (className && className.length > 0) { - additionalClassNames += ` ${className}`; - } - - return additionalClassNames; -} - -/** - * A modal component which can be presented with or without an overlay. - * The modal is presented within a focus trap for accessibility purposes - please note that the selector used for the focusTrap must be unique on the given page, otherwise focus may be trapped on the wrong element. - * - * @param {Object} props - * @param {Array} props.children The content to be displayed inside the Modal. Can be provided by composition (see example). - * @param {string} props.size The desired modal size ('s', 'm' or 'default') - * @param {string} props.className Optional additional classnames to apply to the modal container - * @param {string} props.title The title to be displayed in the modal heading. If provided, a title bar with a close button will be displayed. - * @param {boolean} props.overlay Whether or not to show a semi-opaque overlay behind the modal - * @param {Function} props.onClose Callback for any function to be executed on close button click or Escape - * @param {boolean} props.closeOnClickOutside Whether the modal should close if the user clicks outside of it - * @param {string} props.focusTrapSelector The CSS selector for where to trap the user's focus. This should be unique to the page in which the modal is presented. - * - * @example - * -
-

Some modal content

-
-
- */ export const Modal = ({ children, size, className, title, - overlay = true, + prompt, + centered, + noBackdrop, + backdropDismissible = false, onClose = () => {}, - closeOnClickOutside = false, - focusTrapSelector = '.crayons-modal', + focusTrapSelector = '.crayons-modal__box', }) => { + const classes = classNames('crayons-modal', { + [`crayons-modal--${size}`]: size && size !== 'medium', + 'crayons-modal--prompt': prompt, + 'crayons-modal--centered': centered && prompt, + 'crayons-modal--bg-dismissible': !noBackdrop && backdropDismissible, + [className]: className, + }); + return ( -
+
- {title && ( -
-

{title}

-
- )} +
+

{title}

+
{children}
- {overlay && ( + {!noBackdrop && (
)}
@@ -106,16 +63,15 @@ export const Modal = ({ Modal.displayName = 'Modal'; -Modal.defaultProps = { - size: 'default', -}; - Modal.propTypes = { children: defaultChildrenPropTypes.isRequired, className: PropTypes.string, title: PropTypes.string.isRequired, - overlay: PropTypes.bool, + backdrop: PropTypes.bool, + backdropDismissible: PropTypes.bool, + prompt: PropTypes.bool, + centered: PropTypes.bool, onClose: PropTypes.func, - size: PropTypes.oneOf(['default', 's', 'm']).isRequired, + size: PropTypes.oneOf(['small', 'medium', 'large']), focusTrapSelector: PropTypes.string, }; diff --git a/app/javascript/crayons/Modal/__stories__/Modal.stories.jsx b/app/javascript/crayons/Modal/__stories__/Modal.stories.jsx index 4dbe8300b..826690a85 100644 --- a/app/javascript/crayons/Modal/__stories__/Modal.stories.jsx +++ b/app/javascript/crayons/Modal/__stories__/Modal.stories.jsx @@ -15,24 +15,49 @@ export default { control: { type: 'select', options: { - default: 'default', - small: 's', - medium: 'm', + Small: 'small', + 'Medium (default)': undefined, + Large: 'large', }, }, + description: + 'There are several sizes available - use appropriate size depending on how much content you need to display inside modal.', table: { defaultValue: { summary: 'default' }, }, }, - overlay: { + noBackdrop: { + description: 'Removes the default backdrop overlay.', table: { defaultValue: { summary: true }, }, }, + backdropDismissible: { + description: + 'If `backdrop` is visible you can also make it clickable so clicking it would dismiss the Modal', + table: { + defaultValue: { summary: false }, + }, + }, + prompt: { + description: + 'Special style for Modals that should be used for short prompts (short messages or confirmations). Prompts can only be used with size `small` (`s`).', + table: { + defaultValue: { summary: false }, + }, + }, + centered: { + description: + 'In some cases it might be "nicer" to center Modals content. This will only work with `prompt` though.', + table: { + defaultValue: { summary: false }, + }, + }, title: { control: { type: 'text', }, + description: '🤷‍♂️', table: { defaultValue: { summary: 'Modal title' }, }, @@ -40,7 +65,7 @@ export default { }, }; -export const Default = (args) => { +const Template = (args) => { const [isModalOpen, setIsModalOpen] = useState(false); return ( @@ -48,19 +73,77 @@ export const Default = (args) => { {isModalOpen && ( setIsModalOpen(false)} {...args}> -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse - odio est, ultricies vel euismod ut, fringilla quis tellus. Sed at - dui mi. Fusce cursus nibh lectus, vitae lobortis orci volutpat quis.{' '} -

+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Suspendisse odio est, ultricies vel euismod ut, fringilla quis + tellus. Sed at dui mi. Fusce cursus nibh lectus, vitae lobortis + orci volutpat quis. +

+ {!args.prompt && ( +

+ Sed at dui mi. Fusce cursus nibh lectus, vitae lobortis orci + volutpat quis. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Suspendisse odio est, ultricies vel euismod ut, + fringilla quis tellus. Sed at dui mi. Fusce cursus nibh lectus, + vitae lobortis orci volutpat quis. +

+ )} +
)}
); }; +export const Default = Template.bind({}); Default.args = { - size: 'default', - title: 'My modal', - overlay: true, + size: undefined, + title: 'Modal title', + noBackdrop: false, + backdropDismissible: false, + prompt: false, + centered: false, +}; + +export const Prompt = Template.bind({}); +Prompt.args = { + size: undefined, + title: + 'Are you sure you want to remove Paweł Ludwiczak from Design Department?', + noBackdrop: false, + backdropDismissible: false, + prompt: true, + centered: false, +}; + +export const PromptCentered = Template.bind({}); +PromptCentered.args = { + size: undefined, + title: + 'Are you sure you want to remove Paweł Ludwiczak from Design Department?', + noBackdrop: false, + backdropDismissible: false, + prompt: true, + centered: true, +}; + +export const BackdropDismissible = Template.bind({}); +BackdropDismissible.args = { + size: undefined, + title: 'Modal title', + noBackdrop: false, + backdropDismissible: true, + prompt: false, + centered: false, +}; + +export const NoBackdrop = Template.bind({}); +NoBackdrop.args = { + size: undefined, + title: 'Modal title', + noBackdrop: true, + backdropDismissible: false, + prompt: false, + centered: false, }; diff --git a/app/javascript/crayons/Modal/__stories__/modals.mdx b/app/javascript/crayons/Modal/__stories__/modals.mdx index 396a4f66d..f13fe82ed 100644 --- a/app/javascript/crayons/Modal/__stories__/modals.mdx +++ b/app/javascript/crayons/Modal/__stories__/modals.mdx @@ -1,11 +1,48 @@ -## Modals +# Modals Modals should be positioned centered in relation to entire viewport. So relation -to its trigger doesn’t really matter. +to its trigger doesn't really matter. Use your best judgements when choosing the right size. -### Modal accessibility +## Sizes + +We have 3 possible sizes to use: + +- Small - 480px - for displaying small amount of data/text. +- Medium - 640px (this is default one) - you will probably want to use this one in most cases. +- Large - 768px - use for long and/or complex contnet. + +## Prompts + +Except classic Modals you can also use `prompt` style (prop) to apply _special styling_. +It will work best with really short messages or some sort of confirmation prompts, for example: + +> Are you sure you want to remove Ben's account? +> +> [YES] [NO] + +The "special styling" means it will: + +- always use Small (480px) size, +- remove the divider between title and content so it all looks more like a one message. + +### Centered prompt + +Some prompts may visually benefit from centering the content. +Leave the decision to designers but keep it mind this is possible with `centered` prop +(and it will only work in combination with `prompt`). + +## Backdrop + +By default Modals come with dark backdrop (a layer overlaying underlaying content). +It is possible to not show it but it should be used in some very rare scenarios, when underlaying +content might still be important as additional context. + +Because of having that backdrop is a "good default", you should use `noBackdrop` prop cerfaully, +ideally after consulting with your desinger. + +## Modal accessibility The Preact Modal component utilises the `focus-trap` library to ensure that: @@ -16,3 +53,7 @@ The Preact Modal component utilises the `focus-trap` library to ensure that: - While the modal is open, focus is trapped inside so that when a user presses the Tab key, interactive items behind the modal are not focused - When the modal is open, pressing the Escape key will close it + +## Design + + diff --git a/app/javascript/crayons/Modal/__tests__/Modal.test.jsx b/app/javascript/crayons/Modal/__tests__/Modal.test.jsx index 47d1ac593..74d2902c7 100644 --- a/app/javascript/crayons/Modal/__tests__/Modal.test.jsx +++ b/app/javascript/crayons/Modal/__tests__/Modal.test.jsx @@ -101,7 +101,7 @@ it('should close on click outside, if enabled', () => { This is the modal body content @@ -130,9 +130,9 @@ it('should render with additional class names', async () => { ).toEqual(true); }); -it('should render with an overlay', async () => { +it('should render with a backdrop', async () => { const { getByTestId } = render( - + This is the modal body content , ); @@ -144,12 +144,7 @@ it('should render with an overlay', async () => { it('should render with a different size modal', async () => { const { getByTestId } = render( - + This is the modal body content , ); diff --git a/app/javascript/onboarding/components/EmailPreferencesForm.jsx b/app/javascript/onboarding/components/EmailPreferencesForm.jsx index 3100967d5..5e42ff4d4 100644 --- a/app/javascript/onboarding/components/EmailPreferencesForm.jsx +++ b/app/javascript/onboarding/components/EmailPreferencesForm.jsx @@ -54,7 +54,7 @@ export class EmailPreferencesForm extends Component { return (
+