From 22353a34f226d8a6117c8911824eeda098f4fddf Mon Sep 17 00:00:00 2001 From: wadackel Date: Wed, 11 May 2022 02:14:23 +0900 Subject: [PATCH 1/2] chore(acot-preset-wcag): migrate to scaffdog v1 --- packages/acot-preset-wcag/.scaffdog/01-rule.md | 15 +++++++++------ packages/acot-preset-wcag/.scaffdog/config.js | 3 +++ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 packages/acot-preset-wcag/.scaffdog/config.js diff --git a/packages/acot-preset-wcag/.scaffdog/01-rule.md b/packages/acot-preset-wcag/.scaffdog/01-rule.md index 620cebee..43c5ebf2 100644 --- a/packages/acot-preset-wcag/.scaffdog/01-rule.md +++ b/packages/acot-preset-wcag/.scaffdog/01-rule.md @@ -1,16 +1,19 @@ --- name: 'rule' -description: 'Generate a rule.' -message: 'Please enter the rule name.' root: '.' output: '.' -ignore: ['**/*'] +questions: + name: 'Please enter the rule name.' --- -# `docs/rules/{{ input }}.md` +# Variables + +- name: `{{ inputs.name | kebab }}` + +# `docs/rules/{{ name }}.md` ````markdown -# {{ input }} +# {{ name }} **TODO:** Short summary. @@ -29,7 +32,7 @@ ignore: ['**/*'] ``` ```` -# `src/rules/{{ input }}.ts` +# `src/rules/{{ name }}.ts` ```typescript import { createRule } from '@acot/core'; diff --git a/packages/acot-preset-wcag/.scaffdog/config.js b/packages/acot-preset-wcag/.scaffdog/config.js new file mode 100644 index 00000000..2adf5270 --- /dev/null +++ b/packages/acot-preset-wcag/.scaffdog/config.js @@ -0,0 +1,3 @@ +module.exports = { + files: ['./*'], +}; From 2a70d7e99342c360a0f788ddd96d357dc3a34944 Mon Sep 17 00:00:00 2001 From: wadackel Date: Wed, 11 May 2022 02:14:49 +0900 Subject: [PATCH 2/2] feat(acot-preset-wcag): add `invalid-id-reference` rule --- packages/acot-preset-wcag/README.md | 57 +-- .../docs/rules/invalid-id-reference.md | 351 ++++++++++++++++++ .../src/configs/recommended.ts | 1 + packages/acot-preset-wcag/src/rules/index.ts | 2 + .../src/rules/invalid-id-reference.ts | 118 ++++++ 5 files changed, 501 insertions(+), 28 deletions(-) create mode 100644 packages/acot-preset-wcag/docs/rules/invalid-id-reference.md create mode 100644 packages/acot-preset-wcag/src/rules/invalid-id-reference.ts diff --git a/packages/acot-preset-wcag/README.md b/packages/acot-preset-wcag/README.md index 5c449d29..23e3b459 100644 --- a/packages/acot-preset-wcag/README.md +++ b/packages/acot-preset-wcag/README.md @@ -35,17 +35,18 @@ You can also enable all the recommended rules for our preset. Add `preset:@acot/ -| Name | Summary | :heavy_check_mark: | -| :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :----------------- | -| [`@acot/wcag/dialog-focus`](./docs/rules/dialog-focus.md) | Move focus to inside dialog or set dialog after trigger. | :heavy_check_mark: | -| [`@acot/wcag/focusable-has-indicator`](./docs/rules/focusable-has-indicator.md) | Focusable element has a focus indicator. | :heavy_check_mark: | -| [`@acot/wcag/img-has-name`](./docs/rules/img-has-name.md) | The `img` element or img role MUST has name. | :heavy_check_mark: | -| [`@acot/wcag/interactive-has-enough-size`](./docs/rules/interactive-has-enough-size.md) | The size of the target for pointer inputs is at least 44 by 44 CSS pixels. | :heavy_check_mark: | -| [`@acot/wcag/interactive-has-name`](./docs/rules/interactive-has-name.md) | Interactive elements MUST has name. | :heavy_check_mark: | -| [`@acot/wcag/interactive-supports-focus`](./docs/rules/interactive-supports-focus.md) | _T.B.A_ | | -| [`@acot/wcag/link-has-name`](./docs/rules/link-has-name.md) | Link MUST has name. | :heavy_check_mark: | -| [`@acot/wcag/page-has-title`](./docs/rules/page-has-title.md) | Web pages have titles that describe topic or purpose. WCAG 2.1 - 2.4.2. | :heavy_check_mark: | -| [`@acot/wcag/page-has-valid-lang`](./docs/rules/page-has-valid-lang.md) | The `html` element MUST has a valid lang attribute. | :heavy_check_mark: | +| Name | Summary | :heavy_check_mark: | +| :-------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :----------------- | +| [`@acot/wcag/dialog-focus`](./docs/rules/dialog-focus.md) | Move focus to inside dialog or set dialog after trigger. | :heavy_check_mark: | +| [`@acot/wcag/focusable-has-indicator`](./docs/rules/focusable-has-indicator.md) | Focusable element has a focus indicator. | :heavy_check_mark: | +| [`@acot/wcag/img-has-name`](./docs/rules/img-has-name.md) | The `img` element or img role MUST has name. | :heavy_check_mark: | +| [`@acot/wcag/interactive-has-enough-size`](./docs/rules/interactive-has-enough-size.md) | The size of the target for pointer inputs is at least 44 by 44 CSS pixels. | :heavy_check_mark: | +| [`@acot/wcag/interactive-has-name`](./docs/rules/interactive-has-name.md) | Interactive elements MUST has name. | :heavy_check_mark: | +| [`@acot/wcag/interactive-supports-focus`](./docs/rules/interactive-supports-focus.md) | _T.B.A_ | | +| [`@acot/wcag/invalid-id-reference`](./docs/rules/invalid-id-reference.md) | The target of the ID reference or ID reference list MUST exist in the same document. | :heavy_check_mark: | +| [`@acot/wcag/link-has-name`](./docs/rules/link-has-name.md) | Link MUST has name. | :heavy_check_mark: | +| [`@acot/wcag/page-has-title`](./docs/rules/page-has-title.md) | Web pages have titles that describe topic or purpose. WCAG 2.1 - 2.4.2. | :heavy_check_mark: | +| [`@acot/wcag/page-has-valid-lang`](./docs/rules/page-has-valid-lang.md) | The `html` element MUST has a valid lang attribute. | :heavy_check_mark: | @@ -55,9 +56,9 @@ You can also enable all the recommended rules for our preset. Add `preset:@acot/ #### 1.1 Text Alternatives -| Success criteria | acot rules | -| ---------------------- | ------------------------------------------------------- | -| 1.1.1 Non-text Content | [@acot/wcag/img-has-name](./docs/rules/img-has-name.md) | +| Success criteria | acot rules | +| ---------------------- | ---------- | +| 1.1.1 Non-text Content | - | #### 1.2 Time-based Media @@ -75,14 +76,14 @@ You can also enable all the recommended rules for our preset. Add `preset:@acot/ #### 1.3 Adaptable -| Success criteria | acot rules | -| ----------------------------- | ---------- | -| 1.3.1 Info and Relationships | - | -| 1.3.2 Meaningful Sequence | - | -| 1.3.3 Sensory Characteristics | - | -| 1.3.4 Orientation | - | -| 1.3.5 Identify Input Purpose | - | -| 1.3.6 Identify Purpose | - | +| Success criteria | acot rules | +| ----------------------------- | ----------------------------------------------------------------------- | +| 1.3.1 Info and Relationships | [@acot/wcag/invalid-id-reference](./docs/rules/invalid-id-reference.md) | +| 1.3.2 Meaningful Sequence | - | +| 1.3.3 Sensory Characteristics | - | +| 1.3.4 Orientation | - | +| 1.3.5 Identify Input Purpose | - | +| 1.3.6 Identify Purpose | - | #### 1.4 Distinguishable @@ -139,7 +140,7 @@ You can also enable all the recommended rules for our preset. Add `preset:@acot/ | 2.4.1 Bypass Blocks | - | | 2.4.2 Page Titled | [@acot/wcag/page-has-title](./docs/rules/page-has-title.md) | | 2.4.3 Focus Order | [@acot/wcag/dialog-focus](./docs/rules/dialog-focus.md) | -| 2.4.4 Link Purpose (In Context) | [@acot/wcag/link-has-name](./docs/rules/link-has-name.md)- | +| 2.4.4 Link Purpose (In Context) | [@acot/wcag/link-has-name](./docs/rules/link-has-name.md) | | 2.4.5 Multiple Ways | - | | 2.4.6 Headings and Labels | - | | 2.4.7 Focus Visible | [@acot/wcag/focusable-has-indicator](./docs/rules/focusable-has-indicator.md) | @@ -196,11 +197,11 @@ You can also enable all the recommended rules for our preset. Add `preset:@acot/ #### 4.1 Compatible -| Success criteria | acot rules | -| ----------------------- | --------------------------------------------------------- | -| 4.1.1 Parsing | - | -| 4.1.2 Name, Role, Value | [@acot/wcag/link-has-name](./docs/rules/link-has-name.md) | -| 4.1.3 Status Messages | - | +| Success criteria | acot rules | +| ----------------------- | ----------------------------------------------------------------------- | +| 4.1.1 Parsing | [@acot/wcag/invalid-id-reference](./docs/rules/invalid-id-reference.md) | +| 4.1.2 Name, Role, Value | [@acot/wcag/link-has-name](./docs/rules/link-has-name.md) | +| 4.1.3 Status Messages | - | ## Concept diff --git a/packages/acot-preset-wcag/docs/rules/invalid-id-reference.md b/packages/acot-preset-wcag/docs/rules/invalid-id-reference.md new file mode 100644 index 00000000..9db1aafa --- /dev/null +++ b/packages/acot-preset-wcag/docs/rules/invalid-id-reference.md @@ -0,0 +1,351 @@ +# invalid-id-reference + +The target of the ID reference or ID reference list MUST exist in the same document. + +## Supported Attributes + +- [x] [for](https://html.spec.whatwg.org/multipage/forms.html#attr-label-for) +- [x] [headers](https://html.spec.whatwg.org/multipage/tables.html#attr-tdth-headers) +- [x] [list](https://html.spec.whatwg.org/multipage/input.html#the-list-attribute) +- [x] [aria-activedescendant](https://www.w3.org/TR/wai-aria/#aria-activedescendant) +- [x] [aria-controls](https://www.w3.org/TR/wai-aria/#aria-controls) +- [x] [aria-describedby](https://www.w3.org/TR/wai-aria/#aria-describedby) +- [x] [aria-details](https://www.w3.org/TR/wai-aria/#aria-details) +- [x] [aria-errormessage](https://www.w3.org/TR/wai-aria/#aria-errormessage) +- [x] [aria-flowto](https://www.w3.org/TR/wai-aria/#aria-flowto) +- [x] [aria-labelledby](https://www.w3.org/TR/wai-aria/#aria-labelledby) +- [x] [aria-owns](https://www.w3.org/TR/wai-aria/#aria-owns) + +## Related Success Criteria + +- [WCAG 2.1 - 1.1.1: Non-text Content](https://www.w3.org/TR/WCAG21/#non-text-content) +- [WCAG 2.1 - 1.3.1: Info and Relationships](https://www.w3.org/TR/WCAG21/#info-and-relationships) +- [WCAG 2.1 - 1.3.2 Meaningful Sequence](https://www.w3.org/TR/WCAG21/#meaningful-sequence) +- [WCAG 2.1 - 1.3.6: Identify Purpose](https://www.w3.org/TR/WCAG21/#identify-purpose) +- [WCAG 2.1 - 2.4.1: Bypass Blocks](https://www.w3.org/TR/WCAG21/#bypass-blocks) +- [WCAG 2.1 - 2.4.4: Link Purpose (In Context)](https://www.w3.org/TR/WCAG21/#link-purpose-in-context) +- [WCAG 2.1 - 3.3.2: Labels or Instructions](https://www.w3.org/TR/WCAG21/#labels-or-instructions) +- [WCAG 2.1 - 4.1.2: Name, Role, Value](https://www.w3.org/TR/WCAG21/#name-role-value) + +## :white_check_mark: Correct + +**Example:** `for` + +> https://www.w3.org/WAI/WCAG21/Techniques/html/H44 + +```html + + +``` + +**Example:** `headers` + +> https://www.w3.org/WAI/WCAG21/Techniques/html/H43 + +```html + + + + + + + + + + + + + + + + + + + + + + + +
HomeworkExamsProjects
12Final12Final
15%15%15%20%10%10%15%
+``` + +**Example:** `list` + +> https://html.spec.whatwg.org/multipage/form-elements.html#the-datalist-element + +```html + +``` + +**Example:** `aria-labelledby` + +> https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA7 + +```html +

