Skip to content

Commit

Permalink
Add the require-accessible-name rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukehirao committed Mar 6, 2022
1 parent a79f895 commit 0eb3a1e
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/@markuplint/ml-core/markuplint-recommended.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"no-refer-to-non-existent-id": true,
"no-use-event-handler-attr": false,
"permitted-contents": true,
"require-accessible-name": true,
"required-attr": true,
"required-element": false,
"required-h1": true,
Expand Down
3 changes: 3 additions & 0 deletions packages/@markuplint/rules/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
"permitted-contents": {
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/permitted-contents/schema.json"
},
"require-accessible-name": {
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/require-accessible-name/schema.json"
},
"required-attr": {
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/required-attr/schema.json"
},
Expand Down
1 change: 1 addition & 0 deletions packages/@markuplint/rules/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export function getRoleSpec(specs: Readonly<MLMLSpec>, roleName: string) {
return {
name: role.name,
isAbstract: !!role.isAbstract,
accessibleNameRequired: role.accessibleNameRequired,
statesAndProps: role.ownedAttribute,
superClassRoles,
};
Expand Down
2 changes: 2 additions & 0 deletions packages/@markuplint/rules/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import NoHardCodeId from './no-hard-code-id';
import NoReferToNonExistentId from './no-refer-to-non-existent-id';
import NoUseEventHandlerAttr from './no-use-event-handler-attr';
import PermittedContents from './permitted-contents';
import RequireAccessibleName from './require-accessible-name';
import RequiredAttr from './required-attr';
import RequiredElement from './required-element';
import RequiredH1 from './required-h1';
Expand Down Expand Up @@ -54,6 +55,7 @@ export default {
'no-refer-to-non-existent-id': NoReferToNonExistentId,
'no-use-event-handler-attr': NoUseEventHandlerAttr,
'permitted-contents': PermittedContents,
'require-accessible-name': RequireAccessibleName,
'required-attr': RequiredAttr,
'required-element': RequiredElement,
'required-h1': RequiredH1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# アクセシブルな名前必須(`require-accessible-name`)

要素にアクセシブルな名前がなければ警告します。名前が必要かどうかは ARIA ロールによって異なります。

## ルールの詳細

👎 間違ったコード例

```html
<button>
<span></span>
<span></span>
<span></span>
</button>
```

👍 正しいコード例

```html
<button>
<span class="visually-hidden">Menu</span>
<span></span>
<span></span>
<span></span>
</button>
```

### 設定値

- 型: `boolean`
- デフォルト値: `true`

### デフォルトの警告の厳しさ

`error`
41 changes: 41 additions & 0 deletions packages/@markuplint/rules/src/require-accessible-name/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: 'Require accessible name'
id: 'require-accessible-name'
category: 'a11y'
---

# Require accessible name

Warn if the element has no accessible name. It is according to its ARIA role whether name required.

## Rule Details

👎 Examples of **incorrect** code for this rule

```html
<button>
<span></span>
<span></span>
<span></span>
</button>
```

👍 Examples of **correct** code for this rule

```html
<button>
<span class="visually-hidden">Menu</span>
<span></span>
<span></span>
<span></span>
</button>
```

### Interface

- Type: `boolean`
- Deafult Value: `true`

### Default severity

`error`
102 changes: 102 additions & 0 deletions packages/@markuplint/rules/src/require-accessible-name/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { mlRuleTest } from 'markuplint';

import rule from './';

it('has accessible name', async () => {
const { violations } = await mlRuleTest(rule, '<button>Label</button>');
expect(violations.length).toBe(0);
});

it('has accessible name', async () => {
const { violations } = await mlRuleTest(rule, '<button aria-label="Label"></button>');
expect(violations.length).toBe(0);
});

it("does'nt have accessible name", async () => {
const { violations } = await mlRuleTest(rule, '<button></button>');
expect(violations).toStrictEqual([
{
severity: 'error',
col: 1,
line: 1,
message: 'Require accessible name',
raw: '<button>',
},
]);
});

it("does'nt have accessible name", async () => {
const { violations } = await mlRuleTest(rule, '<input type="text">');
expect(violations).toStrictEqual([
{
severity: 'error',
col: 1,
line: 1,
message: 'Require accessible name',
raw: '<input type="text">',
},
]);
});

it('has accessible name', async () => {
const { violations } = await mlRuleTest(rule, '<input type="text" aria-label="Label">');
expect(violations.length).toBe(0);
});

it('has accessible name', async () => {
const { violations } = await mlRuleTest(rule, '<input type="text" id="foo"><label for="foo">Label</label>');
expect(violations.length).toBe(0);
});

it("does'nt have accessible name", async () => {
const { violations } = await mlRuleTest(rule, '<input type="text" id="foo"><label for="foo2">Label</label>');
expect(violations).toStrictEqual([
{
severity: 'error',
col: 1,
line: 1,
message: 'Require accessible name',
raw: '<input type="text" id="foo">',
},
]);
});

it('has accessible name', async () => {
const { violations } = await mlRuleTest(rule, '<label><input type="text">Label</label>');
expect(violations.length).toBe(0);
});

it("does'nt have accessible name", async () => {
const { violations } = await mlRuleTest(
rule,
`
<button>
<span></span>
<span></span>
<span></span>
</button>`,
);
expect(violations).toStrictEqual([
{
severity: 'error',
col: 1,
line: 2,
message: 'Require accessible name',
raw: '<button>',
},
]);
});

it('has accessible name', async () => {
const { violations } = await mlRuleTest(
rule,
`
<button>
<span class="visually-hidden">Menu</span>
<span></span>
<span></span>
<span></span>
</button>`,
);
expect(violations.length).toBe(0);
});
24 changes: 24 additions & 0 deletions packages/@markuplint/rules/src/require-accessible-name/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createRule } from '@markuplint/ml-core';

import { getComputedRole, getRoleSpec } from '../helpers';

export default createRule<boolean, null>({
async verify({ document, report, t }) {
await document.walkOn('Element', el => {
const role = getComputedRole(document.specs, el);
if (!role) {
return;
}
const roleSpec = getRoleSpec(document.specs, role.name);
if (!roleSpec || !roleSpec.accessibleNameRequired) {
return;
}

const hasAccessibleName = !!el.getAccessibleName().trim();

if (!hasAccessibleName) {
report({ scope: el, message: t('Require {0}', 'accessible name') });
}
});
},
});
30 changes: 30 additions & 0 deletions packages/@markuplint/rules/src/require-accessible-name/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"value": {
"type": "boolean"
}
},
"oneOf": [
{
"type": "boolean",
"enum": [false]
},
{
"$ref": "#/definitions/value"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"value": { "$ref": "#/definitions/value" },
"severity": {
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/ml-config/schema.json#/definitions/severity"
},
"reason": {
"type": "string"
}
}
}
]
}
2 changes: 1 addition & 1 deletion packages/markuplint/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('basic test', () => {

it('is reported from 007.html', async () => {
const { violations } = await mlTestFile('test/fixture/007.html');
expect(violations.length).toEqual(31);
expect(violations.length).toEqual(55);
});

it('is ignoring 008.html', async () => {
Expand Down
1 change: 1 addition & 0 deletions test/fixture/.markuplintrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"landmark-roles": true,
"no-boolean-attr-value": true,
"permitted-contents": true,
"require-accessible-name": true,
"required-attr": true,
"required-h1": true,
"wai-aria": true
Expand Down
1 change: 1 addition & 0 deletions website/src/pages/rules/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ title: Rules

- [`landmark-roles`](/rules/landmark-roles)
- [`no-refer-to-non-existent-id`](/rules/no-refer-to-non-existent-id)
- [`require-accessible-name`](/rules/require-accessible-name)
- [`required-h1`](/rules/required-h1)
- [`wai-aria`](/rules/wai-aria)

Expand Down
1 change: 1 addition & 0 deletions website/src/pages/set-up-rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ This file is _loaded automatically_ when CLI executes or open a target file thro
"no-refer-to-non-existent-id": true,
"no-use-event-handler-attr": true,
"permitted-contents": true,
"require-accessible-name": true,
"required-attr": true,
"required-element": true,
"required-h1": true,
Expand Down

0 comments on commit 0eb3a1e

Please sign in to comment.