eslint-plugin-smarthr 0.5.16 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.node-version +1 -0
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/rules/a11y-form-control-in-form/index.js +1 -0
- package/rules/a11y-heading-in-sectioning-content/index.js +2 -0
- package/rules/a11y-prohibit-sectioning-content-in-form/README.md +62 -0
- package/rules/a11y-prohibit-sectioning-content-in-form/index.js +204 -0
- package/rules/a11y-required-layout-as-attribute/README.md +54 -0
- package/rules/a11y-required-layout-as-attribute/index.js +90 -0
- package/test/a11y-required-layout-as-attribute.js +35 -0
package/.node-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
20.11.1
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.5.18](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.17...v0.5.18) (2024-11-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* a11y-requried-layout-as-attribute ルールを追加する ([#150](https://github.com/kufu/eslint-plugin-smarthr/issues/150)) ([96e0970](https://github.com/kufu/eslint-plugin-smarthr/commit/96e09702ea59cfcd5a173e187300b9054047b65c))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* a11y-form-control-in-form でFilterDropdownをformとして扱うように修正 ([#151](https://github.com/kufu/eslint-plugin-smarthr/issues/151)) ([9d86ef8](https://github.com/kufu/eslint-plugin-smarthr/commit/9d86ef885b607582324df24e5743d42533108db3))
|
|
16
|
+
|
|
17
|
+
### [0.5.17](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.16...v0.5.17) (2024-11-04)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* a11y-prohibit-heading-in-formを追加 ([#148](https://github.com/kufu/eslint-plugin-smarthr/issues/148)) ([973f681](https://github.com/kufu/eslint-plugin-smarthr/commit/973f681fd3538224e7312b6eb38ea43e6fb17cc5))
|
|
23
|
+
|
|
5
24
|
### [0.5.16](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.15...v0.5.16) (2024-10-25)
|
|
6
25
|
|
|
7
26
|
### [0.5.15](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.14...v0.5.15) (2024-09-10)
|
package/README.md
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
- [a11y-numbered-text-within-ol](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-numbered-text-within-ol)
|
|
12
12
|
- [a11y-prohibit-input-maxlength-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-maxlength-attribute)
|
|
13
13
|
- [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
|
|
14
|
+
- [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-sectioning-content-in-form)
|
|
14
15
|
- [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
|
|
15
16
|
- [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)
|
|
16
18
|
- [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
|
|
17
19
|
- [best-practice-for-button-element](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-button-element)
|
|
18
20
|
- [best-practice-for-data-test-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-data-test-attribute)
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ const FORM_EXPECTED_NAMES = {
|
|
|
14
14
|
'((F|^f)orm)$': '(Form)$',
|
|
15
15
|
'(FormDialog)$': '(FormDialog)$',
|
|
16
16
|
'RemoteTrigger(.*)FormDialog$': 'RemoteTrigger(.*)FormDialog$',
|
|
17
|
+
'FilterDropdown$': '(FilterDropdown)$',
|
|
17
18
|
}
|
|
18
19
|
const EXPECTED_NAMES = {
|
|
19
20
|
...FORM_CONTROL_EXPECTED_NAMES,
|
|
@@ -11,6 +11,7 @@ const EXPECTED_NAMES = {
|
|
|
11
11
|
'Section$': 'Section$',
|
|
12
12
|
'ModelessDialog$': 'ModelessDialog$',
|
|
13
13
|
'Center$': 'Center$',
|
|
14
|
+
'Cluster$': '(Cluster)$',
|
|
14
15
|
'Reel$': 'Reel$',
|
|
15
16
|
'Sidebar$': 'Sidebar$',
|
|
16
17
|
'Stack$': 'Stack$',
|
|
@@ -41,6 +42,7 @@ const UNEXPECTED_NAMES = {
|
|
|
41
42
|
unexpectedMessageTemplate,
|
|
42
43
|
],
|
|
43
44
|
'Center$': '(Center)$',
|
|
45
|
+
'Cluster$': '(Cluster)$',
|
|
44
46
|
'Reel$': '(Reel)$',
|
|
45
47
|
'Sidebar$': '(Sidebar)$',
|
|
46
48
|
'Stack$': '(Stack)$',
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# smarthr/a11y-prohibit-sectioning-content-in-form
|
|
2
|
+
|
|
3
|
+
- form, fieldset, smarthr-ui/Fieldset 以下でSectioningContent(section, aside, article, nav)が利用されている場合、smarthr-ui/Fieldsetに置き換えることを促すルールです
|
|
4
|
+
- このルールを適用することで以下のようなメリットがあります
|
|
5
|
+
- form要素内からHeadingが取り除かれ、Fieldsetに統一されることにより、見出しを表現する要素がlegend, label要素のみになります
|
|
6
|
+
- これによってマークアップのルールが統一され、スクリーンリーダーのジャンプ機能などの利便性が向上します
|
|
7
|
+
- ジャンプ機能ではheading, legendは区別されるため統一されることで操作方法が単純化されます
|
|
8
|
+
- a11y-form-control-in-form と組み合わせることでより厳密なフォームのマークアップを行えます
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## rules
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
{
|
|
15
|
+
rules: {
|
|
16
|
+
'smarthr/a11y-prohibit-sectioning-content-in-form': 'error', // 'warn', 'off'
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ❌ Incorrect
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
// form要素以下にSectionが存在するためNG
|
|
25
|
+
const AnyComponent = <form>
|
|
26
|
+
<Section>
|
|
27
|
+
<Heading>ANY TITLE.</Heading>
|
|
28
|
+
</Section>
|
|
29
|
+
</form>
|
|
30
|
+
|
|
31
|
+
// fieldset要素以下にAsideが存在するためNG
|
|
32
|
+
const AnyComponent = <Fieldset>
|
|
33
|
+
<Aside>
|
|
34
|
+
<Heading>ANY TITLE.</Heading>
|
|
35
|
+
</Aside>
|
|
36
|
+
</Fieldset>
|
|
37
|
+
|
|
38
|
+
// ファイル名、もしくは所属するディレクトリがform, fieldsetなどフォームに関連する名称になっている場合
|
|
39
|
+
// 内部でArticleを使っているとNG
|
|
40
|
+
const AnyComponent = <>
|
|
41
|
+
<Article>
|
|
42
|
+
<Heading>ANY TITLE.</Heading>
|
|
43
|
+
</Article>
|
|
44
|
+
</>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## ✅ Correct
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
// form内でSectioningContentを利用していないのでOK
|
|
51
|
+
const AnyComponent = <form>
|
|
52
|
+
<Fieldset title="ANY TITLE.">
|
|
53
|
+
Hoge.
|
|
54
|
+
<Fieldset title="ANY TITLE.">
|
|
55
|
+
Fuga.
|
|
56
|
+
<FormControl title="ANY TITLE.">
|
|
57
|
+
Piyo.
|
|
58
|
+
</FormControl>
|
|
59
|
+
</Fieldset>
|
|
60
|
+
</Fieldset>
|
|
61
|
+
</form>
|
|
62
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const { rootPath } = require('../../libs/common')
|
|
2
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
3
|
+
|
|
4
|
+
const SECTIONING_CONTENT_EXPECTED_NAMES = {
|
|
5
|
+
'(A|^a)rticle$': '(Article)$',
|
|
6
|
+
'(A|^a)side$': '(Aside)$',
|
|
7
|
+
'(N|^n)av$': '(Nav)$',
|
|
8
|
+
'(S|^s)ection$': '(Section)$',
|
|
9
|
+
}
|
|
10
|
+
const FIELDSET_EXPECTED_NAMES = {
|
|
11
|
+
'(FormControl)$': '(FormControl)$',
|
|
12
|
+
'(FormControls)$': '(FormControls)$',
|
|
13
|
+
'((F|^f)ieldset)$': '(Fieldset)$',
|
|
14
|
+
'(Fieldsets)$': '(Fieldsets)$',
|
|
15
|
+
}
|
|
16
|
+
const FORM_EXPECTED_NAMES = {
|
|
17
|
+
'((F|^f)orm)$': '(Form)$',
|
|
18
|
+
'(FormDialog)$': '(FormDialog)$',
|
|
19
|
+
'RemoteTrigger(.*)FormDialog$': '(RemoteTrigger(.*)FormDialog)$',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const WRAPPER_EXPECTED_NAMES = {
|
|
23
|
+
...FIELDSET_EXPECTED_NAMES,
|
|
24
|
+
...FORM_EXPECTED_NAMES,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EXPECTED_NAMES = {
|
|
28
|
+
...SECTIONING_CONTENT_EXPECTED_NAMES,
|
|
29
|
+
...WRAPPER_EXPECTED_NAMES,
|
|
30
|
+
'SideNav$': '(SideNav)$',
|
|
31
|
+
'IndexNav$': '(IndexNav)$',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const UNEXPECTED_NAMES = EXPECTED_NAMES
|
|
35
|
+
|
|
36
|
+
const asRegex = /^(as|forwardedAs)$/
|
|
37
|
+
const asFormRegex = /^(form|fieldset)$/
|
|
38
|
+
const asSectioningContentRegex = /^(article|aside|nav|section)$/
|
|
39
|
+
|
|
40
|
+
const includeAsAttrFormOrFieldset = (a) => a.name?.name.match(asRegex) && asFormRegex.test(a.value.value)
|
|
41
|
+
const includeAsAttrSectioningContent = (a) => a.name?.name.match(asRegex) && asSectioningContentRegex.test(a.value.value)
|
|
42
|
+
const includeWrapper = (fn) => wrapperRegex.test(fn)
|
|
43
|
+
|
|
44
|
+
const sectioningContentRegex = new RegExp(`(${Object.keys(SECTIONING_CONTENT_EXPECTED_NAMES).join('|')})`)
|
|
45
|
+
const wrapperRegex = new RegExp(`(${Object.keys(WRAPPER_EXPECTED_NAMES).join('|')})`)
|
|
46
|
+
const formControlRegex = new RegExp(`(${Object.keys(FIELDSET_EXPECTED_NAMES).join('|')})`)
|
|
47
|
+
const extRegex = /\.[a-z0-9]+?$/
|
|
48
|
+
const ignoreNavRegex = /(Side|Index)Nav$/
|
|
49
|
+
const formPartCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
|
|
50
|
+
|
|
51
|
+
const rootPathSlashed = `${rootPath}/`
|
|
52
|
+
|
|
53
|
+
const searchBubbleUpForSectioningContent = (node) => {
|
|
54
|
+
switch (node.type) {
|
|
55
|
+
case 'Program':
|
|
56
|
+
// rootまで検索した場合はOK
|
|
57
|
+
return null
|
|
58
|
+
case 'JSXElement':
|
|
59
|
+
const openingElement = node.openingElement
|
|
60
|
+
const elementName = openingElement.name.name
|
|
61
|
+
|
|
62
|
+
if (elementName) {
|
|
63
|
+
// formかFieldsetでラップされていればNG
|
|
64
|
+
if (wrapperRegex.test(elementName) || openingElement.attributes.some(includeAsAttrFormOrFieldset)) {
|
|
65
|
+
return node
|
|
66
|
+
} else if ((sectioningContentRegex.test(elementName) && !ignoreNavRegex.test(elementName)) || openingElement.attributes.some(includeAsAttrSectioningContent)) {
|
|
67
|
+
// 他のSectioningContentに到達した場合、同じチェックを繰り返すことになるため終了する
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
break
|
|
73
|
+
// Form系コンポーネントの拡張なのでNG
|
|
74
|
+
case 'VariableDeclarator':
|
|
75
|
+
if (node.parent.parent?.type.match(formPartCheckParentTypeRegex) && wrapperRegex.test(node.id.name)) {
|
|
76
|
+
return node
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
break
|
|
80
|
+
// Form系コンポーネントの拡張なのでNG
|
|
81
|
+
case 'FunctionDeclaration':
|
|
82
|
+
if (formPartCheckParentTypeRegex.test(node.parent.type) && wrapperRegex.test(node.id.name)) {
|
|
83
|
+
return node
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return searchBubbleUpForSectioningContent(node.parent)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const searchBubbleUpForFormControl = (node) => {
|
|
93
|
+
switch (node.type) {
|
|
94
|
+
case 'Program':
|
|
95
|
+
// rootまで検索した場合はOK
|
|
96
|
+
return null
|
|
97
|
+
case 'JSXElement':
|
|
98
|
+
const openingElement = node.openingElement
|
|
99
|
+
const elementName = openingElement.name.name
|
|
100
|
+
|
|
101
|
+
if (elementName) {
|
|
102
|
+
// SectioningContentでラップされていればNG
|
|
103
|
+
if (sectioningContentRegex.test(elementName) && !ignoreNavRegex.test(elementName)) {
|
|
104
|
+
return { node: openingElement, elementName }
|
|
105
|
+
} else {
|
|
106
|
+
const attr = openingElement.attributes.find(includeAsAttrSectioningContent)
|
|
107
|
+
|
|
108
|
+
if (attr) {
|
|
109
|
+
return { node: openingElement, elementName: `${attr.name.name}="${attr.value.value}"` }
|
|
110
|
+
} else if (wrapperRegex.test(elementName) || openingElement.attributes.some(includeAsAttrFormOrFieldset)) {
|
|
111
|
+
// 他のFormControl か Fieldsetに到達した場合、同じチェックを繰り返すことになるため終了する
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
break
|
|
118
|
+
case 'VariableDeclarator':
|
|
119
|
+
if (node.parent.parent?.type.match(formPartCheckParentTypeRegex) && wrapperRegex.test(node.id.name)) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
break
|
|
124
|
+
case 'FunctionDeclaration':
|
|
125
|
+
if (formPartCheckParentTypeRegex.test(node.parent.type) && wrapperRegex.test(node.id.name)) {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return searchBubbleUpForFormControl(node.parent)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
137
|
+
*/
|
|
138
|
+
module.exports = {
|
|
139
|
+
meta: {
|
|
140
|
+
type: 'problem',
|
|
141
|
+
schema: [],
|
|
142
|
+
},
|
|
143
|
+
create(context) {
|
|
144
|
+
const filenames = context.getFilename().replace(rootPathSlashed, '').replace(extRegex, '').split('/')
|
|
145
|
+
const isInnerForm = filenames.some(includeWrapper)
|
|
146
|
+
const notified = []
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
150
|
+
JSXOpeningElement: (node) => {
|
|
151
|
+
const elementName = node.name.name
|
|
152
|
+
|
|
153
|
+
if (elementName) {
|
|
154
|
+
// HINT: smarthr-ui/SideNav,IndexNav は対象外とする
|
|
155
|
+
if (ignoreNavRegex.test(elementName)) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isSection = sectioningContentRegex.test(elementName)
|
|
160
|
+
const asAttr = isSection ? false : node.attributes.find(includeAsAttrSectioningContent)
|
|
161
|
+
|
|
162
|
+
if ((isSection || asAttr)) {
|
|
163
|
+
if (isInnerForm || searchBubbleUpForSectioningContent(node.parent.parent)) {
|
|
164
|
+
if (!notified.includes(node)) {
|
|
165
|
+
notified.push(node)
|
|
166
|
+
context.report({
|
|
167
|
+
node,
|
|
168
|
+
message: `${isSection ? elementName : `${asAttr.name.name}="${asAttr.value.value}"`}とその内部に存在するHeadingをsmarthr-ui/Fieldsetに置き換えてください
|
|
169
|
+
- もしくはform要素を利用していない場合、フォームを構成する入力要素郡すべてを一つのform要素で囲んでください
|
|
170
|
+
- required属性、pattern属性など一部属性はform要素で囲まないと動作しません
|
|
171
|
+
- 送信用ボタンのonClickをform要素のonSubmitに移動し、送信用ボタンに `type="submit"` を指定することでより適切にマークアップ出来ます
|
|
172
|
+
- その際、onSubmitの動作中で "e.preventDefault()" と "e.stopPropagation()" を指定する必要がある場合があります。
|
|
173
|
+
- form内の見出しとなる要素をlegend, labelのみに統一することでスクリーンリーダーのジャンプ機能などの利便性が向上します
|
|
174
|
+
- smarthr-ui/Fieldset が利用できない場合、fieldset要素とlegend要素を使ったマークアップに修正してください
|
|
175
|
+
- その際、fieldset要素の直下にlegend要素が存在するようにしてください。他要素がfieldsetとlegendの間に存在すると、正しく紐づけが行われない場合があります`,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else if (formControlRegex.test(elementName)) {
|
|
180
|
+
const sectioningContent = searchBubbleUpForFormControl(node.parent.parent)
|
|
181
|
+
|
|
182
|
+
if (sectioningContent) {
|
|
183
|
+
if (!notified.includes(sectioningContent.node)) {
|
|
184
|
+
notified.push(sectioningContent.node)
|
|
185
|
+
context.report({
|
|
186
|
+
node: sectioningContent.node,
|
|
187
|
+
message: `${sectioningContent.elementName}とその内部に存在するHeadingをsmarthr-ui/Fieldsetに置き換えてください
|
|
188
|
+
- もしくはform要素を利用していない場合、フォームを構成する入力要素郡すべてを一つのform要素で囲んでください
|
|
189
|
+
- required属性、pattern属性など一部属性はform要素で囲まないと動作しません
|
|
190
|
+
- 送信用ボタンのonClickをform要素のonSubmitに移動し、送信用ボタンに `type="submit"` を指定することでより適切にマークアップ出来ます
|
|
191
|
+
- その際、onSubmitの動作中で "e.preventDefault()" と "e.stopPropagation()" を指定する必要がある場合があります。
|
|
192
|
+
- form内の見出しとなる要素をlegend, labelのみに統一することでスクリーンリーダーのジャンプ機能などの利便性が向上します
|
|
193
|
+
- smarthr-ui/Fieldset が利用できない場合、fieldset要素とlegend要素を使ったマークアップに修正してください
|
|
194
|
+
- その際、fieldset要素の直下にlegend要素が存在するようにしてください。他要素がfieldsetとlegendの間に存在すると、正しく紐づけが行われない場合があります`,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
module.exports.schema = []
|
|
@@ -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
|
+
```
|
|
@@ -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 = []
|
|
@@ -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
|
+
})
|