ShortcutsDialog
A dialog listing the page's keyboard shortcuts in groups, opened with ?, with a box to filter them.
Demo
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.qwemain.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>}Related
SearchOverlayA full-screen search with results as one types, grouped and marked, from a list or a server, opened with /.7 views · 5 downloadsSpotlightTourA guided tour that dims the page and lights one element at a time by id, with a card saying what it is.5 views · 7 downloadsAlertDialogA modal that only informs, with one OK button, on the browser's own <dialog> and no JavaScript.6 views · 7 downloadsCommandPaletteCtrl+K opens a fuzzy search of commands and pages, grouped, with keys, recent picks and the arrow keys.6 views · 8 downloadsConfirmButtonA button that asks first, in a dialog on the page, then submits its form, follows its link or calls back.5 views · 5 downloadsFullscreenModalA dialog taking the whole window, with a bar holding close, its title and a main action, on the browser's <dialog>.5 views · 4 downloads
Thread
Thread 0 posts
On ShortcutsDialog: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.