Commits (8)
# html-validate changelog
### [4.0.1](https://gitlab.com/html-validate/html-validate/compare/v4.0.0...v4.0.1) (2020-11-09)
### Bug Fixes
- **rules:** `wcag/h32` checks for `type="image"` ([4a43819](https://gitlab.com/html-validate/html-validate/commit/4a43819d90db59ae31846f766025d4ffce189391))
- **rules:** `wcag/h32` handles submit buttons using `form` attribute to associate ([cb2e843](https://gitlab.com/html-validate/html-validate/commit/cb2e8437ae6ca4a14b0fb4585cdec3157c5cf2a0))
## [4.0.0](https://gitlab.com/html-validate/html-validate/compare/v3.5.0...v4.0.0) (2020-11-07)
### ⚠ BREAKING CHANGES
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/wcag/h32.md inline validation: associated 1`] = `Array []`;
exports[`docs/rules/wcag/h32.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/wcag/h32.md inline validation: incorrect 1`] = `
......
......@@ -12,6 +12,10 @@ markup["correct"] = `<form>
</label>
<button type="submit">Submit</button>
</form>`;
markup["associated"] = `<form id="my-form">
...
</form>
<button form="my-form" type="submit">Submit</button>`;
describe("docs/rules/wcag/h32.md", () => {
it("inline validation: incorrect", () => {
......@@ -26,4 +30,10 @@ describe("docs/rules/wcag/h32.md", () => {
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: associated", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"wcag/h32":"error"}});
const report = htmlvalidate.validateString(markup["associated"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -7,9 +7,16 @@ summary: "WCAG 2.1 H32: Providing submit buttons"
# WCAG 2.1 H32: Providing submit buttons (`wcag/h32`)
[WCAG 2.1 technique H32][1] requires each `<form>` element to include at least
one submit button in order to allow users to interact with the form in a
predictable way. For instance pressing enter in an input field to submit.
[WCAG 2.1 technique H32][1] requires each `<form>` element to include at least one submit button in order to allow users to interact with the form in apredictable way.
For instance pressing enter in an input field to submit.
This rule checks for the presence of:
- `<button type="submit">`
- `<input type="submit">`
- `<input type="image">`
Submit buttons can either be nested or associated using the `form` attribute.
[1]: https://www.w3.org/WAI/WCAG21/Techniques/html/H32
......@@ -35,3 +42,12 @@ Examples of **correct** code for this rule:
<button type="submit">Submit</button>
</form>
</validate>
Submit buttons may also use the `form` attribute to associate with a form:
<validate name="associated" rules="wcag/h32">
<form id="my-form">
...
</form>
<button form="my-form" type="submit">Submit</button>
</validate>
This diff is collapsed.
{
"name": "html-validate",
"version": "4.0.0",
"version": "4.0.1",
"description": "html linter",
"keywords": [
"html",
......@@ -48,6 +48,7 @@
"eslint": "eslint --ext js,ts .",
"eslint:fix": "eslint --ext js,ts . --fix",
"htmlvalidate": "./bin/html-validate.js",
"prepare": "git config commit.template ./node_modules/@html-validate/commitlint-config/gitmessage",
"prettier:check": "prettier . --check",
"prettier:write": "prettier . --write",
"semantic-release": "semantic-release",
......@@ -97,7 +98,7 @@
"@babel/preset-env": "7.12.1",
"@commitlint/cli": "11.0.0",
"@html-validate/commitlint-config": "1.1.1",
"@html-validate/eslint-config": "2.0.0",
"@html-validate/eslint-config": "2.1.0",
"@html-validate/jest-config": "1.0.33",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.1.0",
......@@ -116,7 +117,7 @@
"canonical-path": "1.0.0",
"cssnano": "4.1.10",
"dgeni": "0.4.12",
"dgeni-front-matter": "2.0.2",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.28.4",
"eslint-plugin-array-func": "3.1.7",
"eslint-plugin-node": "11.1.0",
......@@ -147,7 +148,7 @@
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "26.4.3",
"ts-jest": "26.4.4",
"typescript": "4.0.5"
},
"jest": {
......
......@@ -18,35 +18,77 @@ describe("wcag/h32", () => {
});
});
it("should not report when form has submit button", () => {
it("should not report when form has nested submit button (button)", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<form><button type="submit"></button></form>');
const markup = '<form><button type="submit"></button></form>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report when form has submit button (input)", () => {
it("should not report when form has nested submit button (input)", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<form><input type="submit"></form>');
const markup = '<form><input type="submit"></form>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report when form has nested submit button (image)", () => {
expect.assertions(1);
const markup = '<form><input type="image"></form>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report when form has associated submit button", () => {
expect.assertions(1);
const markup = '<form id="foo"></form><button form="foo" type="submit"></button>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report when form has both nested and associated submit button", () => {
expect.assertions(1);
const markup = '<form id="foo"><button form="foo" type="submit"></button></form>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should report error when form is missing submit button", () => {
expect.assertions(2);
const report = htmlvalidate.validateString("<form></form>");
const markup = "<form></form>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError("wcag/h32", "<form> element must have a submit button");
});
it("should report error when form only has regular button", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<form><button type="button"></button></form>');
const markup = '<form><button type="button"></button></form>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError("wcag/h32", "<form> element must have a submit button");
});
it("should report error when form is associated with regular button", () => {
expect.assertions(2);
const markup = '<form id="foo"></form><button form="foo" type="button"></button>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError("wcag/h32", "<form> element must have a submit button");
});
it("should report error when form nested button is associated with another form", () => {
expect.assertions(2);
const markup = '<form id="foo"><button form="bar" type="submit"></button></form>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError("wcag/h32", "<form> element must have a submit button");
});
it("should support custom elements", () => {
expect.assertions(2);
const report = htmlvalidate.validateString("<my-form></my-form>");
const markup = "<my-form></my-form>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError("wcag/h32", "<my-form> element must have a submit button");
});
......
import { HtmlElement } from "../../dom";
import { DOMTree, HtmlElement } from "../../dom";
import { DOMReadyEvent } from "../../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../../rule";
......@@ -18,18 +18,52 @@ export default class H32 extends Rule {
const formSelector = formTags.join(",");
this.on("dom:ready", (event: DOMReadyEvent) => {
const forms = event.document.querySelectorAll(formSelector);
forms.forEach((node: HtmlElement) => {
/* find submit buttons */
for (const button of node.querySelectorAll("button,input")) {
const type = button.getAttribute("type");
if (type && type.valueMatches("submit")) {
return;
}
const { document } = event;
const forms = document.querySelectorAll(formSelector);
for (const form of forms) {
/* find nested submit buttons */
if (hasNestedSubmit(form)) {
continue;
}
/* find explicitly associated submit buttons */
if (hasAssociatedSubmit(document, form)) {
continue;
}
this.report(node, `<${node.tagName}> element must have a submit button`);
});
this.report(form, `<${form.tagName}> element must have a submit button`);
}
});
}
}
function isSubmit(node: HtmlElement): boolean {
const type = node.getAttribute("type");
return type && type.valueMatches(/submit|image/);
}
function isAssociated(id: string, node: HtmlElement): boolean {
const form = node.getAttribute("form");
return form && form.valueMatches(id, true);
}
function hasNestedSubmit(form: HtmlElement): boolean {
const matches = form
.querySelectorAll("button,input")
.filter(isSubmit)
.filter((node) => !node.hasAttribute("form"));
return matches.length > 0;
}
function hasAssociatedSubmit(document: DOMTree, form: HtmlElement): boolean {
const { id } = form;
if (!id) {
return false;
}
const matches = document
.querySelectorAll("button[form],input[form]")
.filter(isSubmit)
.filter((node) => isAssociated(id, node));
return matches.length > 0;
}