Skip to content

Commit 96e0970

Browse files
authored
feat: a11y-requried-layout-as-attribute ルールを追加する (#150)
1 parent 9d86ef8 commit 96e0970

File tree

5 files changed

+182
-0
lines changed

5 files changed

+182
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-sectioning-content-in-form)
1515
- [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
1616
- [a11y-replace-unreadable-symbol](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-replace-unreadable-symbol)
17+
- [a11y-required-layout-as-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-required-layout-as-attribute)
1718
- [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
1819
- [best-practice-for-button-element](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-button-element)
1920
- [best-practice-for-data-test-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-data-test-attribute)

rules/a11y-heading-in-sectioning-content/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const EXPECTED_NAMES = {
1111
'Section$': 'Section$',
1212
'ModelessDialog$': 'ModelessDialog$',
1313
'Center$': 'Center$',
14+
'Cluster$': '(Cluster)$',
1415
'Reel$': 'Reel$',
1516
'Sidebar$': 'Sidebar$',
1617
'Stack$': 'Stack$',
@@ -41,6 +42,7 @@ const UNEXPECTED_NAMES = {
4142
unexpectedMessageTemplate,
4243
],
4344
'Center$': '(Center)$',
45+
'Cluster$': '(Cluster)$',
4446
'Reel$': '(Reel)$',
4547
'Sidebar$': '(Sidebar)$',
4648
'Stack$': '(Stack)$',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# smarthr/a11y-required-layout-as-attribute
2+
3+
- smarthr-ui/Layoutに属するコンポーネントはデフォルトではdiv要素を出力します
4+
- そのため他の一部コンポーネントとの組み合わせによってはinvalidなマークアップになる場合が起こり得ます
5+
- 例: FormControlのtitle属性内でClusterを使うと `label > div` の構造になるためinvalid
6+
- 対象コンポーネントの使用方法をチェックし、適切なマークアップになるよう、as・forwardedAs属性の利用を促します
7+
8+
## rules
9+
10+
```js
11+
{
12+
rules: {
13+
'smarthr/a11y-required-layout-as-attribute': [
14+
'error', // 'warn', 'off'
15+
]
16+
},
17+
}
18+
```
19+
20+
## ❌ Incorrect
21+
22+
```jsx
23+
<Heading>
24+
<Cluster>any</Cluster>
25+
</Heading>
26+
27+
<HogeFormControl title={
28+
<FugaCluster>any</FugaCluster>
29+
} />
30+
31+
<StyledFieldset title={
32+
<Cluster>any</Cluster>
33+
}>
34+
// any
35+
</StyledFieldset>
36+
```
37+
38+
## ✅ Correct
39+
40+
```jsx
41+
<Heading>
42+
<Cluster as="span">any</Cluster>
43+
</Heading>
44+
45+
<HogeFormControl title={
46+
<FugaCluster forwardedAs="span">any</FugaCluster>
47+
} />
48+
49+
<StyledFieldset title={
50+
<Cluster as="strong">any</Cluster>
51+
}>
52+
// any
53+
</StyledFieldset>
54+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const { generateTagFormatter } = require('../../libs/format_styled_components')
2+
3+
const LAYOUT_EXPECTED_NAMES = {
4+
'Center$': '(Center)$',
5+
'Cluster$': '(Cluster)$',
6+
'Reel$': '(Reel)$',
7+
'Sidebar$': '(Sidebar)$',
8+
'Stack$': '(Stack)$',
9+
'Base$': '(Base)$',
10+
'BaseColumn$': '(BaseColumn)$',
11+
}
12+
const EXPECTED_NAMES = {
13+
...LAYOUT_EXPECTED_NAMES,
14+
'PageHeading$': '(PageHeading)$',
15+
'Heading$': '(Heading)$',
16+
'^h1$': '(PageHeading)$',
17+
'^h(|2|3|4|5|6)$': '(Heading)$',
18+
}
19+
20+
const UNEXPECTED_NAMES = {
21+
...LAYOUT_EXPECTED_NAMES,
22+
'(Heading|^h(1|2|3|4|5|6))$': '(Heading)$',
23+
}
24+
25+
const layoutRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/
26+
const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
27+
const asRegex = /^(as|forwardedAs)$/
28+
const formControlRegex = /(FormControl|Fieldset)$/
29+
30+
const findAsAttr = (a) => a.name?.name.match(asRegex)
31+
32+
const searchBubbleUp = (node) => {
33+
switch (node.type) {
34+
case 'Program':
35+
// rootまで検索した場合は確定でエラーにする
36+
return null
37+
case 'JSXElement': {
38+
const name = node.openingElement.name.name || ''
39+
40+
if (headingRegex.test(name)) {
41+
return name
42+
}
43+
44+
break
45+
}
46+
case 'JSXAttribute': {
47+
const name = node.name.name || ''
48+
49+
if (name === 'title' && formControlRegex.test(node.parent.name.name)) {
50+
return `${node.parent.name.name}のtitle属性`
51+
}
52+
}
53+
}
54+
55+
return searchBubbleUp(node.parent)
56+
}
57+
58+
/**
59+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
60+
*/
61+
module.exports = {
62+
meta: {
63+
type: 'problem',
64+
schema: [],
65+
},
66+
create(context) {
67+
return {
68+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
69+
JSXOpeningElement: (node) => {
70+
const name = node.name.name || ''
71+
72+
if (layoutRegex.test(name) && !node.attributes.some(findAsAttr)) {
73+
const parentName = searchBubbleUp(node.parent.parent)
74+
75+
if (parentName) {
76+
context.report({
77+
node,
78+
message: `${name}${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります
79+
- smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します
80+
- ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます
81+
- span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`,
82+
})
83+
}
84+
}
85+
},
86+
}
87+
},
88+
}
89+
90+
module.exports.schema = []
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const rule = require('../rules/a11y-required-layout-as-attribute')
2+
const RuleTester = require('eslint').RuleTester
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 12,
7+
ecmaFeatures: {
8+
experimentalObjectRestSpread: true,
9+
jsx: true,
10+
},
11+
sourceType: 'module',
12+
},
13+
})
14+
15+
const generateErrorText = (parentName, name) => `${name}${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります
16+
- smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します
17+
- ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます
18+
- span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`
19+
20+
ruleTester.run('a11y-anchor-has-href-attribute', rule, {
21+
valid: [
22+
{ code: `<h1><Cluster as="span">ほげ</Cluster></h1>` },
23+
{ code: `<Heading><Cluster as="strong" /></Heading>` },
24+
{ code: `<StyledHeading><AnyCluster forwardedAs="span" /></StyledHeading>` },
25+
{ code: `<FormControl title={<Cluster as="span" />} />` },
26+
{ code: `<StyledFieldset title={<AnyCluster forwardedAs="strong" />} />` },
27+
],
28+
invalid: [
29+
{ code: `<h1><Cluster>ほげ</Cluster></h1>`, errors: [{ message: generateErrorText('h1', 'Cluster') }] },
30+
{ code: `<Heading><Cluster /></Heading>`, errors: [{ message: generateErrorText('Heading', 'Cluster') }] },
31+
{ code: `<StyledHeading><AnyCluster /></StyledHeading>`, errors: [{ message: generateErrorText('StyledHeading', 'AnyCluster') }] },
32+
{ code: `<FormControl title={<Cluster />} />`, errors: [{ message: generateErrorText('FormControlのtitle属性', 'Cluster') }] },
33+
{ code: `<StyledFieldset title={<AnyCluster />} />`, errors: [{ message: generateErrorText('StyledFieldsetのtitle属性', 'AnyCluster') }] },
34+
]
35+
})

0 commit comments

Comments
 (0)