Skip to content

Commit c42d3c2

Browse files
Prithvi-Webclaude
andcommitted
fix(a11y): each Clean Up pane's working sentence is said in a live region that is in the sheet from the start - it stood only in the aria-label of a role="status" element that one innerHTML write put on screen already filled, and a screen reader announces a live region's text as it changes, not its label, and seldom a region that appears filled (review F3.9 risk)
The Clean Up sheet now keeps #cleanWorking (sr-only, role="status", aria-live="polite", aria-atomic="true") in its markup. Once Smart suggestions, Empty folders, Shrink video or Find writes its working rows, cleanSayWorking reads their sentence from the rows' own label and puts it in the region's text a frame later, emptying the region first so the same sentence said again is a change a screen reader hears. Once those rows are gone (the pane answered, failed or says it waits) the region is emptied, so it never says a pane is working after it stopped; a newer pane's rows take the region over. The rows keep their label, so the pane says what it is doing when it is reached by reading. treeScanKey runs the four panes, the page's own cleanSayWorking, a stub MutationObserver and frames by hand: each sentence reaches the region's text and leaves with its rows; asked again it is emptied and said again; of two panes at work only the newer one's sentence is said, and the older one answering does not empty it. Mutants F39-a (no region in the markup), b (the sentence never set), c (set at once, not a frame later), c2 (not emptied first), d (never emptied), e (an older pane's frame saying its sentence), f (an older pane's watch left running), g1-g4 (each pane's call removed), i (Smart's request not out), each RED, restored by SHA-256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 79c64a0 commit c42d3c2

4 files changed

Lines changed: 199 additions & 4 deletions

File tree

