Overlays

PromptDialog

A modal asking for one text value with OK and Cancel, on the browser's own <dialog>; Enter submits, Escape cancels.

#dialog#overlay#input#forms#interactive

Demo

Preview

Rename file

Reject this request

The reason goes to the person who asked.

File: notes.txt. Nothing yet.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/prompt_dialog.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/prompt_dialog.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/prompt_dialog.qwe
component PromptDialogDemoLive() {    state name = "notes.txt"    state asking = false    state said = "Nothing yet."    <div.demo-stack>        <div.demo-row>            <PromptDialog id="prompt-demo-name" title="Rename file" label="New name" value={name} trigger="Rename" hint="Keep the ending."                          on_submit={(t) => { name = t; said = `Renamed to ${t}.` }} on_cancel={() => { said = "Rename cancelled." }}/>            <Button variant="soft" on_click={() => { asking = true }}>Reject (from state)</Button>            <PromptDialog id="prompt-demo-reason" title="Reject this request" label="Reason" placeholder="Tell them why" open={asking}                          on_submit={(t) => { asking = false; said = `Rejected: ${t}` }} on_cancel={() => { asking = false; said = "Not rejected." }}>                <p>The reason goes to the person who asked.</p>            </PromptDialog>        </div>        <p.demo-note>File: {name}. {said}</p>    </div>}view PromptDialogDemo() = <PromptDialogDemoLive/>

Source

ui/prompt_dialog.qwe
// PromptDialog: a modal that asks for one text value (a name for the file, the// reason, a code) with OK and Cancel, on the browser's own <dialog>: focus stays in// it, Escape cancels, Enter submits. The field is required unless told otherwise.////   <PromptDialog title="Rename file" label="New name" value={name} trigger="Rename" on_submit={(t) => { rename(t) }}/>//   <PromptDialog title="Why?" label="Reason" open={asking} on_submit={(t) => { reject(t) }} on_cancel={() => { asking = false }}/>//// title: its heading, and its name for screen readers. label: the field's label.// value: what the field starts with; placeholder, hint, required (true), kind// ("text", or "email", "number", "url", "password") as for a field. children: text// above the field. trigger: the text of a button that opens it (styled as a Button// when button_css is on the page). Inside one of your components, open it from your// own state instead: open={...}, and on_cancel() is called when it closes without// an answer (Escape, Cancel, a click outside).// ok ("OK") and cancel ("Cancel"): the buttons' text. id: the field's id ("ui-prompt-dialog"),// unique on the page when there are several. on_submit(text): called with the text// when OK is pressed (then it closes).import ui/theme.qwe use ui_clslet prompt_dialog_css = css`.ui-prompt-dialog-text:empty { display: none; }.ui-prompt-dialog-host { display: contents; }.ui-prompt-dialog {  width: min(440px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 15px/1.5 var(--ui-font);}.ui-prompt-dialog::backdrop { background: var(--ui-overlay); }.ui-prompt-dialog[open] { animation: ui-prompt-dialog-in .16s ease-out; }@keyframes ui-prompt-dialog-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }.ui-prompt-dialog form { display: grid; gap: 14px; padding: 20px 22px 20px; }.ui-prompt-dialog h2 { margin: 0; font-size: 17px; font-weight: 600; line-height: 1.3; }.ui-prompt-dialog-text { margin: -6px 0 0; color: var(--ui-muted); }.ui-prompt-dialog-text > :first-child { margin-top: 0; }.ui-prompt-dialog-text > :last-child { margin-bottom: 0; }.ui-prompt-dialog-field { display: grid; gap: 6px; font-size: 13px; }.ui-prompt-dialog-field > span { font-weight: 500; }.ui-prompt-dialog-input {  box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 12px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1 var(--ui-font);  transition: border-color .15s, box-shadow .15s;}.ui-prompt-dialog-input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-prompt-dialog-input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-prompt-dialog-hint { color: var(--ui-muted); }.ui-prompt-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }.ui-prompt-dialog-btn {  height: 36px; padding: 0 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: transparent;  color: var(--ui-fg); font: 500 14px/1 var(--ui-font); cursor: pointer;}.ui-prompt-dialog-btn:hover { background: var(--ui-subtle); }.ui-prompt-dialog-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-prompt-dialog-ok { border-color: transparent; background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-prompt-dialog-ok:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }@media (prefers-reduced-motion: reduce) { .ui-prompt-dialog[open] { animation: none; } .ui-prompt-dialog-input { transition: none; } }`component PromptDialog(title: str, label: str, value: str = "", placeholder: str? = null, hint: str? = null,                       required: bool = true, kind: str = "text", trigger: str? = null, open: bool = false,                       ok: str = "OK", cancel: str = "Cancel", id: str = "ui-prompt-dialog", on_submit = null, on_cancel = null,                       class: str? = null, children) {    style global prompt_dialog_css    state dlg = null    state text = value    state answered = false    let show = () => {        text = value        answered = false        dlg.showModal()        dlg.querySelector("input").select()    }    if open && dlg != null && !dlg.open { show() }    if !open && dlg != null && dlg.open && trigger == null { dlg.close() }    <div.ui-prompt-dialog-host>        {trigger != null && <button.ui-btn.ui-btn-outline type="button" aria-haspopup="dialog" @click={show}>{trigger}</button>}        <dialog class={ui_cls(["ui-prompt-dialog", class])} aria-label={title}                @mount={(el) => {                    dlg = el                    if open { show() }                }}                @close={() => { if !answered { on_cancel && on_cancel() } }}                @click={(e) => { if e.target.tagName == "DIALOG" { dlg.close() } }}>            <form method="dialog" @submit={(e) => {                answered = true                dlg.close()                on_submit && on_submit(e.form.value)            }}>                <h2>{title}</h2>                <div.ui-prompt-dialog-text>{children}</div>                <label.ui-prompt-dialog-field>                    <span>{label}</span>                    <input.ui-prompt-dialog-input type={kind} id={id} name="value" value={text} placeholder={placeholder} required={required}                           autocomplete="off" aria-describedby={hint != null ? `${id}-hint` : null}                           @input={(e) => { text = e.value }}/>                    {hint != null && <span.ui-prompt-dialog-hint id={`${id}-hint`}>{hint}</span>}                </label>                <div.ui-prompt-dialog-actions>                    <button.ui-prompt-dialog-btn type="button" @click={() => dlg.close()}>{cancel}</button>                    <button.ui-prompt-dialog-btn.ui-prompt-dialog-ok type="submit">{ok}</button>                </div>            </form>        </dialog>    </div>}
Thread
45Useful? Like it, or add to its thread below.

Thread 0 posts

On PromptDialog: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.