Layout

Toolbar

A row of controls reached by Tab once and moved through with the arrow keys; it wraps when narrow.

#layout#button#no-js

Demo

Preview

Tab reaches the toolbar once; the arrow keys, Home and End move inside it. Narrow, it wraps:

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

Example

The code of the preview above.

demos/toolbar.qwe
view ToolbarDemo() =    <div.demo-stack>        <Toolbar label="Formatting">            <ToolbarGroup label="History">                <Button variant="ghost" tone="neutral" size="sm">Undo</Button>                <Button variant="ghost" tone="neutral" size="sm">Redo</Button>            </ToolbarGroup>            <ToolbarSeparator/>            <select aria-label="Text size">                <option>Paragraph</option><option>Heading 1</option><option>Heading 2</option>            </select>            <ToolbarSeparator/>            <ToggleButtonGroup label="Style" multiple={true} icon_only={true} variant="ghost" size="sm" attached={false}                               options={[{ label: "Bold", value: "b", icon: "B" }, { label: "Italic", value: "i", icon: "I" },                                         { label: "Underline", value: "u", icon: "U" }]}/>            <ToolbarSeparator/>            <ToggleButtonGroup label="Alignment" value="left" variant="ghost" size="sm" attached={false}                               options={[{ label: "Left", value: "left" }, { label: "Center", value: "center" },                                         { label: "Right", value: "right" }]}/>            <ToolbarSpacer/>            <Button size="sm">Publish</Button>        </Toolbar>        <p.demo-note>Tab reaches the toolbar once; the arrow keys, Home and End move inside it. Narrow, it wraps:</p>        <div style="width: min(300px, 100%)">            <Toolbar label="Selected rows">                <Button variant="ghost" tone="neutral" size="sm">Archive</Button>                <Button variant="ghost" tone="neutral" size="sm">Move</Button>                <Button variant="ghost" tone="neutral" size="sm">Label</Button>                <ToolbarSeparator/>                <Button variant="ghost" tone="danger" size="sm">Delete</Button>                <Button variant="ghost" tone="neutral" size="sm" disabled={true}>Merge</Button>            </Toolbar>        </div>        <Toolbar label="Drawing tools" vertical={true}>            <ToggleButtonGroup label="Tool" value="select" variant="ghost" size="sm" attached={false}                               options={["Select", "Pen", "Text"] | map((t) => { label: t, value: lower(t) })}/>            <ToolbarSeparator/>            <Button variant="ghost" tone="neutral" size="sm">Clear</Button>        </Toolbar>    </div>

Source