Storms hit east coast

+ +

+ Torrential rain and gale force winds have struck the east coast, causing + flooding in many coastal towns. + Read more... +

+``` + +**Example:** `for`, `aria-describedby` + +> https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA1 + +```html +
+ + +

+ A bit of instructions for this field linked with aria-describedby. +

+
+``` + +**Example:** `aria-owns`, `aria-controls`, `aria-activedescendant` + +> https://www.w3.org/TR/wai-aria/#combobox + +```html +
+ +
+ + +``` + +**Example:** `aria-errormessage` + +> https://www.w3.org/TR/wai-aria-1.2/#example-23 + +```html + + +Invalid time: the time must be between 9:00 AM and 5:00 PM +``` + +**Example:** `aria-details` + +> https://www.w3.org/TR/wai-aria-1.2/#example-21 + +```html +bear +
+ Example +

Bear-themed placeholder images for developers

+
+``` + +**Example:** `aria-flowto` + +> https://www.w3.org/WAI/GL/wiki/Using_aria-flowto + +```html +

The Daily Planet

+ +

Weather

+
+

+ The weather will be what it will be, if you want a forecast look out your + window and have a guess. That's what the pros do! +

+
+ +

Election results

+
+

Whoever you vote for government wins. ANOK 1984

+
+ +

