Skip to content

Commit f0dfb3d

Browse files
authored
Merge branch 'main' into dev/coliff/release-notes
2 parents 47b4dea + 64c2a58 commit f0dfb3d

7 files changed

Lines changed: 135 additions & 28 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<h1 align="center">
22
<br>
3-
<a href="https://htmlhint.com"><img src="https://raw.githubusercontent.com/htmlhint/HTMLHint/main/website/src/assets/img/htmlhint.png" alt="Logo HTMLHint" width="170"></a>
3+
<a href="https://htmlhint.com"><img src="https://raw.githubusercontent.com/htmlhint/HTMLHint/main/website/src/assets/img/hero.gif" alt="HTMLHint — static analysis for HTML"></a>
44
<br>
55
HTMLHint
66
<br>

‎dist/core/rules/input-requires-label.js‎

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/core/rules/input-requires-label.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export default {
1717
nested: boolean
1818
}> = []
1919
let labelDepth = 0
20+
const labelStateStack: Array<{
21+
hasText: boolean
22+
inputStartIndex: number
23+
}> = []
2024

2125
parser.addListener('tagstart', (event) => {
2226
const tagName = event.tagName.toLowerCase()
@@ -43,16 +47,39 @@ export default {
4347
// implicit label (no `for`): nesting labels the input
4448
// a self-closing <label/> opens no scope and emits no tagend
4549
labelDepth++
50+
labelStateStack.push({
51+
hasText: false,
52+
inputStartIndex: inputTags.length,
53+
})
4654
}
4755
}
4856
})
4957

5058
parser.addListener('tagend', (event) => {
5159
if (event.tagName.toLowerCase() === 'label' && labelDepth > 0) {
60+
const state = labelStateStack.pop()
61+
if (state && !state.hasText) {
62+
for (let i = state.inputStartIndex; i < inputTags.length; i++) {
63+
inputTags[i].nested = false
64+
}
65+
} else if (state && state.hasText && labelStateStack.length > 0) {
66+
labelStateStack[labelStateStack.length - 1].hasText = true
67+
}
5268
labelDepth--
5369
}
5470
})
5571

72+
parser.addListener('text', (event) => {
73+
if (
74+
labelDepth > 0 &&
75+
event.raw &&
76+
!/^\s*$/.test(event.raw) &&
77+
labelStateStack.length > 0
78+
) {
79+
labelStateStack[labelStateStack.length - 1].hasText = true
80+
}
81+
})
82+
5683
parser.addListener('end', () => {
5784
inputTags.forEach((inputTag) => {
5885
if (!inputTag.nested && !hasMatchingLabelTag(inputTag)) {

‎test/rules/input-requires-label.spec.js‎

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,27 @@ describe(`Rules: ${ruleId}`, () => {
3939
expect(messages.length).toBe(0)
4040
})
4141

42-
it('Input tag nested inside label tag should result in no error', () => {
43-
const code = '<label><input type="password" /></label>'
42+
it('Input nested inside label with text before should result in no error', () => {
43+
const code = '<label>Password <input type="password" /></label>'
44+
const messages = HTMLHint.verify(code, ruleOptions)
45+
expect(messages.length).toBe(0)
46+
})
47+
48+
it('Input nested inside label with text after should result in no error', () => {
49+
const code = '<label><input type="email" /> Email</label>'
50+
const messages = HTMLHint.verify(code, ruleOptions)
51+
expect(messages.length).toBe(0)
52+
})
53+
54+
it('Input nested inside label with text in child element should result in no error', () => {
55+
const code = '<label><span>Email</span> <input type="email" /></label>'
4456
const messages = HTMLHint.verify(code, ruleOptions)
4557
expect(messages.length).toBe(0)
4658
})
4759

48-
// Multiple inputs inside one label are all accepted. The HTML spec
49-
// associates only the first labelable descendant with the label, but
50-
// this rule is about "every input has a label nearby" for a11y, not
51-
// strict spec conformance.
52-
it('Multiple inputs nested inside one label should result in no error', () => {
60+
it('Multiple inputs nested inside label with text should result in no error', () => {
5361
const code =
54-
'<label><input type="password" /><input type="text" /></label>'
62+
'<label>Credentials <input type="text" /><input type="password" /></label>'
5563
const messages = HTMLHint.verify(code, ruleOptions)
5664
expect(messages.length).toBe(0)
5765
})
@@ -136,5 +144,42 @@ describe(`Rules: ${ruleId}`, () => {
136144
expect(messages[0].rule.id).toBe(ruleId)
137145
expect(messages[0].type).toBe('warning')
138146
})
147+
148+
it('Input nested inside label with no text should result in error', () => {
149+
const code = '<label><input type="password" /></label>'
150+
const messages = HTMLHint.verify(code, ruleOptions)
151+
expect(messages.length).toBe(1)
152+
expect(messages[0].rule.id).toBe(ruleId)
153+
expect(messages[0].type).toBe('warning')
154+
})
155+
156+
it('Input nested inside label with whitespace only should result in error', () => {
157+
const code = '<label> <input type="text" /> </label>'
158+
const messages = HTMLHint.verify(code, ruleOptions)
159+
expect(messages.length).toBe(1)
160+
expect(messages[0].rule.id).toBe(ruleId)
161+
expect(messages[0].type).toBe('warning')
162+
})
163+
164+
it('Multiple inputs nested inside label with no text should result in errors', () => {
165+
const code =
166+
'<label><input type="text" /><input type="password" /></label>'
167+
const messages = HTMLHint.verify(code, ruleOptions)
168+
expect(messages.length).toBe(2)
169+
})
170+
171+
it('Valid label followed by empty label should only warn for the empty one', () => {
172+
const code =
173+
'<label>Username <input type="text" /></label><label><input type="password" /></label>'
174+
const messages = HTMLHint.verify(code, ruleOptions)
175+
expect(messages.length).toBe(1)
176+
})
177+
178+
it('Nested labels: outer with text should not false-warn outer input', () => {
179+
const code =
180+
'<label>Outer <label><input type="checkbox" /></label><input type="text" /></label>'
181+
const messages = HTMLHint.verify(code, ruleOptions)
182+
expect(messages.length).toBe(1)
183+
})
139184
})
140185
})

‎website/package-lock.json‎

Lines changed: 30 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎website/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@astrojs/starlight": "^0.38.2",
29-
"astro": "^6.1.3",
29+
"astro": "^6.1.8",
3030
"sharp": "^0.34.5",
3131
"starlight-llms-txt": "^0.8.0"
3232
},

‎website/src/assets/img/hero.gif‎

108 KB
Loading

0 commit comments

Comments
 (0)