‎public/index.html‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4968,6 +4968,12 @@ <h3 id="cleanTitle"><span data-icon="sparkles"></span>Clean Up</h3>
49684968
<!-- Where what Clean Up removes goes: the system Trash, or a cloud account's own trash.
49694969
Written by the Clean Up button for the scan the sheet opens on (cleanUpWhereItGoes). -->
49704970
<div class="muted" id="cleanWhereNote" style="margin-bottom:6px;"></div>
4971+
<!-- What a pane is working on, said aloud (review F3.9): a pane's working
4972+
rows carry their sentence as a label, which a screen reader does not
4973+
announce. This region is in the sheet from the start; cleanSayWorking
4974+
puts the sentence in its text once the rows are on screen, and takes
4975+
it out once they are gone. -->
4976+
<div id="cleanWorking" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
49714977
<div id="cleanPaneRules" class="pane-fade">
49724978
<!-- v4 §4.5 — the ladder: a saved view is a Clean Up rule, and a Clean
49734979
Up rule is an Autopilot policy. One matching engine throughout. -->
@@ -39340,6 +39346,36 @@ <h2><span style="display:inline-flex;">${icon('diff', 13)}</span>${cmpWhenPair(r
3934039346
if (cleanOpen()) void load();
3934139347
}
3934239348

39349+
/* ── What a pane is working on, said aloud (review F3.9) ──
39350+
A pane's working rows (skeletonRows) carry their sentence as the aria-label
39351+
of a role="status" element that one innerHTML write puts on screen already
39352+
filled: a screen reader announces a live region's text as it changes, not
39353+
its label, and seldom a region that appears filled. So the sentence, read
39354+
from those rows, goes into the text of the sheet's own live region,
39355+
#cleanWorking, there from the start: a frame after the rows are written,
39356+
the region emptied first, so the same sentence said again is a change too.
39357+
Once the rows are gone — the pane answered, failed or says it waits — the
39358+
region is emptied, so it never says a pane is working after it stopped. A
39359+
newer pane's rows take the region over. */
39360+
let cleanWorkingFor = null; // { rows, watch }: the working rows the region speaks for, and what sees them go
39361+
function cleanSayWorking(host) {
39362+
const live = $('cleanWorking');
39363+
const rows = host.firstElementChild;
39364+
const sentence = rows && rows.getAttribute('aria-label');
39365+
if (!live || !sentence) return;
39366+
if (cleanWorkingFor) cleanWorkingFor.watch.disconnect();
39367+
const watch = new MutationObserver(() => {
39368+
if (rows.isConnected) return;
39369+
watch.disconnect();
39370+
cleanWorkingFor = null;
39371+
live.textContent = '';
39372+
});
39373+
watch.observe(host, { childList: true });
39374+
cleanWorkingFor = { rows, watch };
39375+
live.textContent = '';
39376+
requestAnimationFrame(() => { if (cleanWorkingFor && cleanWorkingFor.rows === rows) live.textContent = sentence; });
39377+
}
39378+
3934339379
/**
3934439380
* The tree on screen changed, or whether a scan of it is coming (TOPIC.scan;
3934539381
* a scan starting, stopped or failing — beginScanChrome, stopScan, failScan,
@@ -39430,6 +39466,8 @@ <h2><span style="display:inline-flex;">${icon('diff', 13)}</span>${cmpWhenPair(r
3943039466
if (!scanId) { videoCandidates = []; host.innerHTML = cleanNoScanHtml('video'); return; }
3943139467
// Labelled, so assistive technology hears that the pane is working, not bare shimmer rows (review F3.9).
3943239468
host.innerHTML = skeletonRows(4, 34, 'Finding videos worth re-encoding…');
39469+
// And said aloud (cleanSayWorking). Guarded: a page reader may lift this function without it.
39470+
if (typeof cleanSayWorking === 'function') cleanSayWorking(host);
3943339471
let data;
3943439472
try { data = await api('/api/compression/candidates?scanId=' + scanId); }
3943539473
catch (e) {
@@ -39627,6 +39665,7 @@ <h2><span style="display:inline-flex;">${icon('diff', 13)}</span>${cmpWhenPair(r
3962739665
const scanId = treeScanId();
3962839666
if (!scanId) { emptyFolders = []; host.innerHTML = cleanNoScanHtml('empty'); updateCleanSummary(); return; }
3962939667
host.innerHTML = skeletonRows(4, 30, 'Finding empty folders…'); // labelled for assistive technology (review F3.9)
39668+
if (typeof cleanSayWorking === 'function') cleanSayWorking(host); // and said aloud (guarded, as above)
3963039669
try {
3963139670
const data = await api(`/api/empty-folders?scanId=${scanId}&ignoreJunk=${$('emptyIgnoreJunk').checked}`);
3963239671
if (seq !== loadEmptyFoldersSeq) return; // a newer load owns the list
@@ -40217,6 +40256,7 @@ <h2><span style="display:inline-flex;">${icon('diff', 13)}</span>${cmpWhenPair(r
4021740256
// Working from here, while its sections are asked before it: a notice that it waits for the scan stood
4021840257
// through their requests once the scan had finished (review F2.9) — in words assistive technology reads (F3.9).
4021940258
host.innerHTML = skeletonRows(4, 34, 'Working out what Clean Up suggests…');
40259+
if (typeof cleanSayWorking === 'function') cleanSayWorking(host); // and said aloud (guarded, as above)
4022040260
void renderGitRepos();
4022140261
const hadPackages = await renderPackageOrphans();
4022240262
const hadBrowsers = await renderBrowserProfiles();
@@ -40504,7 +40544,11 @@ <h2><span style="display:inline-flex;">${icon('diff', 13)}</span>${cmpWhenPair(r
4050440544
// A list with none of its own — blank, or saying it waits for the scan, which has finished — says it is
4050540545
// working (review F2.9). Results already listed stand until the new ones replace them, as before.
4050640546
const working = cleanFindNoList;
40507-
if (working) { cleanFindNoList = false; $('cleanResults').innerHTML = skeletonRows(4, 30, 'Finding files that match…'); } // labelled (review F3.9)
40547+
if (working) {
40548+
cleanFindNoList = false;
40549+
$('cleanResults').innerHTML = skeletonRows(4, 30, 'Finding files that match…'); // labelled (review F3.9)
40550+
if (typeof cleanSayWorking === 'function') cleanSayWorking($('cleanResults')); // and said aloud (guarded, as above)
40551+
}
4050840552
/** Matched again against the scan the tree now on screen came from, the list still holding none of its own. */
4050940553
const askAgain = () => { if (working) cleanFindNoList = true; cleanAskAgain(() => runCleanFind(fetcher)); };
4051040554
const findBtn = $('cleanFindBtn');

‎src/ui/app/220-clean-up-modal.js‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,36 @@ function cleanAskAgain(load) {
104104
if (cleanOpen()) void load();
105105
}
106106

107+
/* ── What a pane is working on, said aloud (review F3.9) ──
108+
A pane's working rows (skeletonRows) carry their sentence as the aria-label
109+
of a role="status" element that one innerHTML write puts on screen already
110+
filled: a screen reader announces a live region's text as it changes, not
111+
its label, and seldom a region that appears filled. So the sentence, read
112+
from those rows, goes into the text of the sheet's own live region,
113+
#cleanWorking, there from the start: a frame after the rows are written,
114+
the region emptied first, so the same sentence said again is a change too.
115+
Once the rows are gone — the pane answered, failed or says it waits — the
116+
region is emptied, so it never says a pane is working after it stopped. A
117+
newer pane's rows take the region over. */
118+
let cleanWorkingFor = null; // { rows, watch }: the working rows the region speaks for, and what sees them go
119+
function cleanSayWorking(host) {
120+
const live = $('cleanWorking');
121+
const rows = host.firstElementChild;
122+
const sentence = rows && rows.getAttribute('aria-label');
123+
if (!live || !sentence) return;
124+
if (cleanWorkingFor) cleanWorkingFor.watch.disconnect();
125+
const watch = new MutationObserver(() => {
126+
if (rows.isConnected) return;
127+
watch.disconnect();
128+
cleanWorkingFor = null;
129+
live.textContent = '';
130+
});
131+
watch.observe(host, { childList: true });
132+
cleanWorkingFor = { rows, watch };
133+
live.textContent = '';
134+
requestAnimationFrame(() => { if (cleanWorkingFor && cleanWorkingFor.rows === rows) live.textContent = sentence; });
135+
}
136+
107137
/**
108138
* The tree on screen changed, or whether a scan of it is coming (TOPIC.scan;
109139
* a scan starting, stopped or failing — beginScanChrome, stopScan, failScan,
@@ -194,6 +224,8 @@ async function loadVideoCandidates() {
194224
if (!scanId) { videoCandidates = []; host.innerHTML = cleanNoScanHtml('video'); return; }
195225
// Labelled, so assistive technology hears that the pane is working, not bare shimmer rows (review F3.9).
196226
host.innerHTML = skeletonRows(4, 34, 'Finding videos worth re-encoding…');
227+
// And said aloud (cleanSayWorking). Guarded: a page reader may lift this function without it.
228+
if (typeof cleanSayWorking === 'function') cleanSayWorking(host);
197229
let data;
198230
try { data = await api('/api/compression/candidates?scanId=' + scanId); }
199231
catch (e) {
@@ -391,6 +423,7 @@ async function loadEmptyFolders() {
391423
const scanId = treeScanId();
392424
if (!scanId) { emptyFolders = []; host.innerHTML = cleanNoScanHtml('empty'); updateCleanSummary(); return; }
393425
host.innerHTML = skeletonRows(4, 30, 'Finding empty folders…'); // labelled for assistive technology (review F3.9)
426+
if (typeof cleanSayWorking === 'function') cleanSayWorking(host); // and said aloud (guarded, as above)
394427
try {
395428
const data = await api(`/api/empty-folders?scanId=${scanId}&ignoreJunk=${$('emptyIgnoreJunk').checked}`);
396429
if (seq !== loadEmptyFoldersSeq) return; // a newer load owns the list
@@ -981,6 +1014,7 @@ async function loadSmartSuggestions() {
9811014
// Working from here, while its sections are asked before it: a notice that it waits for the scan stood
9821015
// through their requests once the scan had finished (review F2.9) — in words assistive technology reads (F3.9).
9831016
host.innerHTML = skeletonRows(4, 34, 'Working out what Clean Up suggests…');
1017+
if (typeof cleanSayWorking === 'function') cleanSayWorking(host); // and said aloud (guarded, as above)
9841018
void renderGitRepos();
9851019
const hadPackages = await renderPackageOrphans();
9861020
const hadBrowsers = await renderBrowserProfiles();
@@ -1268,7 +1302,11 @@ async function runCleanFind(fetcher) {
12681302
// A list with none of its own — blank, or saying it waits for the scan, which has finished — says it is
12691303
// working (review F2.9). Results already listed stand until the new ones replace them, as before.
12701304
const working = cleanFindNoList;
1271-
if (working) { cleanFindNoList = false; $('cleanResults').innerHTML = skeletonRows(4, 30, 'Finding files that match…'); } // labelled (review F3.9)
1305+
if (working) {
1306+
cleanFindNoList = false;
1307+
$('cleanResults').innerHTML = skeletonRows(4, 30, 'Finding files that match…'); // labelled (review F3.9)
1308+
if (typeof cleanSayWorking === 'function') cleanSayWorking($('cleanResults')); // and said aloud (guarded, as above)
1309+
}
12721310
/** Matched again against the scan the tree now on screen came from, the list still holding none of its own. */
12731311
const askAgain = () => { if (working) cleanFindNoList = true; cleanAskAgain(() => runCleanFind(fetcher)); };
12741312
const findBtn = $('cleanFindBtn');

‎src/ui/markup/090-modal-cleanup.html‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ <h3 id="cleanTitle"><span data-icon="sparkles"></span>Clean Up</h3>
1616
<!-- Where what Clean Up removes goes: the system Trash, or a cloud account's own trash.
1717
Written by the Clean Up button for the scan the sheet opens on (cleanUpWhereItGoes). -->
1818
<div class="muted" id="cleanWhereNote" style="margin-bottom:6px;"></div>
19+
<!-- What a pane is working on, said aloud (review F3.9): a pane's working
20+
rows carry their sentence as a label, which a screen reader does not
21+
announce. This region is in the sheet from the start; cleanSayWorking
22+
puts the sentence in its text once the rows are on screen, and takes
23+
it out once they are gone. -->
24+
<div id="cleanWorking" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
1925
<div id="cleanPaneRules" class="pane-fade">
2026
<!-- v4 §4.5 — the ladder: a saved view is a Clean Up rule, and a Clean
2127
Up rule is an Autopilot policy. One matching engine throughout. -->

0 commit comments

Comments
 (0)