Sports

+
+

+ Today there will be lots of goals scored, tennis played and footballs kicked +

+
+``` + +## :warning: Incorrect + +**Example:** `for` + +```html + + +``` + +**Example:** `headers` + +```html + + + + + + + + + + + + + + + + + + + + + + + +
HomeworkExamsProjects
12Final12Final
15%15%15%20%10%10%15%
+``` + +**Example:** `list` + +```html + +``` + +**Example:** `aria-labelledby` + +```html +

Storms hit east coast

+ +

+ Torrential rain and gale force winds have struck the east coast, causing + flooding in many coastal towns. + Read more... +

+``` + +**Example:** `for`, `aria-describedby` + +```html +
+ + +

+ A bit of instructions for this field linked with aria-describedby. +

+
+``` + +**Example:** `aria-owns`, `aria-controls`, `aria-activedescendant` + +```html +
+ +
+ + +``` + +**Example:** `aria-errormessage` + +```html + + +Invalid time: the time must be between 9:00 AM and 5:00 PM +``` + +**Example:** `aria-details` + +```html +bear +
+ Example +

Bear-themed placeholder images for developers

+
+``` + +**Example:** `aria-flowto` + +```html +

The Daily Planet

+ +

Weather

+
+

+ The weather will be what it will be, if you want a forecast look out your + window and have a guess. That's what the pros do! +

