eslint-plugin-smarthr 0.5.2 → 0.5.4
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/CHANGELOG.md +21 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/rules/a11y-heading-in-sectioning-content/index.js +132 -13
- package/rules/a11y-input-in-form-control/index.js +1 -0
- package/rules/a11y-numbered-text-within-ol/index.js +2 -2
- package/rules/best-practice-for-layouts/README.md +53 -0
- package/rules/best-practice-for-layouts/index.js +100 -0
- package/test/a11y-heading-in-sectioning-content.js +10 -0
- package/test/best-practice-for-layouts.js +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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.4](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.3...v0.5.4) (2024-03-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* a11y-heading-in-sectioning-contentで title, もしくはheading属性がSectioningContent系コンポーネントに設定されている場合、headingがあると判定するように修正 ([#129](https://github.com/kufu/eslint-plugin-smarthr/issues/129)) ([93bbd19](https://github.com/kufu/eslint-plugin-smarthr/commit/93bbd1907cd7f28510dde1ddc93a16d0c92a2c9b))
|
|
11
|
+
|
|
12
|
+
### [0.5.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.2...v0.5.3) (2024-03-26)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* a11y-heading-in-sectioning-contentでHeadingを持たないSectioning Contentを検知するように修正 ([#124](https://github.com/kufu/eslint-plugin-smarthr/issues/124)) ([8b96de0](https://github.com/kufu/eslint-plugin-smarthr/commit/8b96de0c9a474b0c8d72a4b9c3b3b351d2cfb4e5))
|
|
18
|
+
* smarthr-ui/Layouts系コンポーネントの利用方法をチェックする best-practice-for-layouts ルールを追加する ([#126](https://github.com/kufu/eslint-plugin-smarthr/issues/126)) ([e0324e4](https://github.com/kufu/eslint-plugin-smarthr/commit/e0324e4ffab0413e61811cf7cf7f129c0602e0f0))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* a11y-input-in-form-control で Clusterを拡張する際のチェックを追加 ([#125](https://github.com/kufu/eslint-plugin-smarthr/issues/125)) ([f723e24](https://github.com/kufu/eslint-plugin-smarthr/commit/f723e24db86c1095178bb1f28636147e7b619bf2))
|
|
24
|
+
* a11y-numbered-text-within-olのチェックで、属性の値でstyleに数値を指定しているような値の場合、無視する ([#123](https://github.com/kufu/eslint-plugin-smarthr/issues/123)) ([77a6278](https://github.com/kufu/eslint-plugin-smarthr/commit/77a6278ff8ec58c99843746442e1f3c0e54574c5))
|
|
25
|
+
|
|
5
26
|
### [0.5.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.1...v0.5.2) (2024-03-17)
|
|
6
27
|
|
|
7
28
|
### [0.5.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.0...v0.5.1) (2024-03-17)
|
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
- [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
|
|
13
13
|
- [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
|
|
14
14
|
- [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
|
|
15
|
+
- [best-practice-for-button-element](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-button-element)
|
|
15
16
|
- [best-practice-for-date](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-date)
|
|
17
|
+
- [best-practice-for-layouts](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-layouts)
|
|
16
18
|
- [best-practice-for-remote-trigger-dialog](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-remote-trigger-dialog)
|
|
17
19
|
- [format-import-path](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/format-import-path)
|
|
18
20
|
- [format-translate-component](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/format-translate-component)
|
package/package.json
CHANGED
|
@@ -52,11 +52,13 @@ const bareTagRegex = /^(article|aside|nav|section)$/
|
|
|
52
52
|
const modelessDialogRegex = /ModelessDialog$/
|
|
53
53
|
const layoutComponentRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
|
|
54
54
|
const asRegex = /^(as|forwardedAs)$/
|
|
55
|
+
const ignoreCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
|
|
56
|
+
const noHeadingTagNamesRegex = /^(span|legend)$/
|
|
57
|
+
const ignoreHeadingCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
|
|
58
|
+
const headingAttributeRegex = /^(heading|title)$/
|
|
55
59
|
|
|
56
|
-
const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && a.value.value
|
|
57
|
-
|
|
58
|
-
const noHeadingTagNames = ['span', 'legend']
|
|
59
|
-
const ignoreHeadingCheckParentType = ['Program', 'ExportNamedDeclaration']
|
|
60
|
+
const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && bareTagRegex.test(a.value.value)
|
|
61
|
+
const findHeadingAttribute = (a) => headingAttributeRegex.test(a.name?.name || '')
|
|
60
62
|
|
|
61
63
|
const headingMessage = `smarthr-ui/Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。
|
|
62
64
|
- smarthr-uiのArticle, Aside, Nav, SectionのいずれかでHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。
|
|
@@ -102,8 +104,8 @@ const searchBubbleUp = (node) => {
|
|
|
102
104
|
if (
|
|
103
105
|
node.type === 'Program' ||
|
|
104
106
|
node.type === 'JSXElement' && node.openingElement.name.name && (
|
|
105
|
-
node.openingElement.name.name
|
|
106
|
-
node.openingElement.name.name
|
|
107
|
+
sectioningRegex.test(node.openingElement.name.name) ||
|
|
108
|
+
layoutComponentRegex.test(node.openingElement.name.name) && node.openingElement.attributes.some(includeSectioningAsAttr)
|
|
107
109
|
)
|
|
108
110
|
) {
|
|
109
111
|
return node
|
|
@@ -111,16 +113,113 @@ const searchBubbleUp = (node) => {
|
|
|
111
113
|
|
|
112
114
|
if (
|
|
113
115
|
// Headingコンポーネントの拡張なので対象外
|
|
114
|
-
node.type === 'VariableDeclarator' &&
|
|
115
|
-
node.type === 'FunctionDeclaration' &&
|
|
116
|
+
node.type === 'VariableDeclarator' && node.parent.parent?.type.match(ignoreHeadingCheckParentTypeRegex) && declaratorHeadingRegex.test(node.id.name) ||
|
|
117
|
+
node.type === 'FunctionDeclaration' && ignoreHeadingCheckParentTypeRegex.test(node.parent.type) && declaratorHeadingRegex.test(node.id.name) ||
|
|
116
118
|
// ModelessDialogのheaderにHeadingを設定している場合も対象外
|
|
117
|
-
node.type === 'JSXAttribute' && node.name.name === 'header' && node.parent.name.name
|
|
119
|
+
node.type === 'JSXAttribute' && node.name.name === 'header' && modelessDialogRegex.test(node.parent.name.name)
|
|
118
120
|
) {
|
|
119
121
|
return null
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
return searchBubbleUp(node.parent)
|
|
123
125
|
}
|
|
126
|
+
const searchBubbleUpSections = (node) => {
|
|
127
|
+
switch (node.type) {
|
|
128
|
+
case 'Program':
|
|
129
|
+
// rootまで検索した場合は確定でエラーにする
|
|
130
|
+
return null
|
|
131
|
+
case 'VariableDeclarator':
|
|
132
|
+
// SectioningContent系コンポーネントの拡張の場合は対象外
|
|
133
|
+
if (ignoreCheckParentTypeRegex.test(node.parent.parent?.type) && sectioningRegex.test(node.id.name)) {
|
|
134
|
+
return node
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
break
|
|
138
|
+
case 'FunctionDeclaration':
|
|
139
|
+
case 'ClassDeclaration':
|
|
140
|
+
// SectioningContent系コンポーネントの拡張の場合は対象外
|
|
141
|
+
if (ignoreCheckParentTypeRegex.test(node.parent.type) && sectioningRegex.test(node.id.name)) {
|
|
142
|
+
return node
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return searchBubbleUpSections(node.parent)
|
|
149
|
+
}
|
|
150
|
+
const searchChildren = (n) => {
|
|
151
|
+
switch (n.type) {
|
|
152
|
+
case 'BinaryExpression':
|
|
153
|
+
case 'Identifier':
|
|
154
|
+
case 'JSXEmptyExpression':
|
|
155
|
+
case 'JSXText':
|
|
156
|
+
case 'Literal':
|
|
157
|
+
case 'VariableDeclaration':
|
|
158
|
+
// これ以上childrenが存在しないため終了
|
|
159
|
+
return false
|
|
160
|
+
case 'JSXAttribute':
|
|
161
|
+
return n.value ? searchChildren(n.value) : false
|
|
162
|
+
case 'LogicalExpression':
|
|
163
|
+
return searchChildren(n.right)
|
|
164
|
+
case 'ArrowFunctionExpression':
|
|
165
|
+
return searchChildren(n.body)
|
|
166
|
+
case 'MemberExpression':
|
|
167
|
+
return searchChildren(n.property)
|
|
168
|
+
case 'ReturnStatement':
|
|
169
|
+
case 'UnaryExpression':
|
|
170
|
+
return searchChildren(n.argument)
|
|
171
|
+
case 'ChainExpression':
|
|
172
|
+
case 'JSXExpressionContainer':
|
|
173
|
+
return searchChildren(n.expression)
|
|
174
|
+
case 'BlockStatement': {
|
|
175
|
+
return forInSearchChildren(n.body)
|
|
176
|
+
}
|
|
177
|
+
case 'ConditionalExpression': {
|
|
178
|
+
return searchChildren(n.consequent) || searchChildren(n.alternate)
|
|
179
|
+
}
|
|
180
|
+
case 'CallExpression': {
|
|
181
|
+
return forInSearchChildren(n.arguments)
|
|
182
|
+
}
|
|
183
|
+
case 'JSXFragment':
|
|
184
|
+
break
|
|
185
|
+
case 'JSXElement': {
|
|
186
|
+
const name = n.openingElement.name.name || ''
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
sectioningRegex.test(name) ||
|
|
190
|
+
layoutComponentRegex.test(name) && n.openingElement.attributes.some(includeSectioningAsAttr)
|
|
191
|
+
) {
|
|
192
|
+
return false
|
|
193
|
+
} else if (
|
|
194
|
+
(
|
|
195
|
+
headingRegex.test(name) &&
|
|
196
|
+
!n.openingElement.attributes.find(findTagAttr)?.value.value.match(noHeadingTagNamesRegex)
|
|
197
|
+
) ||
|
|
198
|
+
forInSearchChildren(n.openingElement.attributes)
|
|
199
|
+
) {
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return n.children ? forInSearchChildren(n.children) : false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const forInSearchChildren = (ary) => {
|
|
211
|
+
let r = false
|
|
212
|
+
|
|
213
|
+
for (const i in ary) {
|
|
214
|
+
r = searchChildren(ary[i])
|
|
215
|
+
|
|
216
|
+
if (r) {
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return r
|
|
222
|
+
}
|
|
124
223
|
|
|
125
224
|
const findTagAttr = (a) => a.name?.name == 'tag'
|
|
126
225
|
|
|
@@ -151,15 +250,15 @@ module.exports = {
|
|
|
151
250
|
message,
|
|
152
251
|
})
|
|
153
252
|
// Headingに明示的にtag属性が設定されており、それらが span or legend の場合はHeading扱いしない
|
|
154
|
-
} else if (
|
|
253
|
+
} else if (headingRegex.test(elementName)) {
|
|
155
254
|
const tagAttr = node.attributes.find(findTagAttr)
|
|
156
255
|
|
|
157
|
-
if (!
|
|
256
|
+
if (!tagAttr?.value.value.match(noHeadingTagNamesRegex)) {
|
|
158
257
|
const result = searchBubbleUp(node.parent)
|
|
159
258
|
let hit = false
|
|
160
259
|
|
|
161
260
|
if (result) {
|
|
162
|
-
if (
|
|
261
|
+
if (pageHeadingRegex.test(elementName)) {
|
|
163
262
|
h1s.push(node)
|
|
164
263
|
|
|
165
264
|
if (h1s.length > 1) {
|
|
@@ -181,7 +280,7 @@ module.exports = {
|
|
|
181
280
|
node,
|
|
182
281
|
message: rootHeadingMessage,
|
|
183
282
|
})
|
|
184
|
-
} else if (sections.
|
|
283
|
+
} else if (sections.includes(result)) {
|
|
185
284
|
hit = true
|
|
186
285
|
context.report({
|
|
187
286
|
node,
|
|
@@ -199,6 +298,26 @@ module.exports = {
|
|
|
199
298
|
})
|
|
200
299
|
}
|
|
201
300
|
}
|
|
301
|
+
} else if (!node.selfClosing) {
|
|
302
|
+
const isSection = sectioningRegex.test(elementName)
|
|
303
|
+
|
|
304
|
+
// HINT: SectioningContent系コンポーネントの拡張の場合、title, heading属性などにHeadingのテキストが仕込まれている場合がある
|
|
305
|
+
// 対象属性を持っている場合はcorrectとして扱う
|
|
306
|
+
if (isSection && node.attributes.some(findHeadingAttribute)) {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const layoutSectionAsAttr = !isSection && layoutComponentRegex.test(elementName) ? node.attributes.find(includeSectioningAsAttr) : null
|
|
311
|
+
|
|
312
|
+
if ((isSection || layoutSectionAsAttr) && !searchBubbleUpSections(node.parent.parent) && !forInSearchChildren(node.parent.children)) {
|
|
313
|
+
context.report({
|
|
314
|
+
node,
|
|
315
|
+
message: `${isSection ? elementName : `<${elementName} ${layoutSectionAsAttr.name.name}="${layoutSectionAsAttr.value.value}">`} はHeading要素を含んでいません。
|
|
316
|
+
- SectioningContentはHeadingを含むようにマークアップする必要があります
|
|
317
|
+
- ${elementName}に設定しているいずれかの属性がHeading,もしくはHeadingのテキストに該当する場合、その属性の名称を ${headingAttributeRegex.toString()} にマッチする名称に変更してください
|
|
318
|
+
- Headingにするべき適切な文字列が存在しない場合、 ${isSection ? `${elementName} は削除するか、SectioningContentではない要素に差し替えてください` : `${layoutSectionAsAttr.name.name}="${layoutSectionAsAttr.value.value}"を削除、もしくは別の要素に変更してください`}`,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
202
321
|
}
|
|
203
322
|
},
|
|
204
323
|
}
|
|
@@ -9,7 +9,7 @@ const UNEXPECTED_NAMES = EXPECTED_NAMES
|
|
|
9
9
|
const NUMBERED_TEXT_REGEX = /^[\s\n]*(([0-9])([^0-9]{2})[^\s\n]*)/
|
|
10
10
|
const ORDERED_LIST_REGEX = /(Ordered(.*)List|^ol)$/
|
|
11
11
|
const SELECT_REGEX = /(S|s)elect$/
|
|
12
|
-
const
|
|
12
|
+
const IGNORE_ATTRIBUTE_VALUE_REGEX = /^[0-9]+(px|em|ex|ch|rem|lh|rlh|vw|vh|vmin|vmax|vb|vi|svw|svh|lvw|lvh|dvw|dvh|%)?$/
|
|
13
13
|
const AS_ATTRIBUTE_REGEX = /^(as|forwardedAs)$/
|
|
14
14
|
|
|
15
15
|
const findAsOlAttr = (a) => a.type === 'JSXAttribute' && AS_ATTRIBUTE_REGEX.test(a.name?.name) && a.value?.value === 'ol'
|
|
@@ -128,7 +128,7 @@ module.exports = {
|
|
|
128
128
|
return {
|
|
129
129
|
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
130
130
|
JSXAttribute: (node) => {
|
|
131
|
-
if (node.value?.value && !
|
|
131
|
+
if (node.value?.value && !IGNORE_ATTRIBUTE_VALUE_REGEX.test(node.value.value)) {
|
|
132
132
|
checker(node, node.value.value.match(NUMBERED_TEXT_REGEX))
|
|
133
133
|
}
|
|
134
134
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# smarthr/best-practice-for-layouts
|
|
2
|
+
|
|
3
|
+
- smarthr-ui/Layoutsに属するコンポーネントの利用方法をチェックするルールです
|
|
4
|
+
|
|
5
|
+
## rules
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
'smarthr/best-practice-for-layouts': 'error', // 'warn', 'off',
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
// 子が複数無いためエラー
|
|
19
|
+
<Cluster>
|
|
20
|
+
<Any />
|
|
21
|
+
</Cluster>
|
|
22
|
+
<StyledStack>
|
|
23
|
+
{flg ? 'a' : 'b'}
|
|
24
|
+
</StyledStack>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ✅ Correct
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
// 子が複数あるのでOK
|
|
31
|
+
<Cluster>
|
|
32
|
+
<Any />
|
|
33
|
+
<Any />
|
|
34
|
+
</Cluster>
|
|
35
|
+
|
|
36
|
+
<StyledStack>
|
|
37
|
+
{flg ? 'a' : (
|
|
38
|
+
<>
|
|
39
|
+
<Any />
|
|
40
|
+
<Any />
|
|
41
|
+
</>
|
|
42
|
+
)}
|
|
43
|
+
</StyledStack>
|
|
44
|
+
|
|
45
|
+
// Cluster、かつ右寄せをしている場合は子一つでもOK
|
|
46
|
+
<Cluster justify="end">
|
|
47
|
+
<Any />
|
|
48
|
+
</Cluster>
|
|
49
|
+
<Cluster justify="flex-end">
|
|
50
|
+
<Any />
|
|
51
|
+
<Any />
|
|
52
|
+
</Cluster>
|
|
53
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const MULTI_CHILDREN_EXPECTED_NAMES = {
|
|
4
|
+
'Cluster$': '(Cluster)$',
|
|
5
|
+
'Stack$': '(Stack)$',
|
|
6
|
+
}
|
|
7
|
+
const EXPECTED_NAMES = {
|
|
8
|
+
...MULTI_CHILDREN_EXPECTED_NAMES,
|
|
9
|
+
'Center$': '(Center)$',
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const UNEXPECTED_NAMES = EXPECTED_NAMES
|
|
14
|
+
|
|
15
|
+
const MULTI_CHILDREN_REGEX = new RegExp(`(${Object.keys(MULTI_CHILDREN_EXPECTED_NAMES).join('|')})`)
|
|
16
|
+
const REGEX_NLSP = /^\s*\n+\s*$/
|
|
17
|
+
const FLEX_END_REGEX = /^(flex-)?end$/
|
|
18
|
+
|
|
19
|
+
const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText)
|
|
20
|
+
const checkFalsyJSXText = (c) => (
|
|
21
|
+
!(
|
|
22
|
+
c.type === 'JSXText' && c.value.match(REGEX_NLSP) ||
|
|
23
|
+
c.type === 'JSXEmptyExpression'
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const findJustifyAttr = (a) => a.name?.name === 'justify'
|
|
28
|
+
|
|
29
|
+
const searchChildren = (node) => {
|
|
30
|
+
if (
|
|
31
|
+
node.type === 'JSXFragment' ||
|
|
32
|
+
node.type === 'JSXElement' && node.openingElement.name?.name === 'SectioningFragment'
|
|
33
|
+
) {
|
|
34
|
+
const children = node.children.filter(checkFalsyJSXText)
|
|
35
|
+
|
|
36
|
+
if (children.length > 1) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
switch(node.type) {
|
|
42
|
+
case 'JSXExpressionContainer':
|
|
43
|
+
return searchChildren(node.expression)
|
|
44
|
+
case 'CallExpression':
|
|
45
|
+
return node.callee.property?.name !== 'map'
|
|
46
|
+
case 'ConditionalExpression':
|
|
47
|
+
return searchChildren(node.consequent) && searchChildren(node.alternate)
|
|
48
|
+
case 'LogicalExpression':
|
|
49
|
+
return searchChildren(node.right)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SCHEMA = []
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
meta: {
|
|
59
|
+
type: 'problem',
|
|
60
|
+
schema: SCHEMA,
|
|
61
|
+
},
|
|
62
|
+
create(context) {
|
|
63
|
+
return {
|
|
64
|
+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
65
|
+
JSXOpeningElement: (node) => {
|
|
66
|
+
const nodeName = node.name.name;
|
|
67
|
+
|
|
68
|
+
if (nodeName && !node.selfClosing) {
|
|
69
|
+
const matcher = nodeName.match(MULTI_CHILDREN_REGEX)
|
|
70
|
+
|
|
71
|
+
if (matcher) {
|
|
72
|
+
const children = node.parent.children.filter(checkFalsyJSXText)
|
|
73
|
+
|
|
74
|
+
if (children.length === 1) {
|
|
75
|
+
const layoutType = matcher[1]
|
|
76
|
+
const justifyAttr = layoutType === 'Cluster' ? node.attributes.find(findJustifyAttr) : null
|
|
77
|
+
|
|
78
|
+
if (justifyAttr && FLEX_END_REGEX.test(justifyAttr.value.value)) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (searchChildren(children[0])) {
|
|
83
|
+
context.report({
|
|
84
|
+
node,
|
|
85
|
+
message:
|
|
86
|
+
justifyAttr && justifyAttr.value.value === 'center'
|
|
87
|
+
? `${nodeName} は smarthr-ui/${layoutType} ではなく smarthr-ui/Center でマークアップしてください`
|
|
88
|
+
: `${nodeName}には子要素が一つしか無いため、${layoutType}でマークアップする意味がありません。
|
|
89
|
+
- styleを確認し、div・spanなど、別要素でマークアップし直すか、${nodeName}を削除してください
|
|
90
|
+
- as, forwardedAsなどでSectioningContent系要素に変更している場合、対応するsmarthr-ui/Section, Aside, Nav, Article のいずれかに差し替えてください`
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
module.exports.schema = SCHEMA
|
|
@@ -22,6 +22,10 @@ const pageMessage = 'smarthr-ui/PageHeading が同一ファイル内に複数存
|
|
|
22
22
|
const pageInSectionMessage = 'smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。'
|
|
23
23
|
const noTagAttrMessage = `tag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Section, SectioningFragmentのいずれかの自動レベル計算に任せるよう、tag属性を削除してください。
|
|
24
24
|
- tag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
|
|
25
|
+
const notHaveHeadingMessage = (elementName) => `${elementName} はHeading要素を含んでいません。
|
|
26
|
+
- SectioningContentはHeadingを含むようにマークアップする必要があります
|
|
27
|
+
- ${elementName}に設定しているいずれかの属性がHeading,もしくはHeadingのテキストに該当する場合、その属性の名称を /^(heading|title)$/ にマッチする名称に変更してください
|
|
28
|
+
- Headingにするべき適切な文字列が存在しない場合、 ${elementName} は削除するか、SectioningContentではない要素に差し替えてください`
|
|
25
29
|
|
|
26
30
|
ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
27
31
|
valid: [
|
|
@@ -52,6 +56,9 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
|
52
56
|
{ code: 'const FugaStack = styled(HogeStack)``' },
|
|
53
57
|
{ code: '<PageHeading>hoge</PageHeading>' },
|
|
54
58
|
{ code: '<Section><Heading>hoge</Heading></Section>' },
|
|
59
|
+
{ code: '<FugaSection heading={<Heading>hoge</Heading>}>abc</FugaSection>' },
|
|
60
|
+
{ code: '<FugaSection heading="hoge">abc</FugaSection>' },
|
|
61
|
+
{ code: '<FugaSection title="hoge">abc</FugaSection>' },
|
|
55
62
|
{ code: '<><Section><Heading>hoge</Heading></Section><Section><Heading>fuga</Heading></Section></>' },
|
|
56
63
|
{ code: 'const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
|
|
57
64
|
{ code: 'export const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
|
|
@@ -137,5 +144,8 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
|
137
144
|
{ code: '<Section><Heading>hoge</Heading><Heading>fuga</Heading></Section>', errors: [ { message: lowerMessage } ] },
|
|
138
145
|
{ code: '<Section><PageHeading>hoge</PageHeading></Section>', errors: [ { message: pageInSectionMessage } ] },
|
|
139
146
|
{ code: '<Section><Heading tag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
|
|
147
|
+
{ code: '<Section></Section>', errors: [ { message: notHaveHeadingMessage('Section') } ] },
|
|
148
|
+
{ code: '<Aside><HogeSection></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') }, { message: notHaveHeadingMessage('HogeSection') } ] },
|
|
149
|
+
{ code: '<Aside any="hoge"><HogeSection><Heading /></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') } ] },
|
|
140
150
|
],
|
|
141
151
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-layouts')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 2018,
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
experimentalObjectRestSpread: true,
|
|
9
|
+
jsx: true,
|
|
10
|
+
},
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const errorMessage = (type, name) => `${name}には子要素が一つしか無いため、${type}でマークアップする意味がありません。
|
|
16
|
+
- styleを確認し、div・spanなど、別要素でマークアップし直すか、${name}を削除してください
|
|
17
|
+
- as, forwardedAsなどでSectioningContent系要素に変更している場合、対応するsmarthr-ui/Section, Aside, Nav, Article のいずれかに差し替えてください`
|
|
18
|
+
|
|
19
|
+
ruleTester.run('best-practice-for-button-element', rule, {
|
|
20
|
+
valid: [
|
|
21
|
+
{ code: `<Center />` },
|
|
22
|
+
{ code: `<Cluster />` },
|
|
23
|
+
{ code: `<Stack />` },
|
|
24
|
+
{ code: `<HogeCenter />` },
|
|
25
|
+
{ code: `<HogeCluster />` },
|
|
26
|
+
{ code: `<HogeStack />` },
|
|
27
|
+
{ code: `<Center><Hoge /></Center>` },
|
|
28
|
+
{ code: `<Center><Hoge /><Hoge /></Center>` },
|
|
29
|
+
{ code: `<Stack><Hoge /><Hoge /></Stack>` },
|
|
30
|
+
{ code: `<Stack>{a}<Hoge /></Stack>` },
|
|
31
|
+
{ code: `<AnyStack>{a.map(action)}</AnyStack>` },
|
|
32
|
+
{ code: `<AnyStack>{a && <><Hoge /><Hoge /></>}</AnyStack>` },
|
|
33
|
+
{ code: `<AnyStack>{a && a.map(action)}</AnyStack>` },
|
|
34
|
+
{ code: `<AnyStack>{a && a.b.map(action)}</AnyStack>` },
|
|
35
|
+
{ code: `<AnyStack>{a || <><Hoge /><Hoge /></>}</AnyStack>` },
|
|
36
|
+
{ code: `<AnyStack>{a || a.map(action)}</AnyStack>` },
|
|
37
|
+
{ code: `<AnyStack>{a || a.b.map(action)}</AnyStack>` },
|
|
38
|
+
{ code: `<AnyStack>{a ? a.b.map(action) : <Hoge />}</AnyStack>` },
|
|
39
|
+
{ code: `<AnyStack>{a ? <Hoge /> : a.b.map(action)}</AnyStack>` },
|
|
40
|
+
{ code: `<AnyStack>{a ? <Hoge /> : a ? <Hoge /> : a.b.map(action)}</AnyStack>` },
|
|
41
|
+
{ code: `<Cluster><Hoge /><Hoge /></Cluster>` },
|
|
42
|
+
{ code: `<Cluster>{a}<Hoge /></Cluster>` },
|
|
43
|
+
{ code: `<AnyCluster>{a.map(action)}</AnyCluster>` },
|
|
44
|
+
{ code: `<AnyCluster>{a && <><Hoge /><Hoge /></>}</AnyCluster>` },
|
|
45
|
+
{ code: `<AnyCluster>{a && a.map(action)}</AnyCluster>` },
|
|
46
|
+
{ code: `<AnyCluster>{a && a.b.map(action)}</AnyCluster>` },
|
|
47
|
+
{ code: `<AnyCluster>{a || <><Hoge /><Hoge /></>}</AnyCluster>` },
|
|
48
|
+
{ code: `<AnyCluster>{a || a.map(action)}</AnyCluster>` },
|
|
49
|
+
{ code: `<AnyCluster>{a || a.b.map(action)}</AnyCluster>` },
|
|
50
|
+
{ code: `<AnyCluster>{a ? a.b.map(action) : <Hoge />}</AnyCluster>` },
|
|
51
|
+
{ code: `<AnyCluster>{a ? <Hoge /> : a.b.map(action)}</AnyCluster>` },
|
|
52
|
+
{ code: `<AnyCluster>{a ? <Hoge /> : a ? <Hoge /> : a.b.map(action)}</AnyCluster>` },
|
|
53
|
+
{ code: `<Cluster justify="flex-end">{a}</Cluster>` },
|
|
54
|
+
{ code: `<HogeCluster justify="end">{a}</HogeCluster>` },
|
|
55
|
+
],
|
|
56
|
+
invalid: [
|
|
57
|
+
{ code: `<Stack><Hoge /></Stack>`, errors: [ { message: errorMessage('Stack', 'Stack') } ] },
|
|
58
|
+
{ code: `<Stack>{a}</Stack>`, errors: [ { message: errorMessage('Stack', 'Stack') } ] },
|
|
59
|
+
{ code: `<AnyStack>{a.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
60
|
+
{ code: `<AnyStack>{a && <><Hoge /></>}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
61
|
+
{ code: `<AnyStack>{a && a.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
62
|
+
{ code: `<AnyStack>{a && a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
63
|
+
{ code: `<AnyStack>{a || <><Hoge /></>}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
64
|
+
{ code: `<AnyStack>{a || a.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
65
|
+
{ code: `<AnyStack>{a || a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
66
|
+
{ code: `<AnyStack>{a ? a.b.hoge(action) : <Hoge />}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
67
|
+
{ code: `<AnyStack>{a ? <Hoge /> : a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
68
|
+
{ code: `<AnyStack>{a ? <Hoge /> : a ? <Hoge /> : a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
|
|
69
|
+
{ code: `<Cluster><Hoge /></Cluster>`, errors: [ { message: errorMessage('Cluster', 'Cluster') } ] },
|
|
70
|
+
{ code: `<Cluster>{a}</Cluster>`, errors: [ { message: errorMessage('Cluster', 'Cluster') } ] },
|
|
71
|
+
{ code: `<AnyCluster>{a.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
72
|
+
{ code: `<AnyCluster>{a && <><Hoge /></>}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
73
|
+
{ code: `<AnyCluster>{a && a.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
74
|
+
{ code: `<AnyCluster>{a && a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
75
|
+
{ code: `<AnyCluster>{a || <><Hoge /></>}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
76
|
+
{ code: `<AnyCluster>{a || a.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
77
|
+
{ code: `<AnyCluster>{a || a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
78
|
+
{ code: `<AnyCluster>{a ? a.b.hoge(action) : <Hoge />}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
79
|
+
{ code: `<AnyCluster>{a ? <Hoge /> : a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
80
|
+
{ code: `<AnyCluster>{a ? <Hoge /> : a ? <Hoge /> : a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
|
|
81
|
+
{ code: `<HogeCluster justify="center">{a}</HogeCluster>`, errors: [ { message: 'HogeCluster は smarthr-ui/Cluster ではなく smarthr-ui/Center でマークアップしてください' } ] },
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
|