Commit a6af2da3 authored by David Sveningsson's avatar David Sveningsson
Browse files

fix(rules): `input-missing-label` handles multiple labels

parent ff5e8559
Pipeline #210048989 passed with stages
in 10 minutes and 9 seconds
......@@ -62,6 +62,7 @@ Examples of **correct** code for this rule:
### Hidden labels
This rule requires labels to be accessible, i.e. the label must not be `hidden` or `aria-hidden`.
If multiple labels are associated at least one of them must be accessible.
<validate name="hidden" rules="input-missing-label">
<label for="my-input" aria-hidden="true">My field</label>
......
......@@ -40,6 +40,23 @@ describe("rule input-missing-label", () => {
expect(report).toBeValid();
});
it("should handle multiple labels", () => {
expect.assertions(1);
const markup = '<label for="foo">foo</label><label for="foo">bar</label><input id="foo"/>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report error when at least one label is accessible", () => {
expect.assertions(1);
const markup = `
<label for="foo" aria-hidden="true">foo</label>
<label for="foo">bar</label>
<input id="foo"/>`;
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it.each(["input", "textarea", "select"])(
"should report when <%s> is missing label",
(tagName: string) => {
......
......@@ -33,16 +33,16 @@ export default class InputMissingLabel extends Rule {
}
}
let label: HtmlElement;
let label: HtmlElement[] = [];
/* try to find label by id */
if ((label = findLabelById(root, elem.id))) {
if ((label = findLabelById(root, elem.id)).length > 0) {
this.validateLabel(elem, label);
return;
}
/* try to find parent label (input nested in label) */
if ((label = findLabelByParent(elem))) {
if ((label = findLabelByParent(elem)).length > 0) {
this.validateLabel(elem, label);
return;
}
......@@ -50,25 +50,34 @@ export default class InputMissingLabel extends Rule {
this.report(elem, `<${elem.tagName}> element does not have a <label>`);
}
private validateLabel(elem: HtmlElement, label: HtmlElement): void {
if (isHTMLHidden(label) || isAriaHidden(label)) {
/**
* Reports error if none of the labels are accessible.
*/
private validateLabel(elem: HtmlElement, labels: HtmlElement[]): void {
const visible = labels.filter(isVisible);
if (visible.length === 0) {
this.report(elem, `<${elem.tagName}> element has label but <label> element is hidden`);
}
}
}
function findLabelById(root: DOMTree, id: string): HtmlElement {
if (!id) return null;
return root.querySelector(`label[for="${id}"]`);
function isVisible(elem: HtmlElement): boolean {
const hidden = isHTMLHidden(elem) || isAriaHidden(elem);
return !hidden;
}
function findLabelByParent(el: HtmlElement): HtmlElement {
function findLabelById(root: DOMTree, id: string): HtmlElement[] {
if (!id) return [];
return root.querySelectorAll(`label[for="${id}"]`);
}
function findLabelByParent(el: HtmlElement): HtmlElement[] {
let cur = el.parent;
while (cur) {
if (cur.is("label")) {
return cur;
return [cur];
}
cur = cur.parent;
}
return null;
return [];
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment