...
 
Commits (4)
# html-validate changelog
# [1.9.0](https://gitlab.com/html-validate/html-validate/compare/v1.8.0...v1.9.0) (2019-09-17)
### Features
- **rules:** new rule `svg-focusable` ([c354364](https://gitlab.com/html-validate/html-validate/commit/c354364))
# [1.8.0](https://gitlab.com/html-validate/html-validate/compare/v1.7.1...v1.8.0) (2019-09-16)
### Bug Fixes
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/svg-focusable.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/svg-focusable.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 3,
"context": undefined,
"line": 2,
"message": "<svg> is missing required \\"focusable\\" attribute",
"offset": 15,
"ruleId": "svg-focusable",
"severity": 2,
"size": 3,
},
],
"source": "<a href=\\"#\\">
<svg></svg>
</a>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<a href="#">
<svg></svg>
</a>`;
markup["correct"] = `<a href="#">
<svg focusable="false"></svg>
</a>`;
describe("docs/rules/svg-focusable.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"svg-focusable":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"svg-focusable":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
@ngdoc rule
@module rules
@name svg-focusable
@category a17y
@summary Require <svg> to have focusable attribute
@description
# Require `<svg>` elements to have focusable attribute (`svg-focusable`)
Inline SVG elements in IE are focusable by default which may cause issues with tab-ordering.
For instance, if a link or button has an svg icon inside the user would need to press tab twice to move focus to the next element as pressing tab would move focus to the `<svg>` element instead.
Edge and other browsers implements proper support for `tabindex` and are unaffected by this bug.
If support for IE is required the `focusable` attribute should explicitly be set to `true` or `false` to avoid unintended behaviour.
Otherwise this rule can safely be disabled.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="svg-focusable">
<a href="#">
<svg></svg>
</a>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="svg-focusable">
<a href="#">
<svg focusable="false"></svg>
</a>
</validate>
......@@ -4983,7 +4983,30 @@ Array [
exports[`HTML elements <sup> valid markup 1`] = `Array []`;
exports[`HTML elements <svg> invalid markup 1`] = `Array []`;
exports[`HTML elements <svg> invalid markup 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "test-files/elements/svg-invalid.html",
"messages": Array [
Object {
"column": 2,
"context": undefined,
"line": 2,
"message": "<svg> is missing required \\"focusable\\" attribute",
"offset": 39,
"ruleId": "svg-focusable",
"severity": 2,
"size": 3,
},
],
"source": "<!-- requires focusable attribute -->
<svg></svg>
",
"warningCount": 0,
},
]
`;
exports[`HTML elements <svg> valid markup 1`] = `Array []`;
......
{
"name": "html-validate",
"version": "1.8.0",
"version": "1.9.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -2637,12 +2637,12 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.2.0.tgz",
"integrity": "sha512-rOodtI+IvaO8USa6ValYOrdWm9eQBgqwsY+B0PPiB+aSiK6p6Z4l9jLn/jI3z3WM4mkABAhKIqvGIBl0AFRaLQ==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.3.0.tgz",
"integrity": "sha512-QgO/qmNye+rKsU7dan6pkBTSfpbyrHJidsw9bR3gZCrQNTB9eWQ5+UDkrrev/fu9xg6Qh7ebbx03IVuGnGRmEw==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "2.2.0",
"@typescript-eslint/experimental-utils": "2.3.0",
"eslint-utils": "^1.4.2",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^2.0.1",
......@@ -2650,20 +2650,20 @@
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.2.0.tgz",
"integrity": "sha512-IMhbewFs27Frd/ICHBRfIcsUCK213B8MsEUqvKFK14SDPjPR5JF6jgOGPlroybFTrGWpMvN5tMZdXAf+xcmxsA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.3.0.tgz",
"integrity": "sha512-ry+fgd0Hh33LyzS30bIhX/a1HJpvtnecjQjWxxsZTavrRa1ymdmX7tz+7lPrPAxB018jnNzwNtog6s3OhxPTAg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.2.0",
"@typescript-eslint/typescript-estree": "2.3.0",
"eslint-scope": "^5.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.2.0.tgz",
"integrity": "sha512-9/6x23A3HwWWRjEQbuR24on5XIfVmV96cDpGR9671eJv1ebFKHj2sGVVAwkAVXR2UNuhY1NeKS2QMv5P8kQb2Q==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.3.0.tgz",
"integrity": "sha512-WBxfwsTeCOsmQ7cLjow7lgysviBKUW34npShu7dxJYUQCbSG5nfZWZTgmQPKEc+3flpbSM7tjXjQOgETYp+njQ==",
"dev": true,
"requires": {
"glob": "^7.1.4",
......@@ -2725,32 +2725,32 @@
}
},
"@typescript-eslint/parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.2.0.tgz",
"integrity": "sha512-0mf893kj9L65O5sA7wP6EoYvTybefuRFavUNhT7w9kjhkdZodoViwVS+k3D+ZxKhvtL7xGtP/y/cNMJX9S8W4A==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.3.0.tgz",
"integrity": "sha512-Dc+LAtHts0yDuusxG0NVjGvrpPy2kZauxqPbfFs0fmcMB4JhNs+WwIDMFGWeKjbGoPt/SIUC9XJ7E0ZD/f8InQ==",
"dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "2.2.0",
"@typescript-eslint/typescript-estree": "2.2.0",
"@typescript-eslint/experimental-utils": "2.3.0",
"@typescript-eslint/typescript-estree": "2.3.0",
"eslint-visitor-keys": "^1.1.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.2.0.tgz",
"integrity": "sha512-IMhbewFs27Frd/ICHBRfIcsUCK213B8MsEUqvKFK14SDPjPR5JF6jgOGPlroybFTrGWpMvN5tMZdXAf+xcmxsA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.3.0.tgz",
"integrity": "sha512-ry+fgd0Hh33LyzS30bIhX/a1HJpvtnecjQjWxxsZTavrRa1ymdmX7tz+7lPrPAxB018jnNzwNtog6s3OhxPTAg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.2.0",
"@typescript-eslint/typescript-estree": "2.3.0",
"eslint-scope": "^5.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.2.0.tgz",
"integrity": "sha512-9/6x23A3HwWWRjEQbuR24on5XIfVmV96cDpGR9671eJv1ebFKHj2sGVVAwkAVXR2UNuhY1NeKS2QMv5P8kQb2Q==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.3.0.tgz",
"integrity": "sha512-WBxfwsTeCOsmQ7cLjow7lgysviBKUW34npShu7dxJYUQCbSG5nfZWZTgmQPKEc+3flpbSM7tjXjQOgETYp+njQ==",
"dev": true,
"requires": {
"glob": "^7.1.4",
......
{
"name": "html-validate",
"version": "1.8.0",
"version": "1.9.0",
"description": "html linter",
"keywords": [
"html",
......@@ -120,8 +120,8 @@
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.13.20",
"@typescript-eslint/eslint-plugin": "2.2.0",
"@typescript-eslint/parser": "2.2.0",
"@typescript-eslint/eslint-plugin": "2.3.0",
"@typescript-eslint/parser": "2.3.0",
"autoprefixer": "9.6.1",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
......
......@@ -37,6 +37,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -127,6 +128,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -248,6 +250,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -335,6 +338,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -426,6 +430,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -516,6 +521,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -590,6 +596,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "error",
"wcag/h30": "error",
......@@ -664,6 +671,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "off",
"wcag/h30": "error",
......@@ -715,6 +723,7 @@ Object {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
"void": "warn",
"wcag/h30": "error",
......
......@@ -30,6 +30,7 @@ module.exports = {
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-tbody": "error",
"svg-focusable": "error",
"unrecognized-char-ref": "error",
void: "error",
"wcag/h30": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule svg-focusable should contain documentation 1`] = `
Object {
"description": "Inline SVG elements in IE are focusable by default which may cause issues with tab-ordering. The \`focusable\` attribute should explicitly be set to avoid unintended behaviour.",
"url": "https://html-validate.org/rules/svg-focusable.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
describe("rule svg-focusable", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "svg-focusable": "error" },
});
});
it("should not report when <svg> has focusable attribute", () => {
const report = htmlvalidate.validateString('<svg focusable="false"></svg>');
expect(report).toBeValid();
});
it("should not report for boolean attribute", () => {
const report = htmlvalidate.validateString("<svg focusable></svg>");
expect(report).toBeValid();
});
it("should report error when attributes use single quotes", () => {
const report = htmlvalidate.validateString("<svg></svg>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"svg-focusable",
'<svg> is missing required "focusable" attribute'
);
});
it("should contain documentation", () => {
expect(
htmlvalidate.getRuleDocumentation("svg-focusable")
).toMatchSnapshot();
});
});
import { HtmlElement } from "../dom";
import { ElementReadyEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
class SvgFocusable extends Rule {
public documentation(): RuleDocumentation {
return {
description: `Inline SVG elements in IE are focusable by default which may cause issues with tab-ordering. The \`focusable\` attribute should explicitly be set to avoid unintended behaviour.`,
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("element:ready", (event: ElementReadyEvent) => {
if (event.target.is("svg")) {
this.validate(event.target);
}
});
}
private validate(svg: HtmlElement): void {
if (svg.hasAttribute("focusable")) {
return;
}
this.report(
svg,
`<${svg.tagName}> is missing required "focusable" attribute`
);
}
}
module.exports = SvgFocusable;
<!-- requires focusable attribute -->
<svg></svg>
<!-- foreign element should allow unknown children -->
<svg>
<svg focusable="false">
<g></g>
</svg>
<!-- foreign should always allow self-closed -->
<svg/>
<svg focusable="false"/>
<!-- should allow camelcase attributes -->
<svg viewBox="1 2 3 4"></svg>
<svg focusable="false" viewBox="1 2 3 4"></svg>
<!-- should allow nesting -->
<svg>
<svg focusable="false">
<svg></svg>
<svg/>
</svg>