ui/toolbar.qwe
// Toolbar: a row of controls that act on one thing (an editor's formatting, a// table's actions), as an ARIA toolbar: Tab reaches it once, at the control last// used, and the arrow keys move between its controls (Home and End to the ends).// It wraps onto more lines when it is narrow.////   <Toolbar label="Formatting">//       <ToggleButtonGroup label="Style" multiple={true} icon_only={true} variant="ghost"//                          options={[{ label: "Bold", value: "b", icon: "B" }, { label: "Italic", value: "i", icon: "I" }]}/>//       <ToolbarSeparator/>//       <Button variant="ghost" size="sm">Undo</Button>//       <ToolbarSpacer/>//       <Button size="sm">Publish</Button>//   </Toolbar>//// children: buttons, links, toggle buttons, selects, any controls (and the views// below). label: what screen readers call it. vertical: one control above another,// moved through with the up and down arrows. A text box or select in it keeps its// own arrow keys; Tab leaves it.// ToolbarSeparator: a line between groups of controls. ToolbarGroup(label): controls// that belong together, named for screen readers. ToolbarSpacer: pushes what follows// to the far end.import ui/theme.qwe use ui_clslet toolbar_css = css`.ui-toolbar {  display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 4px; min-width: 0;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface);  color: var(--ui-fg); font: 14px/1.4 var(--ui-font);}.ui-toolbar-vertical { flex-direction: column; align-items: stretch; width: fit-content; }.ui-toolbar-group { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }.ui-toolbar-vertical :is(.ui-toolbar-group, .ui-toggle-group, .ui-button-group) { flex-direction: column; align-items: stretch; }.ui-toolbar-sep { align-self: stretch; flex: none; width: 1px; min-height: 24px; margin: 2px 4px; background: var(--ui-border); }.ui-toolbar-vertical .ui-toolbar-sep { width: auto; height: 1px; min-height: 0; margin: 4px 2px; }.ui-toolbar-spacer { flex: 1 1 0; min-width: 0; }.ui-toolbar :is(select, input:not([type=checkbox]):not([type=radio])) {  height: 30px; padding: 0 8px; 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-toolbar :is(select, input):focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }`// What the arrow keys move between: the toolbar's enabled controls.let toolbar_controls = "button:not(:disabled), a[href], input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role=button]:not([aria-disabled=true])"fn toolbar_items(bar) = js("Array.from", bar.querySelectorAll(toolbar_controls))// Where `el` is in `items`, or null. (`==` on two elements compares their fields, so// two bare buttons look equal: isSameNode tells them apart.)fn toolbar_index(items: list, el) = reduce(enumerate(items), null, (at, p) => p[1].isSameNode(el) ? p[0] : at)view Toolbar(label: str? = null, vertical: bool = false, class: str? = null, children) {    style global toolbar_css    <div class={ui_cls(["ui-toolbar", vertical && "ui-toolbar-vertical", class])} role="toolbar" aria-label={label}         aria-orientation={vertical ? "vertical" : "horizontal"}>        {children}        <ToolbarKeys vertical={vertical}/>    </div>// The arrow keys of the toolbar it is in. (Toolbar is a view, so that components// among its children run in the browser on their own; this one listens for it.)}component ToolbarKeys(vertical: bool = false) {    style global toolbar_css    <span.ui-toolbar-keys hidden @mount={(el) => {        let bar = el.closest("[role=toolbar]")        // Makes `to` the one control Tab reaches.        let rove = (to) => {            for b in toolbar_items(bar) => { b.tabIndex = b.isSameNode(to) ? 0 : -1 }        }        let focused = (e) => {            if toolbar_index(toolbar_items(bar), e.target) != null { rove(e.target) }        }        let keys = (e) => {            let tag = e.target.tagName            let typing = tag == "TEXTAREA" || tag == "SELECT" || tag == "INPUT" && e.target.type != "checkbox" && e.target.type != "radio"            let items = toolbar_items(bar)            let i = toolbar_index(items, e.target)            let n = len(items)            let to = typing || i == null ? null : match e.key {                "ArrowRight" => vertical ? null : (i + 1) % n                "ArrowLeft" => vertical ? null : (i + n - 1) % n                "ArrowDown" => vertical ? (i + 1) % n : null                "ArrowUp" => vertical ? (i + n - 1) % n : null                "Home" => 0                "End" => n - 1                _ => null            }            if to != null {                e.preventDefault()                rove(items[to])                items[to].focus()            }        }        let items = toolbar_items(bar)        if len(items) > 0 { rove(items[0]) }        bar.addEventListener("keydown", keys)        bar.addEventListener("focusin", focused)        () => {            bar.removeEventListener("keydown", keys)            bar.removeEventListener("focusin", focused)        }    }}></span>}view ToolbarSeparator() {    style global toolbar_css    <span.ui-toolbar-sep role="separator"></span>}view ToolbarGroup(label: str? = null, class: str? = null, children) {    style global toolbar_css    <div class={ui_cls(["ui-toolbar-group", class])} role="group" aria-label={label}>{children}</div>}view ToolbarSpacer() {    style global toolbar_css    <span.ui-toolbar-spacer aria-hidden="true"></span>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.