diff --git a/dist/core/rules/input-requires-label.js b/dist/core/rules/input-requires-label.js
index bd41c89fc..45a1054c0 100644
--- a/dist/core/rules/input-requires-label.js
+++ b/dist/core/rules/input-requires-label.js
@@ -7,6 +7,7 @@ exports.default = {
const labelTags = [];
const inputTags = [];
let labelDepth = 0;
+ const labelStateStack = [];
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
const mapAttrs = parser.getMapAttrs(event.attrs);
@@ -27,14 +28,35 @@ exports.default = {
}
else if (!event.close) {
labelDepth++;
+ labelStateStack.push({
+ hasText: false,
+ inputStartIndex: inputTags.length,
+ });
}
}
});
parser.addListener('tagend', (event) => {
if (event.tagName.toLowerCase() === 'label' && labelDepth > 0) {
+ const state = labelStateStack.pop();
+ if (state && !state.hasText) {
+ for (let i = state.inputStartIndex; i < inputTags.length; i++) {
+ inputTags[i].nested = false;
+ }
+ }
+ else if (state && state.hasText && labelStateStack.length > 0) {
+ labelStateStack[labelStateStack.length - 1].hasText = true;
+ }
labelDepth--;
}
});
+ parser.addListener('text', (event) => {
+ if (labelDepth > 0 &&
+ event.raw &&
+ !/^\s*$/.test(event.raw) &&
+ labelStateStack.length > 0) {
+ labelStateStack[labelStateStack.length - 1].hasText = true;
+ }
+ });
parser.addListener('end', () => {
inputTags.forEach((inputTag) => {
if (!inputTag.nested && !hasMatchingLabelTag(inputTag)) {
@@ -53,4 +75,4 @@ exports.default = {
}
},
};
-//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5wdXQtcmVxdWlyZXMtbGFiZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbnB1dC1yZXF1aXJlcy1sYWJlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUdBLGtCQUFlO0lBQ2IsRUFBRSxFQUFFLHNCQUFzQjtJQUMxQixXQUFXLEVBQUUsOERBQThEO0lBQzNFLElBQUksQ0FBQyxNQUFNLEVBQUUsUUFBUTtRQUNuQixNQUFNLFNBQVMsR0FJVixFQUFFLENBQUE7UUFDUCxNQUFNLFNBQVMsR0FLVixFQUFFLENBQUE7UUFDUCxJQUFJLFVBQVUsR0FBRyxDQUFDLENBQUE7UUFFbEIsTUFBTSxDQUFDLFdBQVcsQ0FBQyxVQUFVLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRTtZQUN2QyxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLFdBQVcsRUFBRSxDQUFBO1lBQzNDLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFBO1lBQ2hELE1BQU0sR0FBRyxHQUFHLEtBQUssQ0FBQyxHQUFHLEdBQUcsT0FBTyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUE7WUFFMUMsSUFBSSxPQUFPLEtBQUssT0FBTyxFQUFFLENBQUM7Z0JBRXhCLElBQUksUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLFFBQVEsRUFBRSxDQUFDO29CQUNsQyxTQUFTLENBQUMsSUFBSSxDQUFDO3dCQUNiLEtBQUssRUFBRSxLQUFLO3dCQUNaLEdBQUcsRUFBRSxHQUFHO3dCQUNSLEVBQUUsRUFBRSxRQUFRLENBQUMsSUFBSSxDQUFDO3dCQUNsQixNQUFNLEVBQUUsVUFBVSxHQUFHLENBQUM7cUJBQ3ZCLENBQUMsQ0FBQTtnQkFDSixDQUFDO1lBQ0gsQ0FBQztZQUVELElBQUksT0FBTyxLQUFLLE9BQU8sRUFBRSxDQUFDO2dCQUN4QixJQUFJLEtBQUssSUFBSSxRQUFRLElBQUksUUFBUSxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDO29CQUVoRCxTQUFTLENBQUMsSUFBSSxDQUFDLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLFFBQVEsRUFBRSxRQUFRLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFBO2dCQUN2RSxDQUFDO3FCQUFNLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLENBQUM7b0JBR3hCLFVBQVUsRUFBRSxDQUFBO2dCQUNkLENBQUM7WUFDSCxDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUE7UUFFRixNQUFNLENBQUMsV0FBVyxDQUFDLFFBQVEsRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFO1lBQ3JDLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsS0FBSyxPQUFPLElBQUksVUFBVSxHQUFHLENBQUMsRUFBRSxDQUFDO2dCQUM5RCxVQUFVLEVBQUUsQ0FBQTtZQUNkLENBQUM7UUFDSCxDQUFDLENBQUMsQ0FBQTtRQUVGLE1BQU0sQ0FBQyxXQUFXLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRTtZQUM3QixTQUFTLENBQUMsT0FBTyxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUU7Z0JBQzdCLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxJQUFJLENBQUMsbUJBQW1CLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQztvQkFDdkQsUUFBUSxDQUFDLElBQUksQ0FDWCxrQ0FBa0MsRUFDbEMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQ25CLFFBQVEsQ0FBQyxHQUFHLEVBQ1osSUFBSSxFQUNKLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUNuQixDQUFBO2dCQUNILENBQUM7WUFDSCxDQUFDLENBQUMsQ0FBQTtRQUNKLENBQUMsQ0FBQyxDQUFBO1FBRUYsU0FBUyxtQkFBbUIsQ0FBQyxRQUF5QjtZQUNwRCxJQUFJLEtBQUssR0FBRyxLQUFLLENBQUE7WUFDakIsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFO2dCQUM3QixJQUFJLFFBQVEsQ0FBQyxFQUFFLElBQUksUUFBUSxDQUFDLEVBQUUsS0FBSyxRQUFRLENBQUMsUUFBUSxFQUFFLENBQUM7b0JBQ3JELEtBQUssR0FBRyxJQUFJLENBQUE7Z0JBQ2QsQ0FBQztZQUNILENBQUMsQ0FBQyxDQUFBO1lBQ0YsT0FBTyxLQUFLLENBQUE7UUFDZCxDQUFDO0lBQ0gsQ0FBQztDQUNNLENBQUEifQ==
\ No newline at end of file
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5wdXQtcmVxdWlyZXMtbGFiZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbnB1dC1yZXF1aXJlcy1sYWJlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUdBLGtCQUFlO0lBQ2IsRUFBRSxFQUFFLHNCQUFzQjtJQUMxQixXQUFXLEVBQUUsOERBQThEO0lBQzNFLElBQUksQ0FBQyxNQUFNLEVBQUUsUUFBUTtRQUNuQixNQUFNLFNBQVMsR0FJVixFQUFFLENBQUE7UUFDUCxNQUFNLFNBQVMsR0FLVixFQUFFLENBQUE7UUFDUCxJQUFJLFVBQVUsR0FBRyxDQUFDLENBQUE7UUFDbEIsTUFBTSxlQUFlLEdBR2hCLEVBQUUsQ0FBQTtRQUVQLE1BQU0sQ0FBQyxXQUFXLENBQUMsVUFBVSxFQUFFLENBQUMsS0FBSyxFQUFFLEVBQUU7WUFDdkMsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQTtZQUMzQyxNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQTtZQUNoRCxNQUFNLEdBQUcsR0FBRyxLQUFLLENBQUMsR0FBRyxHQUFHLE9BQU8sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFBO1lBRTFDLElBQUksT0FBTyxLQUFLLE9BQU8sRUFBRSxDQUFDO2dCQUV4QixJQUFJLFFBQVEsQ0FBQyxNQUFNLENBQUMsS0FBSyxRQUFRLEVBQUUsQ0FBQztvQkFDbEMsU0FBUyxDQUFDLElBQUksQ0FBQzt3QkFDYixLQUFLLEVBQUUsS0FBSzt3QkFDWixHQUFHLEVBQUUsR0FBRzt3QkFDUixFQUFFLEVBQUUsUUFBUSxDQUFDLElBQUksQ0FBQzt3QkFDbEIsTUFBTSxFQUFFLFVBQVUsR0FBRyxDQUFDO3FCQUN2QixDQUFDLENBQUE7Z0JBQ0osQ0FBQztZQUNILENBQUM7WUFFRCxJQUFJLE9BQU8sS0FBSyxPQUFPLEVBQUUs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
\ No newline at end of file
diff --git a/src/core/rules/input-requires-label.ts b/src/core/rules/input-requires-label.ts
index 80b601f10..95b1b85f5 100644
--- a/src/core/rules/input-requires-label.ts
+++ b/src/core/rules/input-requires-label.ts
@@ -17,6 +17,10 @@ export default {
nested: boolean
}> = []
let labelDepth = 0
+ const labelStateStack: Array<{
+ hasText: boolean
+ inputStartIndex: number
+ }> = []
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase()
@@ -43,16 +47,39 @@ export default {
// implicit label (no `for`): nesting labels the input
// a self-closing opens no scope and emits no tagend
labelDepth++
+ labelStateStack.push({
+ hasText: false,
+ inputStartIndex: inputTags.length,
+ })
}
}
})
parser.addListener('tagend', (event) => {
if (event.tagName.toLowerCase() === 'label' && labelDepth > 0) {
+ const state = labelStateStack.pop()
+ if (state && !state.hasText) {
+ for (let i = state.inputStartIndex; i < inputTags.length; i++) {
+ inputTags[i].nested = false
+ }
+ } else if (state && state.hasText && labelStateStack.length > 0) {
+ labelStateStack[labelStateStack.length - 1].hasText = true
+ }
labelDepth--
}
})
+ parser.addListener('text', (event) => {
+ if (
+ labelDepth > 0 &&
+ event.raw &&
+ !/^\s*$/.test(event.raw) &&
+ labelStateStack.length > 0
+ ) {
+ labelStateStack[labelStateStack.length - 1].hasText = true
+ }
+ })
+
parser.addListener('end', () => {
inputTags.forEach((inputTag) => {
if (!inputTag.nested && !hasMatchingLabelTag(inputTag)) {
diff --git a/test/rules/input-requires-label.spec.js b/test/rules/input-requires-label.spec.js
index a9269e469..94615acc4 100644
--- a/test/rules/input-requires-label.spec.js
+++ b/test/rules/input-requires-label.spec.js
@@ -39,19 +39,27 @@ describe(`Rules: ${ruleId}`, () => {
expect(messages.length).toBe(0)
})
- it('Input tag nested inside label tag should result in no error', () => {
- const code = ''
+ it('Input nested inside label with text before should result in no error', () => {
+ const code = ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(0)
+ })
+
+ it('Input nested inside label with text after should result in no error', () => {
+ const code = ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(0)
+ })
+
+ it('Input nested inside label with text in child element should result in no error', () => {
+ const code = ''
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})
- // Multiple inputs inside one label are all accepted. The HTML spec
- // associates only the first labelable descendant with the label, but
- // this rule is about "every input has a label nearby" for a11y, not
- // strict spec conformance.
- it('Multiple inputs nested inside one label should result in no error', () => {
+ it('Multiple inputs nested inside label with text should result in no error', () => {
const code =
- ''
+ ''
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})
@@ -136,5 +144,42 @@ describe(`Rules: ${ruleId}`, () => {
expect(messages[0].rule.id).toBe(ruleId)
expect(messages[0].type).toBe('warning')
})
+
+ it('Input nested inside label with no text should result in error', () => {
+ const code = ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(1)
+ expect(messages[0].rule.id).toBe(ruleId)
+ expect(messages[0].type).toBe('warning')
+ })
+
+ it('Input nested inside label with whitespace only should result in error', () => {
+ const code = ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(1)
+ expect(messages[0].rule.id).toBe(ruleId)
+ expect(messages[0].type).toBe('warning')
+ })
+
+ it('Multiple inputs nested inside label with no text should result in errors', () => {
+ const code =
+ ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(2)
+ })
+
+ it('Valid label followed by empty label should only warn for the empty one', () => {
+ const code =
+ ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(1)
+ })
+
+ it('Nested labels: outer with text should not false-warn outer input', () => {
+ const code =
+ ''
+ const messages = HTMLHint.verify(code, ruleOptions)
+ expect(messages.length).toBe(1)
+ })
})
})