Overlays

ShortcutsDialog

A dialog listing the page's keyboard shortcuts in groups, opened with ?, with a box to filter them.

#dialog#keyboard#accessibility#interactive

Demo

Preview

Keyboard shortcuts

General

Open the command palette
Show these shortcuts
Switch project
Close a dialog

Inbox

Next message
Previous message
Archive
Go to the inbox

Editing

Bold
Italic
Undo

Press ? anywhere on the page (outside a text box) to open it.

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/shortcuts_dialog.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/shortcuts_dialog.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/shortcuts_dialog.qwe
view ShortcutsDialogDemo() =    <div.demo-stack>        <div.demo-row>            <ShortcutsDialog groups={[                { title: "General", shortcuts: [                    { keys: "Ctrl K", label: "Open the command palette" }, { keys: "?", label: "Show these shortcuts" },                    { keys: "Ctrl Shift P", label: "Switch project" }, { keys: "Esc", label: "Close a dialog" },                ] },                { title: "Inbox", shortcuts: [                    { keys: "J", label: "Next message" }, { keys: "K", label: "Previous message" },                    { keys: "E", label: "Archive" }, { keys: "G then I", label: "Go to the inbox" },                ] },                { title: "Editing", shortcuts: [                    { keys: "Ctrl B", label: "Bold" }, { keys: "Ctrl I", label: "Italic" }, { keys: "Ctrl Z", label: "Undo" },                ] },            ]}/>        </div>        <p.demo-note>Press ? anywhere on the page (outside a text box) to open it.</p>    </div>

Source

