eslint-plugin-smarthr 0.5.1 → 0.5.3
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 +16 -0
- package/index.js +1 -20
- package/package.json +1 -1
- package/rules/a11y-heading-in-sectioning-content/index.js +122 -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 +6 -0
- package/test/best-practice-for-layouts.js +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.2...v0.5.3) (2024-03-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 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))
|
|
11
|
+
* 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))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* 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))
|
|
17
|
+
* 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))
|
|
18
|
+
|
|
19
|
+
### [0.5.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.1...v0.5.2) (2024-03-17)
|
|
20
|
+
|
|
5
21
|
### [0.5.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.0...v0.5.1) (2024-03-17)
|
|
6
22
|
|
|
7
23
|
|
package/index.js
CHANGED
|
@@ -23,23 +23,6 @@ function generateRulesMap() {
|
|
|
23
23
|
return rulesMap;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const DISAPPROVE_RULE_NAMES = [
|
|
27
|
-
'a11y-form-control-in-form', // formを使用することの是非について議論中のため
|
|
28
|
-
|
|
29
|
-
// ルールが動作するために設定が必要なものはrecommendedに含めない
|
|
30
|
-
'format-import-path',
|
|
31
|
-
'format-translate-component',
|
|
32
|
-
'jsx-start-with-spread-attributes',
|
|
33
|
-
'no-import-other-domain',
|
|
34
|
-
'prohibit-file-name',
|
|
35
|
-
'prohibit-import',
|
|
36
|
-
'prohibit-path-within-template-literal',
|
|
37
|
-
'require-declaration',
|
|
38
|
-
'require-export',
|
|
39
|
-
'require-import',
|
|
40
|
-
]
|
|
41
|
-
const DISAPPROVE_RULE_NAMES_REGEX = new RegExp(`^(${DISAPPROVE_RULE_NAMES.join('|')})$`)
|
|
42
|
-
|
|
43
26
|
function generateRecommendedConfig(rules) {
|
|
44
27
|
let config = {
|
|
45
28
|
plugins: ['smarthr'],
|
|
@@ -47,9 +30,7 @@ function generateRecommendedConfig(rules) {
|
|
|
47
30
|
};
|
|
48
31
|
|
|
49
32
|
for (let ruleName of Object.keys(rules)) {
|
|
50
|
-
|
|
51
|
-
config.rules[`smarthr/${ruleName}`] = 'off';
|
|
52
|
-
}
|
|
33
|
+
config.rules[`smarthr/${ruleName}`] = 'off';
|
|
53
34
|
}
|
|
54
35
|
|
|
55
36
|
return config;
|
package/package.json
CHANGED
|
@@ -52,11 +52,11 @@ 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)$/
|
|
55
58
|
|
|
56
|
-
const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && a.value.value
|
|
57
|
-
|
|
58
|
-
const noHeadingTagNames = ['span', 'legend']
|
|
59
|
-
const ignoreHeadingCheckParentType = ['Program', 'ExportNamedDeclaration']
|
|
59
|
+
const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && bareTagRegex.test(a.value.value)
|
|
60
60
|
|
|
61
61
|
const headingMessage = `smarthr-ui/Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。
|
|
62
62
|
- smarthr-uiのArticle, Aside, Nav, SectionのいずれかでHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。
|
|
@@ -102,8 +102,8 @@ const searchBubbleUp = (node) => {
|
|
|
102
102
|
if (
|
|
103
103
|
node.type === 'Program' ||
|
|
104
104
|
node.type === 'JSXElement' && node.openingElement.name.name && (
|
|
105
|
-
node.openingElement.name.name
|
|
106
|
-
node.openingElement.name.name
|
|
105
|
+
sectioningRegex.test(node.openingElement.name.name) ||
|
|
106
|
+
layoutComponentRegex.test(node.openingElement.name.name) && node.openingElement.attributes.some(includeSectioningAsAttr)
|
|
107
107
|
)
|
|
108
108
|
) {
|
|
109
109
|
return node
|
|
@@ -111,16 +111,113 @@ const searchBubbleUp = (node) => {
|
|
|
111
111
|
|
|
112
112
|
if (
|
|
113
113
|
// Headingコンポーネントの拡張なので対象外
|
|
114
|
-
node.type === 'VariableDeclarator' &&
|
|
115
|
-
node.type === 'FunctionDeclaration' &&
|
|
114
|
+
node.type === 'VariableDeclarator' && node.parent.parent?.type.match(ignoreHeadingCheckParentTypeRegex) && declaratorHeadingRegex.test(node.id.name) ||
|
|
115
|
+
node.type === 'FunctionDeclaration' && ignoreHeadingCheckParentTypeRegex.test(node.parent.type) && declaratorHeadingRegex.test(node.id.name) ||
|
|
116
116
|
// ModelessDialogのheaderにHeadingを設定している場合も対象外
|
|
117
|
-
node.type === 'JSXAttribute' && node.name.name === 'header' && node.parent.name.name
|
|
117
|
+
node.type === 'JSXAttribute' && node.name.name === 'header' && modelessDialogRegex.test(node.parent.name.name)
|
|
118
118
|
) {
|
|
119
119
|
return null
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
return searchBubbleUp(node.parent)
|
|
123
123
|
}
|
|
124
|
+
const searchBubbleUpSections = (node) => {
|
|
125
|
+
switch (node.type) {
|
|
126
|
+
case 'Program':
|
|
127
|
+
// rootまで検索した場合は確定でエラーにする
|
|
128
|
+
return null
|
|
129
|
+
case 'VariableDeclarator':
|
|
130
|
+
// SectioningContent系コンポーネントの拡張の場合は対象外
|
|
131
|
+
if (ignoreCheckParentTypeRegex.test(node.parent.parent?.type) && sectioningRegex.test(node.id.name)) {
|
|
132
|
+
return node
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
break
|
|
136
|
+
case 'FunctionDeclaration':
|
|
137
|
+
case 'ClassDeclaration':
|
|
138
|
+
// SectioningContent系コンポーネントの拡張の場合は対象外
|
|
139
|
+
if (ignoreCheckParentTypeRegex.test(node.parent.type) && sectioningRegex.test(node.id.name)) {
|
|
140
|
+
return node
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return searchBubbleUpSections(node.parent)
|
|
147
|
+
}
|
|
148
|
+
const searchChildren = (n) => {
|
|
149
|
+
switch (n.type) {
|
|
150
|
+
case 'BinaryExpression':
|
|
151
|
+
case 'Identifier':
|
|
152
|
+
case 'JSXEmptyExpression':
|
|
153
|
+
case 'JSXText':
|
|
154
|
+
case 'Literal':
|
|
155
|
+
case 'VariableDeclaration':
|
|
156
|
+
// これ以上childrenが存在しないため終了
|
|
157
|
+
return false
|
|
158
|
+
case 'JSXAttribute':
|
|
159
|
+
return n.value ? searchChildren(n.value) : false
|
|
160
|
+
case 'LogicalExpression':
|
|
161
|
+
return searchChildren(n.right)
|
|
162
|
+
case 'ArrowFunctionExpression':
|
|
163
|
+
return searchChildren(n.body)
|
|
164
|
+
case 'MemberExpression':
|
|
165
|
+
return searchChildren(n.property)
|
|
166
|
+
case 'ReturnStatement':
|
|
167
|
+
case 'UnaryExpression':
|
|
168
|
+
return searchChildren(n.argument)
|
|
169
|
+
case 'ChainExpression':
|
|
170
|
+
case 'JSXExpressionContainer':
|
|
171
|
+
return searchChildren(n.expression)
|
|
172
|
+
case 'BlockStatement': {
|
|
173
|
+
return forInSearchChildren(n.body)
|
|
174
|
+
}
|
|
175
|
+
case 'ConditionalExpression': {
|
|
176
|
+
return searchChildren(n.consequent) || searchChildren(n.alternate)
|
|
177
|
+
}
|
|
178
|
+
case 'CallExpression': {
|
|
179
|
+
return forInSearchChildren(n.arguments)
|
|
180
|
+
}
|
|
181
|
+
case 'JSXFragment':
|
|
182
|
+
break
|
|
183
|
+
case 'JSXElement': {
|
|
184
|
+
const name = n.openingElement.name.name || ''
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
sectioningRegex.test(name) ||
|
|
188
|
+
layoutComponentRegex.test(name) && n.openingElement.attributes.some(includeSectioningAsAttr)
|
|
189
|
+
) {
|
|
190
|
+
return false
|
|
191
|
+
} else if (
|
|
192
|
+
(
|
|
193
|
+
headingRegex.test(name) &&
|
|
194
|
+
!n.openingElement.attributes.find(findTagAttr)?.value.value.match(noHeadingTagNamesRegex)
|
|
195
|
+
) ||
|
|
196
|
+
forInSearchChildren(n.openingElement.attributes)
|
|
197
|
+
) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return n.children ? forInSearchChildren(n.children) : false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const forInSearchChildren = (ary) => {
|
|
209
|
+
let r = false
|
|
210
|
+
|
|
211
|
+
for (const i in ary) {
|
|
212
|
+
r = searchChildren(ary[i])
|
|
213
|
+
|
|
214
|
+
if (r) {
|
|
215
|
+
break
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return r
|
|
220
|
+
}
|
|
124
221
|
|
|
125
222
|
const findTagAttr = (a) => a.name?.name == 'tag'
|
|
126
223
|
|
|
@@ -151,15 +248,15 @@ module.exports = {
|
|
|
151
248
|
message,
|
|
152
249
|
})
|
|
153
250
|
// Headingに明示的にtag属性が設定されており、それらが span or legend の場合はHeading扱いしない
|
|
154
|
-
} else if (
|
|
251
|
+
} else if (headingRegex.test(elementName)) {
|
|
155
252
|
const tagAttr = node.attributes.find(findTagAttr)
|
|
156
253
|
|
|
157
|
-
if (!
|
|
254
|
+
if (!tagAttr?.value.value.match(noHeadingTagNamesRegex)) {
|
|
158
255
|
const result = searchBubbleUp(node.parent)
|
|
159
256
|
let hit = false
|
|
160
257
|
|
|
161
258
|
if (result) {
|
|
162
|
-
if (
|
|
259
|
+
if (pageHeadingRegex.test(elementName)) {
|
|
163
260
|
h1s.push(node)
|
|
164
261
|
|
|
165
262
|
if (h1s.length > 1) {
|
|
@@ -181,7 +278,7 @@ module.exports = {
|
|
|
181
278
|
node,
|
|
182
279
|
message: rootHeadingMessage,
|
|
183
280
|
})
|
|
184
|
-
} else if (sections.
|
|
281
|
+
} else if (sections.includes(result)) {
|
|
185
282
|
hit = true
|
|
186
283
|
context.report({
|
|
187
284
|
node,
|
|
@@ -199,6 +296,18 @@ module.exports = {
|
|
|
199
296
|
})
|
|
200
297
|
}
|
|
201
298
|
}
|
|
299
|
+
} else if (!node.selfClosing) {
|
|
300
|
+
const isSection = sectioningRegex.test(elementName)
|
|
301
|
+
const layoutSectionAsAttr = !isSection && layoutComponentRegex.test(elementName) ? node.attributes.find(includeSectioningAsAttr) : null
|
|
302
|
+
|
|
303
|
+
if ((isSection || layoutSectionAsAttr) && !searchBubbleUpSections(node.parent.parent) && !forInSearchChildren(node.parent.children)) {
|
|
304
|
+
context.report({
|
|
305
|
+
node,
|
|
306
|
+
message: `${isSection ? elementName : `<${elementName} ${layoutSectionAsAttr.name.name}="${layoutSectionAsAttr.value.value}">`} はHeading要素を含んでいません。
|
|
307
|
+
- SectioningContentはHeadingを含むようにマークアップする必要があります
|
|
308
|
+
- Headingにするべき適切な文字列が存在しない場合、 ${isSection ? `${elementName} は削除するか、SectioningContentではない要素に差し替えてください` : `${layoutSectionAsAttr.name.name}="${layoutSectionAsAttr.value.value}"を削除、もしくは別の要素に変更してください`}`,
|
|
309
|
+
})
|
|
310
|
+
}
|
|
202
311
|
}
|
|
203
312
|
},
|
|
204
313
|
}
|
|
@@ -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,9 @@ 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
|
+
- Headingにするべき適切な文字列が存在しない場合、 ${elementName} は削除するか、SectioningContentではない要素に差し替えてください`
|
|
25
28
|
|
|
26
29
|
ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
27
30
|
valid: [
|
|
@@ -137,5 +140,8 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
|
137
140
|
{ code: '<Section><Heading>hoge</Heading><Heading>fuga</Heading></Section>', errors: [ { message: lowerMessage } ] },
|
|
138
141
|
{ code: '<Section><PageHeading>hoge</PageHeading></Section>', errors: [ { message: pageInSectionMessage } ] },
|
|
139
142
|
{ code: '<Section><Heading tag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
|
|
143
|
+
{ code: '<Section></Section>', errors: [ { message: notHaveHeadingMessage('Section') } ] },
|
|
144
|
+
{ code: '<Aside><HogeSection></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') }, { message: notHaveHeadingMessage('HogeSection') } ] },
|
|
145
|
+
{ code: '<Aside><HogeSection><Heading /></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') } ] },
|
|
140
146
|
],
|
|
141
147
|
});
|
|
@@ -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
|
+
|