From 0e79e1052a553902f9f08d40914896fec67cd5e4 Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Sat, 15 Aug 2026 20:06:40 +0000 Subject: [PATCH 1/2] fix: name 33 more form controls and stop the a11y allowlist from rotting (#4156) Continues the unlabeled-control sweep on the Chief of Staff, MeatSpace POST and Voice settings surfaces, and closes two blind spots that were keeping correctly-labeled controls on the allowlist: - The htmlFor-forwarding wrapper recognizer hardcoded a prop named `htmlFor`. StackerNews's `Field({ id, label })` does the same job through `id`, so its 13 already-labeled controls looked unnamed. Both prop names are now read out of the wrapper, and both must still be parameters of it. - A FormField whose only child is a conditional (`{isSelect ? }`) was not credited, though React clones the generated id onto whichever branch renders. Rendered lists stay excluded: Children.map flattens an array and clones only its first element. VoiceTab carried a page-local clone of the shared FormField; it is deleted in favour of the real one, which is what the guard already understands. A new test fails on any allowlist entry that no longer matches an unnamed input, so the burn-down can only shrink. It immediately caught 9 entries left behind by #4313. PREEXISTING_INPUT_NAME_ALLOWLIST: 142 -> 71. --- .changelog/next/fixed-issue-4156.md | 1 + client/src/a11yConventions.test.js | 232 +++++++++++------- client/src/components/cos/TaskAddForm.jsx | 1 + client/src/components/cos/tabs/AgentCard.jsx | 2 + client/src/components/cos/tabs/ConfigRow.jsx | 10 +- client/src/components/cos/tabs/JobsTab.jsx | 7 + .../components/cos/tabs/MemoryEditModal.jsx | 1 + client/src/components/cos/tabs/TaskItem.jsx | 2 + .../cos/tabs/workflow/ScheduleEditor.jsx | 1 + .../meatspace/EpigeneticTracker.jsx | 4 + .../meatspace/post/ElementsSong.jsx | 4 +- .../meatspace/post/MemoryPractice.jsx | 1 + .../meatspace/post/MorseTrainer.jsx | 10 +- .../post/PostCognitiveDrillRunner.jsx | 1 + .../meatspace/post/PostDrillRunner.jsx | 1 + .../meatspace/post/PostLlmDrillRunner.jsx | 3 + .../meatspace/post/WordplayDrillUI.jsx | 2 + client/src/components/settings/VoiceTab.jsx | 111 ++++----- 18 files changed, 242 insertions(+), 152 deletions(-) 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..4b1af047e4 100644 --- a/client/src/a11yConventions.test.js +++ b/client/src/a11yConventions.test.js @@ -457,15 +457,21 @@ 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 forwarders = new Map(); const re = /function\s+([A-Z][\w]*)\s*\(/g; let match; while ((match = re.exec(src))) { @@ -478,25 +484,32 @@ function localHtmlForForwarderNames(src) { if (bodyEnd === -1) continue; const body = src.slice(bodyStart, bodyEnd); // The