Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 23 additions & 41 deletions src/components/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
}
}

/* stylelint-disable no-descending-specificity */
.modal {
/* Public API (customizable component options) */
--_op-modal-width: calc(141 * var(--op-size-unit)); /* 564px */
Expand Down Expand Up @@ -87,63 +86,46 @@
gap: var(--op-space-small);
}
}
/* stylelint-enable no-descending-specificity */

dialog.modal {
position: fixed;
display: block;
padding: 0;
border: none;
inset: 0;
overscroll-behavior: contain;
transform: scale(0.7);
transition:
display var(--op-transition-modal-time) allow-discrete,
overlay var(--op-transition-modal-time) allow-discrete,
transform var(--op-transition-modal-time),
opacity var(--op-transition-modal-time);

&::backdrop {
/* The Dialog backdrop does not inheret from :root so these need to be duplicated here. */
--op-color-black: hsl(0deg 0% 0%);
--_op-modal-backdrop-active-opacity: 0.5;
--op-opacity-none: 0;
--op-opacity-full: 1;

animation: show-backdrop 300ms ease-in;
background: var(--op-color-black);
opacity: var(--_op-modal-backdrop-active-opacity);
overflow: hidden;
background-color: var(--op-color-black);
opacity: var(--op-opacity-none);
overscroll-behavior: contain;
transition:
display var(--op-transition-modal-time) allow-discrete,
overlay var(--op-transition-modal-time) allow-discrete,
transform var(--op-transition-modal-time),
opacity var(--op-transition-modal-time);
}

&[open] {
opacity: var(--op-opacity-full);
transform: scale(1);
}

&.modal--closing {
opacity: var(--op-opacity-none);
transform: scale(0.7);

&::backdrop {
animation: hide-backdrop 300ms ease-in;
opacity: var(--op-opacity-none);
opacity: var(--op-opacity-half);
}
}
}

/* Using the Dialog element */

/* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog */

@keyframes show-backdrop {
from {
opacity: var(--op-opacity-none);
}

to {
opacity: var(--_op-modal-backdrop-active-opacity);
}
}

