diff --git a/.changelog/next/fixed-issue-4156.md b/.changelog/next/fixed-issue-4156.md index 90421ca0a8..e8e1bdf8af 100644 --- a/.changelog/next/fixed-issue-4156.md +++ b/.changelog/next/fixed-issue-4156.md @@ -1,2 +1,3 @@ - Screen readers now announce the inline form controls on the Character Sheet, GitHub, Sharing, Browser, Mood Board and Video Timeline Editor pages, which previously had no accessible name - Screen readers now announce 80 more form controls that previously had no accessible name, across the Goals views, the Brain tabs (Daily Log, Feeds, Inbox, Links, Memory, Notes), the MeatSpace tabs (Age, Alcohol, Genome, Lifestyle, Nicotine) and the Agent World/Tools tabs — visible labels where the surface had room, `aria-label` in compact toolbars and table rows +- Named 33 more unlabeled form controls across the Chief of Staff, MeatSpace POST and Voice settings surfaces, and taught the accessible-name guard two wrapper shapes it was blind to — burning the pre-existing allowlist down from 142 to 71 entries diff --git a/client/src/a11yConventions.test.js b/client/src/a11yConventions.test.js index dbe10f6291..d0a61efe29 100644 --- a/client/src/a11yConventions.test.js +++ b/client/src/a11yConventions.test.js @@ -457,46 +457,50 @@ function isUsableLabelAttributeValue(value) { // localLabelWrapperNames: same-file `function` declarations only, so a missed // one is a false negative that leaves its controls on the allowlist. // +// The two prop names are read out of the wrapper rather than hardcoded: the +// forwarded id arrives as `htmlFor` in LifestyleTab's `FieldGroup` but as `id` +// in StackerNews's `Field({ id, label, children })`, and both name their +// control just as well. Hardcoding `htmlFor` left the second shape's controls +// looking unnamed. +// // Both helpers below read whatever source they are handed. The input scan hands // them `maskComments(src)`, which is what keeps a commented-out wrapper — or a // commented-out call site — from registering; same as localLabelWrapperNames. -const htmlForForwarderNamesBySource = new Map(); +const htmlForForwardersBySource = new Map(); -function localHtmlForForwarderNames(src) { - const cached = htmlForForwarderNamesBySource.get(src); +function localHtmlForForwarders(src) { + const cached = htmlForForwardersBySource.get(src); if (cached) return cached; - const names = new Set(); - const re = /function\s+([A-Z][\w]*)\s*\(/g; - let match; - while ((match = re.exec(src))) { - const parenIndex = match.index + match[0].length - 1; - const params = balancedCallAt(src, parenIndex); - if (!params) continue; - const bodyStart = src.indexOf('{', parenIndex + params.length); - if (bodyStart === -1) continue; - const bodyEnd = matchingBraceEnd(src, bodyStart); - if (bodyEnd === -1) continue; - const body = src.slice(bodyStart, bodyEnd); + const forwarders = new Map(); + forEachLocalComponent(src, (name, body, params) => { // The