LiveAnnouncer
A hidden aria-live region the page writes messages to with live_announcer_say(), politely or at once.
Demo
What screen readers would hear, shown here with visible on; the region itself is hidden.
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/live_announcer.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/live_announcer.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/live_announcer.qwe
component LiveAnnouncerDemoLive() { state n = 0 <div.demo-stack> <div.demo-row> <Button id="live-announcer-demo-polite" variant="outline" on_click={() => { n += 1; live_announcer_say(`${n * 3} results for “lisbon”`) }}>Search</Button> <Button id="live-announcer-demo-same" variant="outline" on_click={() => { live_announcer_say("Saved") }}>Save</Button> <Button id="live-announcer-demo-assertive" variant="outline" tone="danger" on_click={() => { live_announcer_say("Could not send: you are offline", "assertive") }}>Fail</Button> </div> </div>}view LiveAnnouncerDemo() = <div.demo-stack> <LiveAnnouncer visible={true} history={4}/> <LiveAnnouncerDemoLive/> <p.demo-note>What screen readers would hear, shown here with visible on; the region itself is hidden.</p> </div>Source
ui/live_announcer.qwe
// LiveAnnouncer: a hidden aria-live region that a page writes messages to, so// screen readers say what changed where nothing visible does: "3 results",// "Item moved to Done", "Saved". Put one <LiveAnnouncer/> on the page; then in// any component's handler://// <LiveAnnouncer/>// live_announcer_say("12 results for “lisbon”")// live_announcer_say("Could not save: you are offline", "assertive")//// live_announcer_say(message, politeness): politeness "polite" (default),// waiting for the reader to finish, or "assertive", interrupting it. The same// message twice in a row is said twice. LiveAnnouncer: visible shows the last// messages on the page too, for testing and for sighted users of a log (false);// history: how many it keeps when visible (5); clear: seconds after which the// region is emptied, so a reader moving through the page does not find stale// text (8; 0 keeps it).import ui/theme.qwe use ui_clslet live_announcer_css = css`.ui-announce-log { display: grid; gap: 4px; margin: 0; padding: 10px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); font: 13px/1.45 var(--ui-mono); color: var(--ui-fg); list-style: none; }.ui-announce-log:empty::before { content: "Nothing announced yet"; color: var(--ui-muted); font-family: var(--ui-font); }.ui-announce-assertive { color: var(--ui-danger); }`// Says a message through the LiveAnnouncer on the page.fn live_announcer_say(message: str, politeness: str = "polite") { js("document.dispatchEvent", js_new("CustomEvent", "ui-announce", { detail: { message, politeness } }))}component LiveAnnouncer(visible: bool = false, history: int = 5, clear: int = 8, class: str? = null) { style global live_announcer_css state polite = "" state assertive = "" state said = [] // { n, message, politeness }, newest last state count = 0 let any_text = polite != "" || assertive != "" if any_text && clear > 0 { every(clear * 1000, () => { polite = ""; assertive = "" }) } <div class={ui_cls(["ui-announce", class])} @mount={(_el) => { let hear = (e) => { let d = e.detail count += 1 // A narrow no-break space after a repeat makes the text differ, so it is read again. let text = str(d.message ?? "") let assert = d.politeness == "assertive" if assert { assertive = assertive == text ? text + " " : text } else { polite = polite == text ? text + " " : text } said = take(reverse(push(said, { n: count, message: text, politeness: assert ? "assertive" : "polite" })), history) | reverse } js("document.addEventListener", "ui-announce", hear) () => js("document.removeEventListener", "ui-announce", hear) }}> <div.ui-sr role="status" aria-live="polite" aria-atomic="true">{polite}</div> <div.ui-sr role="alert" aria-live="assertive" aria-atomic="true">{assertive}</div> {visible && <ul.ui-announce-log aria-hidden="true"> {for m in said => <li key={m.n} class={m.politeness == "assertive" ? "ui-announce-assertive" : null}>{m.message}</li>} </ul>} </div>}Related
KeyboardShortcutsBinds key combinations such as ⌘S or ? to functions on the page, leaving text boxes alone, and lists them.6 views · 8 downloadsOfflineBannerA banner across the top of the page for as long as the browser is offline, gone by itself when the connection is back.5 views · 5 downloadsScrollAreaA box with its own scrollbars and a height limit, fading at the edges where there is more to see.6 views · 7 downloadsConfettiA burst of confetti over the page on a canvas, fired by a button, on mount, or by confetti_fire().6 views · 10 downloadsDraggableA box dragged anywhere in its container with the pointer or the arrow keys, kept inside it, with on_move(x, y).8 views · 7 downloadsIdleTimerCalls on_idle() after a time without mouse, key, touch or scroll, with a warning dialog and a Stay button first.6 views · 7 downloads
Thread
Thread 0 posts
On LiveAnnouncer: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.