...
 
Commits (11)
# html-validate changelog
# [2.14.0](https://gitlab.com/html-validate/html-validate/compare/v2.13.0...v2.14.0) (2020-02-06)
### Features
- **elements:** make `<legend>` in `<fieldset>` optional (covered by new h71 rule instead) ([f3a59b9](https://gitlab.com/html-validate/html-validate/commit/f3a59b917addb05e920b30e7ce32c1be375157e2))
- **rules:** new method `getTagsDerivedFrom` to get tag and tags inheriting from it ([0118738](https://gitlab.com/html-validate/html-validate/commit/011873818a5e8997887547895a5be519baa589b0))
- **rules:** new rule `wcag/h71` requiring `<fieldset>` to have `<legend>` ([1b8ceab](https://gitlab.com/html-validate/html-validate/commit/1b8ceab724e9bb886b6b9d08a1c7563163786ad9))
# [2.13.0](https://gitlab.com/html-validate/html-validate/compare/v2.12.0...v2.13.0) (2020-02-02)
### Features
......
......@@ -175,3 +175,11 @@ Report a new error.
location)
- _`context`_ - If set it will be passed to `documentation()` later to allow
retrieving contextual documentation.
### `getTagsWithProperty(propName: MetaLookupableProperty): string[]`
Find all tags which has enabled given property.
### `getTagsDerivedFrom(tagName: string): string[]`
Find tag matching tagName or inheriting from it.
......@@ -30,9 +30,11 @@ overrides for Vue.js.
Configure with:
```js
```json
{
"elements": ["html5", "html-validate-vue/elements"],
"plugins": ["html-validate-vue"],
"extends": ["html-validate:recommended", "html-validate-vue:recommended"],
"elements": ["html5"],
"transform": {
"^.*\\.vue$": "html-validate-vue"
}
......
......@@ -105,29 +105,32 @@ The most common case is to prevent nesting of the component or limit usage of ce
Other properties to limit content also exits, check the [element metadata reference](/usage/elements.html) for details.
### Case study: `<fieldset>`
### Case study: `<html>`
(simplified for brevity)
```json
{
"fieldset": {
"flow": true,
"permittedContent": ["@flow", "legend?"],
"permittedOrder": ["legend", "@flow"],
"requiredContent": ["legend"]
"html": {
"permittedContent": ["head?", "body?"],
"permittedOrder": ["head", "body"],
"requiredContent": ["head", "body"]
}
}
```
Like we seen before the `permittedContent` property is used to restrict to only accept flow content and the `<legend>` element.
Note the usage of a trailing `?` after legend, this limits the allowed occurrences to 0 or 1 (2 or more is disallowed).
Like we seen before the `permittedContent` property is used to restrict to only accept the `<head>` and `<body>` elements.
Note the usage of a trailing `?`, this limits the allowed occurrences to 0 or 1 (2 or more is disallowed).
Default is to allow any number of occurrences.
Next it uses `permittedOrder` to declare that `<legend>` must come before any flow content.
`permittedOrder` must not list all the possible elements from `permittedContent` but for the items listed the order must be adhered to.
Unlisted elements can be used in any order.
Next it uses `permittedOrder` to declare that `<head>` must come before `<body>`.
`permittedOrder` doesnt have to list all the possible elements from `permittedContent` but for the items listed the order must be adhered to.
Contents groups such as `@flow` is allowed and unlisted elements can be used in any anywhere (even inbetween listed elements).
Lastly it uses `requiredContent` to declare that a `<legend>` element must be present.
Lastly it uses `requiredContent` to declare that both `<head>` and `<body>` must be present.
To sum up, the `<fieldset>` elements puts the following restrictions in place:
To sum up, the `<html>` elements puts the following restrictions in place:
- It must contain a single `<legend>` element.
- The `<legend>` element must come before any other content.
- It must contain a single `<head>` element.
- It must contain a single `<body>` element.
- The `<head>` element must come before the `<body>` element.
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/wcag/h71.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/wcag/h71.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 2,
"context": undefined,
"line": 1,
"message": "<fieldset> must have a <legend> as the first child",
"offset": 1,
"ruleId": "WCAG/H71",
"selector": "fieldset",
"severity": 2,
"size": 8,
},
],
"source": "<fieldset>
...
</fieldset>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<fieldset>
...
</fieldset>`;
markup["correct"] = `<fieldset>
<legend>Lorem ipsum</legend>
...
</fieldset>`;
describe("docs/rules/wcag/h71.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"wcag/h71":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"wcag/h71":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: wcag/h71
category: a17y
summary: "WCAG 2.1 H71: Providing a description for groups of form controls"
---
# WCAG 2.1 H71: Providing a description for groups of form controls (`wcag/h71`)
[WCAG 2.1 technique H71][1] requires all fieldsets to have a `<legend>` element as first child element.
[1]: https://www.w3.org/WAI/WCAG21/Techniques/html/H71
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="wcag/h71">
<fieldset>
...
</fieldset>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="wcag/h71">
<fieldset>
<legend>Lorem ipsum</legend>
...
</fieldset>
</validate>
......@@ -202,7 +202,7 @@ textual description of the content. E.g. it cannot suggest to use `<abbr>` or
</tr>
<tr>
<td class="table-right">H71</td>
<td>Providing a description for groups of form controls using fieldset and legend elements.<em>{@link rule:element-required-content} validates presence of <code>&lt;legend&gt;</code> inside <code>&lt;fieldset&gt;</code>, but not whenever <code>&lt;fieldset&gt;</code> itself is used.</em></td>
<td>Providing a description for groups of form controls using fieldset and legend elements.<em>Use {@link rule:wcag/h71} to validate presence of <code>&lt;legend&gt;</code> inside <code>&lt;fieldset&gt;</code> but it will not validate if <code>&lt;fieldset&gt;</code> itself is used.</em></td>
<td class="support-yes">Yes</td>
</tr>
<tr>
......
......@@ -1713,14 +1713,11 @@ Array [
"messages": Array [
Object {
"column": 2,
"context": Object {
"missing": "legend",
"node": "fieldset",
},
"context": undefined,
"line": 2,
"message": "<fieldset> element must have <legend> as content",
"message": "<fieldset> must have a <legend> as the first child",
"offset": 29,
"ruleId": "element-required-content",
"ruleId": "WCAG/H71",
"selector": "fieldset:nth-child(1)",
"severity": 2,
"size": 8,
......
......@@ -300,8 +300,7 @@
},
"deprecatedAttributes": ["datafld"],
"permittedContent": ["@flow", "legend?"],
"permittedOrder": ["legend", "@flow"],
"requiredContent": ["legend"]
"permittedOrder": ["legend", "@flow"]
},
"figcaption": {
......
This diff is collapsed.
......@@ -41,5 +41,6 @@ module.exports = {
"wcag/h36": "error",
"wcag/h37": "error",
"wcag/h67": "error",
"wcag/h71": "error",
},
};
......@@ -392,6 +392,27 @@ describe("MetaTable", () => {
expect(table.getTagsWithProperty("phrasing")).toEqual([]);
});
});
describe("getTagsDerivedFrom()", () => {
it("should return list of all tags derived from given tagname", () => {
expect.assertions(2);
const table = new MetaTable();
table.loadFromObject({
foo: mockEntry({}),
bar: mockEntry({
inherit: "foo",
}),
});
expect(table.getTagsDerivedFrom("foo")).toEqual(["foo", "bar"]);
expect(table.getTagsDerivedFrom("bar")).toEqual(["bar"]);
});
it("should return empty list if nothing matches", () => {
expect.assertions(1);
const table = new MetaTable();
expect(table.getTagsDerivedFrom("missing")).toEqual([]);
});
});
});
function mockEntry(stub = {}): MetaData {
......
......@@ -134,6 +134,15 @@ export class MetaTable {
.map(([tagName]) => tagName);
}
/**
* Find tag matching tagName or inheriting from it.
*/
public getTagsDerivedFrom(tagName: string): string[] {
return Object.entries(this.elements)
.filter(([key, entry]) => key === tagName || entry.inherit === tagName)
.map(([tagName]) => tagName);
}
private addEntry(tagName: string, entry: MetaData): void {
const defaultEntry = {
void: false,
......@@ -149,7 +158,6 @@ export class MetaTable {
`Element <${tagName}> cannot inherit from <${name}>: no such element`
);
}
delete entry.inherit;
}
/* merge all sources together */
......
......@@ -206,6 +206,12 @@ describe("rule base class", () => {
expect(rule.getTagsWithProperty("form")).toEqual(["form"]);
expect(spy).toHaveBeenCalledWith("form");
});
it("getTagsDerivedFrom() should lookup properties from metadata", () => {
const spy = jest.spyOn(meta, "getTagsDerivedFrom");
expect(rule.getTagsDerivedFrom("form")).toEqual(["form"]);
expect(spy).toHaveBeenCalledWith("form");
});
});
it("ruleDocumentationUrl() should return URL to rule documentation", () => {
......
......@@ -82,10 +82,20 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
return this.enabled && this.severity >= Severity.WARN;
}
/**
* Find all tags which has enabled given property.
*/
public getTagsWithProperty(propName: MetaLookupableProperty): string[] {
return this.meta.getTagsWithProperty(propName);
}
/**
* Find tag matching tagName or inheriting from it.
*/
public getTagsDerivedFrom(tagName: string): string[] {
return this.meta.getTagsDerivedFrom(tagName);
}
/**
* Report a new error.
*
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`wcag/h71 should contain documentation 1`] = `
Object {
"description": "H71: Providing a description for groups of form controls using fieldset and legend elements",
"url": "https://html-validate.org/rules/h71.html",
}
`;
import HtmlValidate from "../../htmlvalidate";
import "../../matchers";
describe("wcag/h71", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
elements: [
"html5",
{
custom: {
inherit: "fieldset",
},
},
],
rules: { "wcag/h71": "error" },
});
});
it("should report error when <fieldset> is missing <legend>", () => {
const report = htmlvalidate.validateString("<fieldset></fieldset>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"WCAG/H71",
"<fieldset> must have a <legend> as the first child"
);
});
it("should report error when custom element inherits from <fieldset>", () => {
const report = htmlvalidate.validateString("<custom></custom>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"WCAG/H71",
"<custom> must have a <legend> as the first child"
);
});
it("should not report when <fieldset> have <legend>", () => {
const report = htmlvalidate.validateString(
"<fieldset><legend>foo</legend></fieldset>"
);
expect(report).toBeValid();
});
it("should not report when <fieldset> have multiple <legend>", () => {
const report = htmlvalidate.validateString(
"<fieldset><legend>foo</legend><legend>bar</legend></fieldset>"
);
expect(report).toBeValid();
});
it("should not report when <fieldset> have out-of-order <legend>", () => {
const report = htmlvalidate.validateString(
"<fieldset><div>foo</div><legend>bar</legend></fieldset>"
);
expect(report).toBeValid();
});
it("should contain documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "wcag/h71": "error" },
});
expect(htmlvalidate.getRuleDocumentation("wcag/h71")).toMatchSnapshot();
});
});
import { DOMReadyEvent } from "../../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../../rule";
import { HtmlElement } from "../../dom";
class H71 extends Rule {
public documentation(): RuleDocumentation {
return {
description:
"H71: Providing a description for groups of form controls using fieldset and legend elements",
url: ruleDocumentationUrl(__filename),
};
}
public constructor(options: void) {
super(options);
this.name = "WCAG/H71";
}
public setup(): void {
this.on("dom:ready", (event: DOMReadyEvent) => {
const { document } = event;
const fieldsets = document.querySelectorAll(this.selector);
for (const fieldset of fieldsets) {
this.validate(fieldset);
}
});
}
private validate(fieldset: HtmlElement): void {
const legend = fieldset.querySelectorAll("> legend");
if (legend.length === 0) {
this.reportNode(fieldset);
}
}
private reportNode(node: HtmlElement): void {
super.report(
node,
`${node.annotatedName} must have a <legend> as the first child`
);
}
private get selector(): string {
return this.getTagsDerivedFrom("fieldset").join(",");
}
}
module.exports = H71;