Use Modal Dialog...
- To capture immediate attention for critical information without leaving the current context.
- To confirm significant actions to prevent user errors.
- To collect brief inputs in-context with minimal disruption.
Don't use Modal Dialog...
- To show lengthy content that requires scrolling.
- To layer over another modal.
- To facilitate complex decisions.
- To implement checkout flows.
- To display SEO-critical information.
ModalDialog contains many subcomponents that need to be used in a particular way to ensure consistency and proper accessibility across Vista. All ModalDialog's should contain:
ModalDialogNav
which contains aModalDialogCloseButton
with localised text.ModalDialogHeader
ModalDialogBody
ModalDialogFooter
, which can containModalDialogButtons
as needed.
Vanilla versions of these components also exist, as seen in the blow example.
Opening and closing
In React, use isOpen
to dictate whether or not the ModalDialog
should be open. Use this in combination with onRequestDismiss
, a callback function which will be invoked when the ModalDialog
wants to be dismissed. This can happen in a number of different ways e.g. user clicked on the ModalDialogCloseButton
, user pressed the escape key, user "clicked away".
In Vanilla, use the native showModal
function, which shows the native dialog
element with the appropriate attributes. The ModalDialog
HTML can be located anywhere in the HTML when the page loads.
The preview has been updated.
Dialog Buttons
Use ModalDialogButtons
and ModalDialogButtonsText
inside of a ModalDialogFooter
to render the required buttons and text for your dialog, e.g. 'Cancel'/'Confirm'.
See the Button Placement guidelines.
The preview has been updated.
Panel
Use variant
with a panel
option to turn the dialog into a panel that slides out from the side of the screen.
variant="panel-right"
dialogs slide out from the right side, and will be as tall as the screen. Use these to provide additional detail and/or choices.variant="panel-bottom"
dialogs slide up from the bottom, and will be as wide as the screen. They are used for messaging, in applications such as Studio and Cart/Checkout.variant="panel-top"
dialogs slide down from the top, and will be as wide as the screen. They are used for navigation content such as a hidden search form.- All
panel
andmenu
dialogs should use thepinned
option that pins their footers to the bottom of the panel. In React, this is<ModalDialogFooter pinned>
. - There is a option that makes a panel's body be
capped
, giving it a maximum width. (In React, this isbodyWidth="capped"
.) Most panel dialogs should use this option; Gallery and PDC are notable exceptions, since their panels need extra width.
In Vanilla, these options are available via swan-modal-dialog-panel
and the relevant swan-modal-dialog-panel-*
class.
Most panel dialogs should use the pinned
footer and bodyWidth="capped"
.
The preview has been updated.
Panel with a custom nav
Use ModalDialogNav
with additional custom elements to create a custom nav at the top of the panel. This can even include placing ModalDialogHeader
inside of ModalDialogNav
.
The preview has been updated.
Menu
Use variant="menu"
for left-hand flyout menus, such as the site navigation on mobile devices.
All menu
dialogs should use the pinned
option that pins their footers to the bottom of the panel.
In Vanilla, use the swan-modal-dialog-panel-menu
class can be used.
Even though it currently resembles 'panel-left', use 'menu' for menu-like content to support future styling differences.
The preview has been updated.
Pinned footer
Use pinned
with ModalDialogFooter
to pin the footer to the bottom of the dialog, always staying visible as the users scrolls through the dialog content. This is only usable on panel
and menu
dialogs, and is recommended for them.
In Vanilla, use the swan-modal-dialog-panel-pinned
can be used.
On small screens in landscape mode, footers will not be pinned to avoid obscuring content.
The preview has been updated.
Body width
The dialog defaults to a max width of 600px (on Medium screens) or 95% of screen width (on smaller screens), but will attempt to stretch to fit its contents.
Capped
Use bodyWidth="capped"
with panel
dialogs to cap their width at 344px, but 100% on XS screens. Most panel dialogs will want to use this option, save for those exceptions (as in Gallery or PDC) that have panels with especially wide content.
In Vanilla, use the swan-modal-dialog-panel-capped
class can be used.
The preview has been updated.
Grow
Use bodyWidth="grow"
with panel
dialogs to make the dialog grow as wide as the content requires, up to almost the full browser width.
In Vanilla, use the swan-modal-dialog-grow
class can be used.
The preview has been updated.
Full bleed
Use fullBleed
on ModalDialogContent
to make the dialog's content touch the dialog's edges, by removing the padding around the dialog content.
In Vanilla, use the swan-modal-dialog-skin-full-bleed
class can be used.
The preview has been updated.
Takeover
Use takeOver
to cause the dialog to fill the full screen on all screen sizes. (Any dialog, even those without this option, will cover the full screen on Extra-Small displays.)
In Vanilla, use the swan-modal-dialog-takeover
class can be used.
The preview has been updated.
Conditionally render contents
Use onlyRenderWhenOpen
to stop the 'ModalDialogContent' being rendered in the DOM when the dialog is not yet open. This is useful if you have a network request or tracking fired inside of a 'useEffect' on mount.
The preview has been updated.
No close button
If necessary, ModalDialogCloseButton
can be omitted. In these cases, your dialog must provide some other visible and accessible way to close it.
In general, this is not recommended.
Browser History
Sometimes, we want our ModalDialog
to close when the user clicks the browser's "back" button. This is especially important on Android devices where the "back" button is globally available across all apps.
On a mobile device, when a Modal Dialog opens, it may appear to the user as if they've navigated to a new page, because the dialog covers all or most of the screen. In this context, it is natural to assume that the back button will close the dialog.
Therefore, we support a Browser History feature that lets the "back" button close the dialog:
React
We provide a useBrowserHistoryState
hook which is capable of syncing React state-changes with the browsers history stack.
It relies on the fact that you can actually push arbitrary (serializable) data on to the browser's history stack without changing the URL.
So, we're able to serialize and store your state as part of the history stack whenever it changes. Then, when the user interacts with the back/forward/refresh buttons, we can get the new value from the history stack and update it the local React state accordingly.
The usage/signature is very similar to React.useState
. The main difference is that we require a key
in order to identify your state in the browser's history stack.
Use it just like you'd use React.useState
and your state-changes will automatically become undo/redo-able via the back/forward buttons and the state will be persisted across refreshes as an added bonus.
Be mindful of where you use this hook. Pushing loads of entries onto the browser's history stack could result in a bad UX if the user wants to make their way back to a previous page and needs to run through all of your states as they repeatedly click the back
button.
In general, only use this hook if your state-changes feel like navigation events.
The preview has been updated.
With this code, we will see the following behavior:
<button>
is clicked,setIsOpen(true)
, browser history entry is created with{ 'modal-dialog-open': true }
.back
button is clicked, browser history is updated and there is nomodal-dialog-open
state associated with the now-current history entry, soisOpen
is set tofalse
(the default value).{replace:true}
be used as a third parameter in the functionuseBrowserHistoryState()
to indicate that when the user closes a modal dialog by clicking on the close icon, the modal should not be reopened if the user goes back to the previous page.
Accessibility
- If the dialog has an internal title, then the dialog's outermost tag should have an attribute aria-labelledby whose value is the id of the title; the React component will do this automatically if there is a ModalDialogTitle component. If the dialog does not have an element that can act as its title, then the outermost tag needs an aria-label attribute whose value acts as a title for the dialog; the text for this value must be localized, since some browsers will read it to the user.
- If the dialog has a piece of content that can act as a description of the dialog's contents, then the outermost tag should have an attribute aria-describedby whose value is the id of that descriptor element.
- You need to place localized text inside the ModalDialogCloseButton that indicates the function of the close button, e.g. "Close". This text will not be visible on screens, but supports accessibility.
- The dialog will normally place focus onto the close button once the dialog opens. If you are using the "no close button" option, the dialog may attempt to put focus on the first interactive element (button, input field, etc) inside the dialog. If no such element exists at the time the dialog opens, you will need to move focus yourself onto the first interactive element, once one becomes available.
- Modal Dialogs must be dismissible.
Implementation
The polyfill for older browsers will move the location of the dialog node within the DOM tree, in order to position it properly on the screen and to provide accessibility support. Therefore, you shouldn't rely upon the dialog being able to inherit any CSS from any given ancestor, and you shouldn't use an ancestor selector in your CSS that expects the dialog to have a certain ancestor. Any CSS selectors for the dialog content should only refer to elements within the dialog itself.
Developer Guidelines - React API
Unit Testing in Jest
jsdom, the node-based implementation of browser standards that jest uses to render react inside of unit tests, does not support the HTMLDialogElement API. This might cause unit tests to fail with an error like "dialogRef.current.showModal is not a function." Add the following to your jest setup file (referenced in `setupFilesAfterEnv`:
Note: Since JSDOM doesn't implement the HTMLDialogElement
API, the onRequestDismiss
callback won't be triggered when the dialog's close event occurs.
Developer Guidelines - Vanilla API
- The JavaScript file for the dialog should go at the end of the body tag, and not in the head tag. This will ensure that it smoothly loads its polyfill as needed.
- All dialog elements on the page with the class name swan-dialog will be registered by the script automatically.
- dialogs added to the DOM after the initial load can be registered by calling window.registerVanillaSwanDialog(dialogEl)
- The dialog will be hidden by default. It should be opened by calling the showModal() function of the dialog element.
- The dialog will close automatically when there is a click on the backdrop of the modal. It will not close when the X button is clicked - to close the modal, call the native close() function of the element. Once the dialog has been closed, it will trigger a close event.
- The dialog does not allow you to change its options or configuration after the dialog has been instantiated.