Toolbar
A row of controls reached by Tab once and moved through with the arrow keys; it wraps when narrow.
Demo
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.qwemain.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>}Related
ActionBarA page's main action and a line about it, fixed to the bottom of the screen on a phone.13 views · 6 downloadsDockA floating bar of icon buttons fixed to the bottom or a side of the page, with badges and the current one marked.6 views · 7 downloadsAppShellAn app's frame: a sidebar of links, a top bar and the page; the sidebar is a drawer on phones.10 views · 7 downloadsAspectRatioKeeps what it holds at a shape (16/9, 4/3, 1): an image or a video fills a box of the right height.9 views · 7 downloadsBentoGridA grid of tiles spanning columns and rows, with titles, tints and links, packed to leave no holes.9 views · 6 downloadsBrowserFrameA mock browser window around its content, with the address in its bar, for screenshots and demos.7 views · 6 downloads
Thread
Thread 0 posts
On Toolbar: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.