Dialogs and modals
The common questions are built in: confirm, alert, prompt and ask, plus a titled modal, <xo-dialog heading>. They are the browser's own <dialog>: the page behind is inert, the focus stays inside, Escape answers "no". Import @xoong/ui and they work; there is nothing to configure.
Which one to use
| You want to | Use | Blocks the page |
|---|---|---|
| Ask yes or no before something that cannot be undone | confirm(), or xo-confirm on a button | Yes |
| Tell the person something they must see (info, success, warning, error) | alert() | Yes |
| Ask for one line of text | prompt() | Yes |
| Offer two or three named choices (Save, Don't save, Keep editing) | ask() | Yes |
| Show a form or any content in a window | <xo-dialog heading> | Yes |
| Say "done" without asking anything, maybe with Undo | A toast: xo-notice or toast.show() | No |
| Show a small panel next to a button | <xo-popover> | No |
| List actions for one item | <xo-menu> | No |
| Explain an icon button on hover or focus | xo-tooltip | No |
Rule of thumb: block the page only when the person must decide or must see it. Everything that can be undone is better as a toast with Undo than as a question.
Confirmation
confirm(message, options) resolves to true or false. The focus starts on Cancel, the safe choice. With no kind the confirming button is red, as for a delete. A kind of warning, info or success adds an icon and colours the button to match; title adds a heading and detail a second, quieter line.
Answer:
<p class="toolbar">
<button data-demo="confirm-plain">Plain</button>
<button data-demo="confirm-danger">Danger</button>
<button data-demo="confirm-warning">Warning</button>
<button data-demo="confirm-info">Info</button>
</p>
<p class="hint">Answer: <output id="out-confirm">nothing yet</output></p>import { confirm } from '@xoong/ui';
if (await confirm('The 14 articles in it are deleted too.', {
kind: 'danger', // 'danger' | 'warning' | 'info' | 'success'
title: 'Delete project?',
ok: 'Delete', // the text of the confirming button
cancel: 'Keep it',
detail: 'You can restore it for 30 days.',
})) {
// go on
}On a button that runs an operation, the same question is an attribute. The extra attributes tune the dialog; the button is red unless xo-confirm-kind says otherwise.
<button xo-click="deletePerson" xo-confirm="Delete {name}? Their posts stay."
xo-confirm-title="Delete person" xo-confirm-ok="Delete" xo-confirm-kind="danger" xo-notice="Deleted">Delete</button>
<button xo-click="publishPost" xo-confirm="Subscribers get an e-mail." xo-confirm-kind="info"
xo-confirm-title="Publish now?" xo-confirm-ok="Publish">Publish</button>Alert: info, success, warning, error
alert(message, options) shows one OK button and resolves when it is closed. kind defaults to info; use success, warning or danger (also written error). The focus starts on OK. An alert is announced as an alertdialog with the title as its name and the message as its description.
Result:
<p class="toolbar">
<button data-demo="alert-info">Info</button>
<button data-demo="alert-success">Success</button>
<button data-demo="alert-warning">Warning</button>
<button data-demo="alert-error">Error</button>
</p>
<p class="hint">Result: <output id="out-alert">nothing yet</output></p>import { alert } from '@xoong/ui';
await alert('We e-mail you when the file is ready.', { title: 'Export started' }); // info
await alert('Your changes are live.', { kind: 'success', title: 'Published' });
await alert('Two of the five images have no description.', { kind: 'warning', title: 'Saved with warnings' });
await alert('The file is larger than 10 MB.', { kind: 'error', title: 'Upload failed', detail: 'Choose a smaller file.' });Prompt
prompt(message, options) asks for a line of text: it resolves with the text, or null when cancelled. The field is a native input, so type, required and pattern are checked by the browser, Enter submits, and an empty required field keeps the dialog open and marks the field invalid.
Answer:
<p class="toolbar">
<button data-demo="prompt">Name a view</button>
<button data-demo="prompt-required">Required web address</button>
</p>
<p class="hint">Answer: <output id="out-prompt">nothing yet</output></p>import { prompt } from '@xoong/ui';
const name = await prompt('Name this view', { title: 'Save view', label: 'Name', value: 'My view' });
if (name !== null) save(name); // null means cancelled
const href = await prompt('Where should the link point?', { label: 'Address', type: 'url', required: true });Choices
ask(message, { buttons }) offers any set of choices and resolves with the value of the one pressed. Mark one button primary (the filled one), one danger if it destroys something, and one cancel: Escape answers with its value, and the focus starts on it. Without a cancel button, Escape resolves null.
Answer:
<p class="toolbar"><button data-demo="ask">Leave with unsaved changes</button></p>
<p class="hint">Answer: <output id="out-ask">nothing yet</output></p>import { ask } from '@xoong/ui';
const answer = await ask('You have unsaved changes.', {
kind: 'warning',
title: 'Leave this page?',
buttons: [
{ label: "Don't save", value: 'discard', danger: true },
{ label: 'Keep editing', value: 'stay', cancel: true },
{ label: 'Save and leave', value: 'save', primary: true },
],
});
// 'discard' | 'stay' | 'save'A titled modal
Give <xo-dialog> a heading and it becomes a window: a header with the title and a Close button, the content as the body, and anything marked slot="footer" as the footer. size is sm, the default, wide or full; on a phone it fills the screen. dismiss closes it on a click outside it, which is right for a harmless dialog and wrong for a form with unsaved input, so it is off by default.
A footer button outside the form still submits it with form="id"; with method="dialog" the form closes the dialog and the button's value is the answer.
Closed by:
Press Ctrl + K for the command palette.
Wide dialogs suit tables and side-by-side content. The body scrolls when it is taller than the window; the header and footer stay put.
<p class="toolbar">
<button xo-open="#d-edit">Edit person</button>
<button xo-open="#d-small">Small, click outside closes</button>
<button xo-open="#d-wide">Wide</button>
</p>
<p class="hint">Closed by: <output id="out-modal">nothing yet</output></p>
<xo-dialog id="d-edit" heading="Edit person" data-out="out-modal">
<form method="dialog" id="edit-form" class="xo-fields">
<label>Name <input name="name" value="Ada Lovelace" autocomplete="off"></label>
<label>E-mail <input type="email" name="email" value="ada@example.org" autocomplete="off"></label>
</form>
<button slot="footer" type="button" xo-close>Cancel</button>
<button slot="footer" type="submit" form="edit-form" value="saved" class="primary">Save</button>
</xo-dialog>
<xo-dialog id="d-small" heading="Keyboard shortcuts" size="sm" dismiss data-out="out-modal">
<p>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> for the command palette.</p>
<button slot="footer" type="button" xo-close value="got it" class="primary">Got it</button>
</xo-dialog>
<xo-dialog id="d-wide" heading="Release notes" size="wide" data-out="out-modal">
<p>Wide dialogs suit tables and side-by-side content. The body scrolls when it is taller than the window; the header and footer stay put.</p>
<button slot="footer" type="button" xo-close>Close</button>
</xo-dialog>// xo-open="#id" opens it from markup; from JavaScript, open() resolves with the answer
const answer = await document.querySelector('#d-edit').open(); // 'saved' | '' (Escape, Cancel, Close)A form made by xoong goes in the same way: put <xo-form op="createPerson"> in the body and the dialog closes by itself when the form succeeds (xo-done). <xo-dialog> without a heading stays a plain wrapper around whatever you put in it, as before.
Notices that need no answer
If nothing has to be decided, do not block the page. xo-notice="Saved" on a button or form raises a toast after success; from JavaScript use toast.show(), with an action such as Undo.
<p class="toolbar"><button data-demo="toast">Move to the trash</button></p>import { toast } from '@xoong/ui';
toast.show({ message: 'Article moved to the trash', action: { label: 'Undo', run: () => restore() } });A failed action can offer the same kind of button. Listen for xo-error and set event.detail.action = { label: 'Retry', run } (and event.detail.timeout when the toast should stay a different length) before the toast draws it, one microtask later.
Behaviour and accessibility
- Native. Each dialog is a
<dialog>opened withshowModal(): it is in the top layer, the page behind is inert, Tab stays inside, and Escape closes it. When it closes the browser returns the focus to the element that opened it. - Names. The built-in dialogs are
role="alertdialog". The title (or the message, when there is no title) is the name and the message is the description.<xo-dialog heading>is named by its heading. - Where the focus starts. Cancel for a question, OK for a notice, the field for a prompt, the
cancelbutton (else the primary one) forask(). - One at a time. A built-in dialog that opens while another is showing answers the first one "no" and takes its place. Modals made with
<xo-dialog>stack like any native modals. - The icon is decoration. The kind is conveyed by the icon, the colour and the words you write, never by colour alone.
- Without
@xoong/uithe question behindxo-confirmfalls back to the browser's ownconfirm(), which headless browsers dismiss silently, so import it.
Texts and languages
Every built-in text is a catalog key (xoong.confirm.ok, xoong.confirm.cancel, xoong.dialog.ok, xoong.dialog.close), Vietnamese included. For one call pass ok and cancel; for the whole app set confirmLabels.ok = 'Delete'.
Your own dialog
To draw the question yourself, for xo-confirm and for the leave-this-page question of a dirty form, install a replacement; resolve to true to go on.
import { setConfirm } from '@xoong/ui';
setConfirm(async (message, context) => {
// context: { kind: 'action', element } for xo-confirm, { kind: 'leave', forms } for the leave guard
return myDialog(message);
});In tests
Happy-dom has no showModal(); xoong then sets the open attribute instead, so a test finds dialog.xo-confirm, reads its text and clicks its buttons like any other markup: screen.$('dialog.xo-confirm .xo-confirm-ok').click().