Utilities

LiveAnnouncer

A hidden aria-live region the page writes messages to with live_announcer_say(), politely or at once.

#utility#accessibility#notification#interactive

Demo

Preview

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.qwe
main.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>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.