+
+ +

Election results

+
+

Whoever you vote for government wins. ANOK 1984

+
+ +

Sports

+
+

+ Today there will be lots of goals scored, tennis played and footballs kicked +

+
+``` diff --git a/packages/acot-preset-wcag/src/configs/recommended.ts b/packages/acot-preset-wcag/src/configs/recommended.ts index dafd0ab5..00c75a75 100644 --- a/packages/acot-preset-wcag/src/configs/recommended.ts +++ b/packages/acot-preset-wcag/src/configs/recommended.ts @@ -8,6 +8,7 @@ const config: Config = { '@acot/wcag/img-has-name': 'error', '@acot/wcag/interactive-has-enough-size': 'warn', '@acot/wcag/interactive-has-name': 'error', + '@acot/wcag/invalid-id-reference': 'error', '@acot/wcag/link-has-name': 'error', '@acot/wcag/page-has-title': 'error', '@acot/wcag/page-has-valid-lang': 'error', diff --git a/packages/acot-preset-wcag/src/rules/index.ts b/packages/acot-preset-wcag/src/rules/index.ts index 31486584..e3015b9c 100644 --- a/packages/acot-preset-wcag/src/rules/index.ts +++ b/packages/acot-preset-wcag/src/rules/index.ts @@ -5,6 +5,7 @@ import imgHasName from './img-has-name'; import interactiveHasEnoughSize from './interactive-has-enough-size'; import interactiveHasName from './interactive-has-name'; import interactiveSupportsFocus from './interactive-supports-focus'; +import invalidIdReference from './invalid-id-reference'; import linkHasName from './link-has-name'; import pageHasTitle from './page-has-title'; import pageHasValidLang from './page-has-valid-lang'; @@ -16,6 +17,7 @@ export const rules: RuleRecord = { 'interactive-has-enough-size': interactiveHasEnoughSize, 'interactive-has-name': interactiveHasName, 'interactive-supports-focus': interactiveSupportsFocus, + 'invalid-id-reference': invalidIdReference, 'link-has-name': linkHasName, 'page-has-title': pageHasTitle, 'page-has-valid-lang': pageHasValidLang, diff --git a/packages/acot-preset-wcag/src/rules/invalid-id-reference.ts b/packages/acot-preset-wcag/src/rules/invalid-id-reference.ts new file mode 100644 index 00000000..998e7c19 --- /dev/null +++ b/packages/acot-preset-wcag/src/rules/invalid-id-reference.ts @@ -0,0 +1,118 @@ +import { createRule } from '@acot/core'; + +const attributes: { + name: string; + list: boolean; + help: string; +}[] = [ + { + name: 'for', + list: false, + help: 'https://html.spec.whatwg.org/multipage/forms.html#attr-label-for', + }, + { + name: 'headers', + list: true, + help: 'https://html.spec.whatwg.org/multipage/tables.html#attr-tdth-headers', + }, + { + name: 'list', + list: false, + help: 'https://html.spec.whatwg.org/multipage/input.html#the-list-attribute', + }, + { + name: 'aria-activedescendant', + list: false, + help: 'https://www.w3.org/TR/wai-aria/#aria-activedescendant', + }, + { + name: 'aria-controls', + list: true, + help: 'https://www.w3.org/TR/wai-aria/#aria-controls', + }, + { + name: 'aria-describedby', + list: true, + help: 'https://www.w3.org/TR/wai-aria/#aria-describedby', + }, + { + name: 'aria-details', + list: false, + help: 'https://www.w3.org/TR/wai-aria/#aria-details', + }, + { + name: 'aria-errormessage', + list: false, + help: 'https://www.w3.org/TR/wai-aria/#aria-errormessage', + }, + { + name: 'aria-flowto', + list: true, + help: 'https://www.w3.org/TR/wai-aria/#aria-flowto', + }, + { + name: 'aria-labelledby', + list: true, + help: 'https://www.w3.org/TR/wai-aria/#aria-labelledby', + }, + { + name: 'aria-owns', + list: true, + help: 'https://www.w3.org/TR/wai-aria/#aria-owns', + }, +]; + +type Options = {}; + +export default createRule({ + meta: { + recommended: true, + }, + + test: async (context) => { + await Promise.all( + attributes.map(async (attr) => { + const nodes = await context.page.$$(`[${attr.name}]`); + + await Promise.all( + nodes.map(async (node) => { + const [valid, invalid] = await node.evaluate((el, attr) => { + const value = el.getAttribute(attr.name)!; + const ids = attr.list ? value.split(/\s+/g) : [value]; + + return ids.reduce<[string[], string[]]>( + (acc, cur) => { + const id = cur.trim(); + if (document.getElementById(id) != null) { + acc[0].push(id); + } else { + acc[1].push(id); + } + return acc; + }, + [[], []], + ); + }, attr); + + const joined = { + valid: valid.join(', '), + invalid: invalid.join(', '), + }; + + context.debug( + `valid: "${joined.valid}" / invalid: "${joined.invalid}"`, + ); + + if (invalid.length > 0) { + await context.report({ + node, + message: `"${attr.name}" attribute referent element MUST exist in the same document. (invalid: "${joined.invalid}")`, + help: attr.help, + }); + } + }), + ); + }), + ); + }, +});