Commit 06c44cec authored by David Sveningsson's avatar David Sveningsson

feat(rules): new rule `prefer-native-element`

parent f5ffb643
Pipeline #111546678 passed with stages
in 11 minutes and 2 seconds
......@@ -27,6 +27,13 @@ footer {
padding: 3rem 0;
}
table {
th,
td {
padding: $table-cell-padding;
}
}
.rules-table {
.set {
width: 5%;
......
......@@ -35,6 +35,7 @@
</div>
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
<!-- [html-validate-disable-block prefer-native-element: bootstrap styles button way different and this is their recommended markup] -->
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">User guide <span class="caret"></span></a>
<ul class="dropdown-menu">
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/prefer-native-element.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/prefer-native-element.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 6,
"context": Object {
"replacement": "main",
"role": "main",
},
"line": 1,
"message": "Prefer to use the native <main> element",
"offset": 5,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 11,
},
],
"source": "<div role=\\"main\\">
<p>Lorem ipsum</p>
</div>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<div role="main">
<p>Lorem ipsum</p>
</div>`;
markup["correct"] = `<main>
<p>Lorem ipsum</p>
</main>`;
describe("docs/rules/prefer-native-element.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"prefer-native-element":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"prefer-native-element":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: prefer-native-element
category: a17y
summary: Prefer to use native HTML element over roles
---
# Prefer to use native HTML element over roles (`prefer-native-element`)
While WAI-ARIA describes many [roles][wai-aria-roles] which can provide semantic information about what the element represents.
Support for roles is varying and since HTML5 has many native equivalent elements it is better to use the native when possible.
[wai-aria-roles]: https://www.w3.org/TR/wai-aria-1.1/#role_definitions
Table of equivalent elements:
<!-- [html-validate-disable-block element-required-attributes: marked does not generate tables with scope attribute] -->
| Role | Element |
| ------------- | ---------------------- |
| article | article |
| banner | header |
| button | button |
| cell | td |
| checkbox | input |
| complementary | aside |
| contentinfo | footer |
| figure | figure |
| form | form |
| heading | h1, h2, h3, h4, h5, h6 |
| input | input |
| link | a |
| list | ul, ol |
| listbox | select |
| listitem | li |
| main | main |
| navigation | nav |
| progressbar | progress |
| radio | input |
| region | section |
| table | table |
| textbox | textarea |
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="prefer-native-element">
<div role="main">
<p>Lorem ipsum</p>
</div>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="prefer-native-element">
<main>
<p>Lorem ipsum</p>
</main>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"mapping": { .. },
"include": [],
"exclude": [],
}
```
### `mapping`
Object with roles to map to native elements.
This can be used to provide a custom map with roles and their replacement.
### `include`
If set only roles listed in this array generates errors.
### `exclude`
If set roles listed in this array is ignored.
......@@ -30,6 +30,7 @@ module.exports = {
"no-raw-characters": "error",
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-native-element": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule prefer-native-element default config smoketest 1`] = `
Array [
Object {
"errorCount": 22,
"filePath": "test-files/rules/prefer-native-element.html",
"messages": Array [
Object {
"column": 6,
"context": Object {
"replacement": "article",
"role": "article",
},
"line": 3,
"message": "Prefer to use the native <article> element",
"offset": 33,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 14,
},
Object {
"column": 6,
"context": Object {
"replacement": "header",
"role": "banner",
},
"line": 4,
"message": "Prefer to use the native <header> element",
"offset": 60,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 13,
},
Object {
"column": 6,
"context": Object {
"replacement": "button",
"role": "button",
},
"line": 5,
"message": "Prefer to use the native <button> element",
"offset": 86,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 13,
},
Object {
"column": 6,
"context": Object {
"replacement": "td",
"role": "cell",
},
"line": 6,
"message": "Prefer to use the native <td> element",
"offset": 112,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 11,
},
Object {
"column": 6,
"context": Object {
"replacement": "input",
"role": "checkbox",
},
"line": 7,
"message": "Prefer to use the native <input> element",
"offset": 136,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 15,
},
Object {
"column": 6,
"context": Object {
"replacement": "aside",
"role": "complementary",
},
"line": 8,
"message": "Prefer to use the native <aside> element",
"offset": 164,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 20,
},
Object {
"column": 6,
"context": Object {
"replacement": "footer",
"role": "contentinfo",
},
"line": 9,
"message": "Prefer to use the native <footer> element",
"offset": 197,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 18,
},
Object {
"column": 6,
"context": Object {
"replacement": "figure",
"role": "figure",
},
"line": 10,
"message": "Prefer to use the native <figure> element",
"offset": 228,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 13,
},
Object {
"column": 6,
"context": Object {
"replacement": "form",
"role": "form",
},
"line": 11,
"message": "Prefer to use the native <form> element",
"offset": 254,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 11,
},
Object {
"column": 6,
"context": Object {
"replacement": "hN",
"role": "heading",
},
"line": 12,
"message": "Prefer to use the native <hN> element",
"offset": 278,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 14,
},
Object {
"column": 6,
"context": Object {
"replacement": "input",
"role": "input",
},
"line": 13,
"message": "Prefer to use the native <input> element",
"offset": 305,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 12,
},
Object {
"column": 6,
"context": Object {
"replacement": "a",
"role": "link",
},
"line": 14,
"message": "Prefer to use the native <a> element",
"offset": 330,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 11,
},
Object {
"column": 6,
"context": Object {
"replacement": "ul",
"role": "list",
},
"line": 15,
"message": "Prefer to use the native <ul> element",
"offset": 354,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 11,
},
Object {
"column": 6,
"context": Object {
"replacement": "select",
"role": "listbox",
},
"line": 16,
"message": "Prefer to use the native <select> element",
"offset": 378,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 14,
},
Object {
"column": 6,
"context": Object {
"replacement": "li",
"role": "listitem",
},
"line": 17,
"message": "Prefer to use the native <li> element",
"offset": 405,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 15,
},
Object {
"column": 6,
"context": Object {
"replacement": "main",
"role": "main",
},
"line": 18,
"message": "Prefer to use the native <main> element",
"offset": 433,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 11,
},
Object {
"column": 6,
"context": Object {
"replacement": "nav",
"role": "navigation",
},
"line": 19,
"message": "Prefer to use the native <nav> element",
"offset": 457,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 17,
},
Object {
"column": 6,
"context": Object {
"replacement": "progress",
"role": "progressbar",
},
"line": 20,
"message": "Prefer to use the native <progress> element",
"offset": 487,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 18,
},
Object {
"column": 6,
"context": Object {
"replacement": "input",
"role": "radio",
},
"line": 21,
"message": "Prefer to use the native <input> element",
"offset": 518,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 12,
},
Object {
"column": 6,
"context": Object {
"replacement": "section",
"role": "region",
},
"line": 22,
"message": "Prefer to use the native <section> element",
"offset": 543,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 13,
},
Object {
"column": 6,
"context": Object {
"replacement": "table",
"role": "table",
},
"line": 23,
"message": "Prefer to use the native <table> element",
"offset": 569,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 12,
},
Object {
"column": 6,
"context": Object {
"replacement": "textarea",
"role": "textbox",
},
"line": 24,
"message": "Prefer to use the native <textarea> element",
"offset": 594,
"ruleId": "prefer-native-element",
"severity": 2,
"size": 14,
},
],
"source": "<div role=\\"unknown\\"></div>
<div role=\\"article\\"></div>
<div role=\\"banner\\"></div>
<div role=\\"button\\"></div>
<div role=\\"cell\\"></div>
<div role=\\"checkbox\\"></div>
<div role=\\"complementary\\"></div>
<div role=\\"contentinfo\\"></div>
<div role=\\"figure\\"></div>
<div role=\\"form\\"></div>
<div role=\\"heading\\"></div>
<div role=\\"input\\"></div>
<div role=\\"link\\"></div>
<div role=\\"list\\"></div>
<div role=\\"listbox\\"></div>
<div role=\\"listitem\\"></div>
<div role=\\"main\\"></div>
<div role=\\"navigation\\"></div>
<div role=\\"progressbar\\"></div>
<div role=\\"radio\\"></div>
<div role=\\"region\\"></div>
<div role=\\"table\\"></div>
<div role=\\"textbox\\"></div>
",
"warningCount": 0,
},
]
`;
exports[`rule prefer-native-element should contain contextual documentation 1`] = `
Object {
"description": "Instead of using the WAI-ARIA role \\"the-role\\" prefer to use the native <the-replacement> element.",
"url": "https://html-validate.org/rules/prefer-native-element.html",
}
`;
exports[`rule prefer-native-element should contain documentation 1`] = `
Object {
"description": "Instead of using WAI-ARIA roles prefer to use the native HTML elements.",
"url": "https://html-validate.org/rules/prefer-native-element.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule prefer-native-element", () => {
describe("default config", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "prefer-native-element": "error" },
});
});
it("should not report error when using role without native equivalent element", () => {
const report = htmlvalidate.validateString('<div role="unknown"></div>');
expect(report).toBeValid();
});
it("should not report error when role is boolean", () => {
const report = htmlvalidate.validateString("<div role></div>");
expect(report).toBeValid();
});
it("should not report error for dynamic attributes", () => {
const report = htmlvalidate.validateString(
'<input dynamic-role="main">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it("should report error when using role with native equivalent element", () => {
const report = htmlvalidate.validateString('<div role="main"></div>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-native-element",
"Prefer to use the native <main> element"
);
});
it("should handle unquoted role", () => {
const report = htmlvalidate.validateString("<div role=main></div>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-native-element",
"Prefer to use the native <main> element"
);
});
it("smoketest", () => {
const report = htmlvalidate.validateFile(
"test-files/rules/prefer-native-element.html"
);
expect(report.results).toMatchSnapshot();
});
});
it("should not report error when role is excluded", () => {
const htmlvalidate = new HtmlValidate({
rules: { "prefer-native-element": ["error", { exclude: ["main"] }] },
});
const valid = htmlvalidate.validateString('<div role="main"></div>');
const invalid = htmlvalidate.validateString('<div role="article"></div>');
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should report error only for included roles", () => {
const htmlvalidate = new HtmlValidate({
rules: { "prefer-native-element": ["error", { include: ["main"] }] },
});
const valid = htmlvalidate.validateString('<div role="article"></div>');
const invalid = htmlvalidate.validateString('<div role="main"></div>');
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should contain documentation", () => {
const htmlvalidate = new HtmlValidate({
rules: { "prefer-native-element": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("prefer-native-element")
).toMatchSnapshot();
});