ui/shortcuts_dialog.qwe
// ShortcutsDialog: the list of a page's keyboard shortcuts, in groups, opened by// pressing ? anywhere (outside a text box) or by its button. A box at the top filters// them as one types. It is the browser's own <dialog>, so focus stays in it and Escape// closes it. Each combination is drawn as keys, and given to screen readers whole.////   <ShortcutsDialog groups={[//       { title: "General", shortcuts: [{ keys: "Ctrl K", label: "Open the command palette" }, { keys: "?", label: "Show shortcuts" }] },//       { title: "Inbox", shortcuts: [{ keys: "J", label: "Next message" }, { keys: "G then I", label: "Go to the inbox" }] },//   ]}/>//// groups: a list of { title, shortcuts }, each shortcut { keys, label }: keys are// written with spaces between keys pressed together ("Ctrl Shift P"), and "then"// between keys pressed one after the other ("G then I").// title: its heading ("Keyboard shortcuts"). trigger: the text of a button that opens// it ("Shortcuts"; styled as a Button when button_css is on the page), null for none.// hotkey: ? opens it (true); give only one on a page the hotkey. searchable: the box// to filter them (true). empty: what it says when nothing matches ("No shortcuts match.").import ui/theme.qwe use ui_clslet shortcuts_dialog_css = css`.ui-shortcuts-dialog-host { display: contents; }.ui-shortcuts-dialog {  width: min(720px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); 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: 14px/1.45 var(--ui-font);}.ui-shortcuts-dialog[open] { display: flex; flex-direction: column; animation: ui-shortcuts-dialog-in .16s ease-out; }.ui-shortcuts-dialog::backdrop { background: var(--ui-overlay); }@keyframes ui-shortcuts-dialog-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }.ui-shortcuts-dialog-head { display: flex; align-items: center; gap: 12px; flex: none; padding: 16px 18px 12px; border-bottom: 1px solid var(--ui-border); }.ui-shortcuts-dialog-head h2 { flex: 1; margin: 0; font-size: 17px; font-weight: 600; }.ui-shortcuts-dialog-find {  width: 200px; height: 32px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface); color: var(--ui-fg); font: 13px/1 var(--ui-font);}.ui-shortcuts-dialog-find:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-shortcuts-dialog-x {  display: grid; place-items: center; flex: none; width: 30px; height: 30px; padding: 0; border: 0; border-radius: var(--ui-radius-sm);  background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-shortcuts-dialog-x:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-shortcuts-dialog-x:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-shortcuts-dialog-x svg { width: 18px; height: 18px; }.ui-shortcuts-dialog-body { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px 32px; min-height: 0; overflow: auto; padding: 16px 18px 20px; }.ui-shortcuts-dialog-group h3 { margin: 0 0 6px; color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }.ui-shortcuts-dialog-group dl { display: grid; margin: 0; }.ui-shortcuts-dialog-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 7px 0; border-bottom: 1px solid var(--ui-border); }.ui-shortcuts-dialog-row:last-child { border-bottom: 0; }.ui-shortcuts-dialog-row dt { min-width: 0; }.ui-shortcuts-dialog-row dd { display: inline-flex; flex: none; align-items: center; gap: 4px; margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-shortcuts-dialog kbd {  display: inline-block; min-width: 22px; padding: 2px 6px; border: 1px solid var(--ui-border); border-bottom-width: 2px;  border-radius: 4px; background: var(--ui-subtle); color: var(--ui-fg); font: 500 12px/1.4 var(--ui-font); text-align: center;}.ui-shortcuts-dialog-empty { margin: 0; padding: 24px 0; color: var(--ui-muted); text-align: center; grid-column: 1 / -1; }@media (max-width: 520px) { .ui-shortcuts-dialog-find { width: 130px; } }@media (prefers-reduced-motion: reduce) { .ui-shortcuts-dialog[open] { animation: none; } }`// What screen readers hear for keys: "Ctrl+Shift+P", "G, then I".fn shortcuts_dialog_spoken(keys: str) -> str = split(keys, " then ") | map((k) => replace(trim(k), " ", "+")) | join(", then ")component ShortcutsDialog(groups: list = [], title: str = "Keyboard shortcuts", trigger: str? = "Shortcuts", hotkey: bool = true,                          searchable: bool = true, empty: str = "No shortcuts match.", class: str? = null) {    style global shortcuts_dialog_css    state dlg = null    state query = ""    let q = lower(trim(query))    let shown = groups        | map((g) => { title: g.title, shortcuts: (get(g, "shortcuts") ?? []) | filter((s) => q == "" || contains(lower(`${s.label} ${s.keys}`), q)) })        | filter((g) => len(g.shortcuts) > 0)    let show = () => {        query = ""        if !dlg.open { dlg.showModal() }        let box = dlg.querySelector("input")        box && box.focus()    }    <div class={ui_cls(["ui-shortcuts-dialog-host", class])}>        {trigger != null && <button.ui-btn.ui-btn-outline type="button" aria-haspopup="dialog"                                    aria-keyshortcuts={hotkey ? "?" : null} @click={show}>{trigger}</button>}        <dialog.ui-shortcuts-dialog aria-label={title}                @mount={(el) => {                    dlg = el                    let onkey = (e) => {                        let t = e.target                        let typing = t != null && t.closest != null && t.closest("input, textarea, select, [contenteditable]") != null                        if e.key == "?" && !typing && !el.open && !e.ctrlKey && !e.metaKey && !e.altKey {                            e.preventDefault()                            show()                        }                    }                    if hotkey { js("document.addEventListener", "keydown", onkey) }                    () => { if hotkey { js("document.removeEventListener", "keydown", onkey) } }                }}                @click={(e) => { if e.target.tagName == "DIALOG" { dlg.close() } }}>            <div.ui-shortcuts-dialog-head>                <h2>{title}</h2>                {searchable && <input.ui-shortcuts-dialog-find type="search" value={query} placeholder="Filter" aria-label="Filter shortcuts"                                 @input={(e) => { query = e.value }}/>}                <button.ui-shortcuts-dialog-x type="button" aria-label="Close" @click={() => dlg.close()}>                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                         aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>                </button>            </div>            <div.ui-shortcuts-dialog-body>                {for g in shown => <section.ui-shortcuts-dialog-group key={g.title}>                    <h3>{g.title}</h3>                    <dl>                        {for s in g.shortcuts => <div.ui-shortcuts-dialog-row key={s.keys}>                            <dt>{s.label}</dt>                            <dd aria-label={shortcuts_dialog_spoken(s.keys)}>                                {for [i, part] in enumerate(split(s.keys, " then ")) => [                                    i > 0 && <span aria-hidden="true">then</span>,                                    for k in split(trim(part), " ") | filter((k) => k != "") => <kbd aria-hidden="true">{k}</kbd>,                                ]}                            </dd>                        </div>}                    </dl>                </section>}                {len(shown) == 0 && <p.ui-shortcuts-dialog-empty>{empty}</p>}            </div>        </dialog>    </div>}
Thread
58Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.