Skip to content

Commit dc5fd87

Browse files
authored
Merge pull request #2591 from IBMa/issue-2540
fix(simulator): Handle the case of links within labels
2 parents 0e4fcc5 + 58a7bcc commit dc5fd87

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

accessibility-checker-engine/src/v4/simulator/SRNavigator.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,16 @@ export namespace SRNavigator {
279279
}
280280
if (elem.nodeName.toUpperCase() === "BODY") return retVal = { skipCurrent: false, skipChildren: false };
281281

282-
// Skip labels that are associated with controls - they'll be read with the related input
282+
// Skip labels that are associated with controls - they'll be read with the related input.
283+
// Keep children so interactive content (e.g. links) inside the label still appears in
284+
// reading/tab order.
283285
if (elem.nodeName.toUpperCase() === "LABEL") {
284286
if (
285287
elem.hasAttribute("for")
286288
&& document.getElementById(elem.getAttribute("for"))
287289
&& document.getElementById(elem.getAttribute("for")).getAttribute("type") !== "hidden"
288290
) {
289-
return retVal = { skipCurrent: true, skipChildren: true };
291+
return retVal = { skipCurrent: true, skipChildren: false };
290292
} else {
291293
const nestedControl = elem.querySelector("input, select, textarea, button, [role='checkbox'], [role='combobox'], [role='listbox'], [role='menuitemcheckbox'], [role='menuitemradio'], [role='radio'], [role='searchbox'], [role='slider'], [role='spinbutton'], [role='switch'], [role='textbox']");
292294
if (nestedControl && (nestedControl as HTMLElement).getAttribute("type") !== "hidden") {
@@ -295,6 +297,23 @@ export namespace SRNavigator {
295297
}
296298
}
297299

300+
// Skip non-interactive content that is a descendant of a for-linked label.
301+
// Such content is already surfaced as part of the associated control's announcement.
302+
// Interactive descendants (links, buttons, etc.) are allowed through.
303+
{
304+
const labelFor = elem.closest("label[for]") as HTMLElement | null;
305+
const labelForTarget = labelFor && labelFor.getAttribute("for");
306+
if (
307+
labelFor
308+
&& labelForTarget
309+
&& document.getElementById(labelForTarget)
310+
&& document.getElementById(labelForTarget).getAttribute("type") !== "hidden"
311+
&& !elem.closest("a[href], [role='link'], button, [role='button']")
312+
) {
313+
return retVal = { skipCurrent: true, skipChildren: nodeType === 1 };
314+
}
315+
}
316+
298317
const role = cursorStart.getRole();
299318

300319
// If we have presentational children, read the element, skip the children
@@ -349,14 +368,33 @@ export namespace SRNavigator {
349368
if (nodeType === 3) return VisUtil.isNodeHiddenFromAT(elem) ? { skipCurrent: true, skipChildren: false } : null;
350369
// We have an elemenet
351370
if (VisUtil.isNodeHiddenFromAT(elem)) return { skipCurrent: true, skipChildren: true };
352-
// Skip label fors - they'll be read with the related input
371+
// Skip label fors - they'll be read with the related input.
372+
// Keep children so interactive content (e.g. links) inside the label still appears in
373+
// reading/tab order.
353374
if (
354-
elem.nodeName.toUpperCase() === "LABEL"
355-
&& elem.hasAttribute("for")
375+
elem.nodeName.toUpperCase() === "LABEL"
376+
&& elem.hasAttribute("for")
356377
&& document.getElementById(elem.getAttribute("for"))
357378
&& document.getElementById(elem.getAttribute("for")).getAttribute("type") !== "hidden"
358379
) {
359-
return { skipCurrent: true, skipChildren: true };
380+
return { skipCurrent: true, skipChildren: false };
381+
}
382+
383+
// Skip non-interactive content that is a descendant of a for-linked label.
384+
// Such content is already surfaced as part of the associated control's announcement.
385+
// Interactive descendants (links, buttons, etc.) are allowed through.
386+
{
387+
const labelFor = elem.closest("label[for]") as HTMLElement | null;
388+
const labelForTarget = labelFor && labelFor.getAttribute("for");
389+
if (
390+
labelFor
391+
&& labelForTarget
392+
&& document.getElementById(labelForTarget)
393+
&& document.getElementById(labelForTarget).getAttribute("type") !== "hidden"
394+
&& !elem.closest("a[href], [role='link'], button, [role='button']")
395+
) {
396+
return { skipCurrent: true, skipChildren: nodeType === 1 };
397+
}
360398
}
361399

362400
const role = cursor.getRole();

accessibility-checker-engine/test/v4/simulator/Link_test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,33 @@ describe('Link Component Screen Reader Tests', function() {
330330
]);
331331
});
332332
});
333+
334+
describe("Link Within Label", function() {
335+
336+
it("Should render checkbox with aria-label and a linked label containing a link and aria-hidden spans", function() {
337+
let fixture = `<div id='fixture'>
338+
<h2>Regular order</h2>
339+
<div>
340+
<input type="checkbox" id="terms" name="terms" required aria-label="I have read and accept the terms and conditions."/>
341+
<label id="chkLabel" for="terms">
342+
<span aria-hidden="true">I have read and accept the </span>
343+
<a href="#" target="_blank">terms and conditions</a>
344+
<span aria-hidden="true">.</span>
345+
</label>
346+
</div>
347+
</div>`;
348+
document.body.insertAdjacentHTML('afterbegin', fixture);
349+
350+
let result = trimItems(ace.SRController.renderStructure(document));
351+
352+
expect(result).withContext(JSON.stringify(result, null, 2)).toEqual([
353+
{ "region": "", "heading": "", "item": "[Start of document]", "tab_focus": "", "image": "", "selector": "body" },
354+
{ "region": "", "heading": `["Regular order", heading level 2]`, "item": `[heading level 2] Regular order`, "tab_focus": "", "image": "", "selector": "#fixture > h2" },
355+
{ "region": "", "heading": "", "item": `[checkbox, not checked, required, "I have read and accept the terms and conditions."]`, "tab_focus": "", "image": "", "selector": "#fixture > div" },
356+
{ "region": "", "heading": "", "item": "", "tab_focus": `[checkbox, not checked, required, "I have read and accept the terms and conditions."]`, "image": "", "selector": "#terms" },
357+
{ "region": "", "heading": "", "item": "[same page link] terms and conditions", "tab_focus": "terms and conditions [same page link]", "image": "", "selector": `#chkLabel > a` },
358+
{ "region": "", "heading": "", "item": "[End of document]", "tab_focus": "", "image": "" }
359+
]);
360+
});
361+
});
333362
});

0 commit comments

Comments
 (0)