@keyframes hide-backdrop {
from {
opacity: var(--_op-modal-backdrop-active-opacity);
}
@starting-style {
dialog.modal[open] {
transform: scale(0.7);

to {
opacity: var(--op-opacity-none);
&::backdrop {
opacity: var(--op-opacity-none);
}
}
}
4 changes: 3 additions & 1 deletion src/components/navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
align-items: center;
padding: var(--op-space-small) var(--_op-navbar-horizontal-spacing);
background-color: var(--_op-navbar-background-color);
box-shadow: var(--op-border-bottom) var(--_op-navbar-border-color);
box-shadow:
var(--op-border-top) var(--_op-navbar-border-color),
var(--op-border-bottom) var(--_op-navbar-border-color);
color: var(--_op-navbar-text-color);
gap: var(--_op-navbar-content-spacing);

Expand Down
4 changes: 3 additions & 1 deletion src/components/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
display: flex;
flex-direction: column;
background-color: var(--_op-sidebar-background-color);
box-shadow: var(--op-border-right) var(--_op-sidebar-border-color);
box-shadow:
var(--op-border-left) var(--_op-sidebar-border-color),
var(--op-border-right) var(--_op-sidebar-border-color);
color: var(--_op-sidebar-text-color);
inline-size: var(--__op-sidebar-width);
min-inline-size: var(--__op-sidebar-width);
Expand Down
3 changes: 2 additions & 1 deletion src/core/tokens/base_tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@
--op-transition-accordion-content: height 300ms ease, content-visibility 300ms ease allow-discrete;
--op-transition-input: all 120ms ease-in;
--op-transition-sidebar: all 200ms ease-in-out;
--op-transition-modal: all 300ms ease-in;
--op-transition-modal-time: 300ms;
--op-transition-modal: all var(--op-transition-modal-time) ease-in;
--op-transition-panel: right 400ms ease-in;
--op-transition-tooltip: all 300ms ease-in 300ms;

Expand Down
42 changes: 41 additions & 1 deletion src/stories/Components/Modal/Modal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export const createModal = ({ header, body, footer = '', inlineDemo = false }) => {
export const createModal = ({ header, body, footer = '', inlineDemo = false, dialog = false }) => {
if (dialog) {
return createDialogModal({ header, body, footer })
}

const element = document.createElement('div')

element.classList = `modal-wrapper modal-wrapper--active ${inlineDemo ? 'modal-wrapper--demo' : ''}`
Expand Down Expand Up @@ -31,3 +35,39 @@ export const createModal = ({ header, body, footer = '', inlineDemo = false }) =

return element
}

export const createDialogModal = ({ header, body, footer = '' }) => {
const example = document.createElement('div')

const footerContents =
footer !== ''
? footer
: `
<form method="dialog">
<button class="btn">Cancel</button>
</form>
<button class='btn btn--primary'>Save</button>
`

example.innerHTML = `
<button class="btn" commandfor="my-dialog" command="show-modal">Show Dialog</button>
<dialog id="my-dialog" class="modal" closedby="any">
<div class='modal__header'>
${header}
<form method="dialog">
<button class="btn btn--no-border btn--icon btn--pill">
<span class='material-symbols-outlined icon--x-large'>close</span>
</button>
</form>
</div>
<div class='modal__body'>
${body}
</div>
<div class='modal__footer'>
${footerContents}
</div>
</dialog>
`

return example
}
97 changes: 19 additions & 78 deletions src/stories/Components/Modal/Modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,85 +51,28 @@ Modal can be used as a standalone component, however, it does have a few depende

[\<dialog\> The Dialog Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)

The modal styles will still work using the HTML dialog element. Simply add the `modal` class to the dialog element. No need for the wrapper classes or the active class.
The modal styles work well using the HTML dialog element. Simply add the `modal` class to the dialog element. No need for the wrapper classes or the active class.

```html
<dialog id="my-dialog" class="modal">
<div class="modal__header">
<span>Favorite animal:</span>
<button id="dialog-close" class="btn btn--icon btn--pill">
<span class="material-symbols-outlined icon--x-large">close</span>
</button>
</div>
<div class="modal__body">
<p>Red panda</p>
</div>
<div class="modal__footer">
<button id="dialog-cancel" class="btn">Cancel</button>
<button id="dialog-confirm" class="btn btn--primary">Submit</button>
</div>
</dialog>
```

You can then use Javascript to open the dialog:

```javascript
const dialog = document.getElementById('my-dialog')
const showButton = document.getElementById('show-dialog')
const closeButton = document.getElementById('dialog-close')
const cancelButton = document.getElementById('dialog-cancel')

// This handles the escape key as well as other events
// This is a bit of a gotcha to get the dialog to close and animate correctly
// Normally you would just call dialog.close() but this will not fade the backdrop correctly.
dialog.addEventListener('cancel', (event) => {
event.preventDefault()

dialog.classList.add('modal--closing') // run animation here

dialog.addEventListener(
'animationend',
() => {
dialog.classList.remove('modal--closing')
dialog.close() // then run the default close method
},
{ once: true }
) // add this to prevent bugs when reopening the modal
})

// This handles clicking outside the dialog
dialog.addEventListener('click', (event) => {
//This prevents issues with forms
if (event.target.tagName !== 'DIALOG') {
return
}
<Canvas of={ModalStories.Dialog} />

const rect = event.target.getBoundingClientRect()
To open a `<dialog>` modally by clicking a `<button>` you typically need an onclick handler that calls the showModal method on that `<dialog>`.

const clickedInDialog =
rect.top <= event.clientY &&
event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX &&
event.clientX <= rect.left + rect.width
```html
<button onclick="document.querySelector('#my-dialog').showModal();">Show Dialog</button>
<dialog id="my-dialog">…</dialog>
```

console.log(clickedInDialog)
With [invoker commands](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API), newly available in all browsers though only as of December 12, 2025, buttons can now perform actions on other elements declaratively, without the need for any JavaScript.

if (clickedInDialog === false) {
event.target.dispatchEvent(new Event('cancel'))
}
})

showButton.addEventListener('click', () => {
dialog.showModal()
})
```html
<button commandfor="my-dialog" command="show-modal">Show Dialog</button>
<dialog id="my-dialog">…</dialog>
```

closeButton.addEventListener('click', () => {
dialog.dispatchEvent(new Event('cancel'))
})
If you want to use the invoker approach in your project today and ensure backwards compatibility, you can use a polyfill.

cancelButton.addEventListener('click', () => {
dialog.dispatchEvent(new Event('cancel'))
})
```js
import 'https://esm.sh/invokers-polyfill'
```

## Modal API
Expand All @@ -138,20 +81,18 @@ Styles are built on CSS variables scoped to the modal.

Here are the variables that can be customized:

{/* prettier-ignore-start */}
```css
/* base tokens */
--op-transition-modal-time
--op-transition-modal

/* modal-wrapper */
--_op-modal-backdrop-active-opacity

/* modal */
--_op-modal-width
--_op-modal-max-height

/* dialog.modal::backdrop */
--op-color-black
--_op-modal-backdrop-active-opacity
```
{/* prettier-ignore-end */}

## Customizing Modal styles

Expand Down
8 changes: 8 additions & 0 deletions src/stories/Components/Modal/Modal.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ export const Inline = {
inlineDemo: true,
},
}

export const Dialog = {
args: {
header: 'Modal Title',
body: 'This is the contents of the modal!',
dialog: true,
},
}