Say how many search results there are, in a combobox
M3's search accessibility page asks that results be announced when they appear and read as a list. The bar now wraps its input in a role="combobox" that carries aria-expanded and aria-controls — ARIA gives a bare textbox neither — the results container is a role="list", and a polite live region counts the results whenever the view's DOM settles. Plan step 13, finding IN-01. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0adfb6fff4
commit
31663c48ba
+46
-1
@@ -7,6 +7,11 @@
|
||||
* compact window (below `medium`, 600px) the view is full screen and modal: focus stays in it and
|
||||
* the page behind does not scroll — M3 docks the view from medium upwards. The results themselves
|
||||
* are the caller's, rendered by Livewire into the view as the query changes.
|
||||
*
|
||||
* Because the results arrive from the server, nothing in the page tells a screen reader they are
|
||||
* there; M3 asks that it be told. A MutationObserver counts the list items whenever the view's DOM
|
||||
* settles and writes "N results" into the polite live region the view renders, which is the one
|
||||
* announcement M3's search accessibility page names.
|
||||
*/
|
||||
import { upTo } from './breakpoints.js'
|
||||
|
||||
@@ -19,17 +24,57 @@ const CHOOSES = 'a[href], button:not([disabled]), [data-list-open]'
|
||||
// coming to search.
|
||||
const RETURN_GUARD_MS = 250
|
||||
|
||||
// A Livewire morph replaces the results in several mutations; wait for the batch to end before
|
||||
// counting, so the live region speaks once.
|
||||
const SETTLE_MS = 120
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialSearch', (docked = false) => ({
|
||||
window.Alpine.data('materialSearch', (docked = false, announce = {}) => ({
|
||||
open: false,
|
||||
compact: false,
|
||||
closedAt: -Infinity,
|
||||
announcement: '',
|
||||
observer: null,
|
||||
settle: null,
|
||||
|
||||
init() {
|
||||
const query = upTo('medium')
|
||||
|
||||
this.compact = query.matches
|
||||
query.addEventListener('change', (event) => (this.compact = event.matches))
|
||||
|
||||
this.observer = new MutationObserver(() => this.countLater())
|
||||
this.observer.observe(this.$refs.view, { childList: true, subtree: true, characterData: true })
|
||||
|
||||
this.$watch('open', () => this.countLater())
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.observer?.disconnect()
|
||||
clearTimeout(this.settle)
|
||||
},
|
||||
|
||||
countLater() {
|
||||
clearTimeout(this.settle)
|
||||
this.settle = setTimeout(() => this.count(), SETTLE_MS)
|
||||
},
|
||||
|
||||
count() {
|
||||
if (! this.open) {
|
||||
this.announcement = ''
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const results = this.$refs.view.querySelector('[data-search-results]')
|
||||
const items = results ? results.querySelectorAll('[role="listitem"], li') : []
|
||||
const total = items.length || (results ? this.results().length : 0)
|
||||
|
||||
this.announcement = total === 0
|
||||
? (announce.none ?? '')
|
||||
: total === 1
|
||||
? (announce.one ?? '')
|
||||
: (announce.many ?? '').replace(':count', total)
|
||||
},
|
||||
|
||||
get fullScreen() {
|
||||
|
||||
Reference in New Issue
Block a user