eslint-plugin-smarthr 6.2.0 → 6.2.2
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 +14 -0
- package/package.json +2 -2
- package/rules/a11y-anchor-has-href-attribute/index.js +4 -1
- package/rules/a11y-heading-in-sectioning-content/README.md +127 -31
- package/rules/best-practice-for-interactive-element/index.js +8 -4
- package/test/best-practice-for-interactive-element.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
## [6.2.2](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.1...eslint-plugin-smarthr-v6.2.2) (2026-02-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **a11y-anchor-has-href-attribute:** href属性に空文字・#が指定されていることをチェックするselectorの記述がinvalidになる場合があるため変更 ([#1049](https://github.com/kufu/tamatebako/issues/1049)) ([fa210cb](https://github.com/kufu/tamatebako/commit/fa210cb4d691bfd402ed86210a7552fe3b001c3a))
|
|
11
|
+
|
|
12
|
+
## [6.2.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.0...eslint-plugin-smarthr-v6.2.1) (2026-02-02)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **best-practice-for-interactive-element:** button要素に対してrole="menuitem"の設定を許容する ([#1038](https://github.com/kufu/tamatebako/issues/1038)) ([022b22f](https://github.com/kufu/tamatebako/commit/022b22f5432cd5cc7afd54525dd66c3e0e5d3d49))
|
|
18
|
+
|
|
5
19
|
## [6.2.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.1.0...eslint-plugin-smarthr-v6.2.0) (2026-01-29)
|
|
6
20
|
|
|
7
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.2",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "71497e58907b3eb5e52681f3e6473b3e30902ebf"
|
|
41
41
|
}
|
|
@@ -45,6 +45,9 @@ const OPTION = (() => {
|
|
|
45
45
|
|
|
46
46
|
const ANCHOR_ELEMENT = 'JSXOpeningElement[name.name=/(Anchor|Link|^a)$/]'
|
|
47
47
|
const HREF_ATTRIBUTE = `JSXAttribute[name.name=${OPTION.react_router ? '/^(href|to)$/' : '"href"'}]`
|
|
48
|
+
const NULL_HREF_ATTRIBUTE_VALUES = `${HREF_ATTRIBUTE}:matches(${['#', ''].reduce((prev, v) => {
|
|
49
|
+
return `${prev},:has(>Literal[value="${v}"]),:has(>JSXExpressionContainer[expression.value="${v}"])`
|
|
50
|
+
}, '[value=null]')})`
|
|
48
51
|
const NEXT_LINK_REGEX = /Link$/
|
|
49
52
|
// HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
|
|
50
53
|
const nextCheck = (node) => ((node.parent.parent.openingElement.name.name || '').test(NEXT_LINK_REGEX))
|
|
@@ -92,7 +95,7 @@ module.exports = {
|
|
|
92
95
|
reporter(node)
|
|
93
96
|
}
|
|
94
97
|
},
|
|
95
|
-
[`${ANCHOR_ELEMENT}:has(${
|
|
98
|
+
[`${ANCHOR_ELEMENT}:has(${NULL_HREF_ATTRIBUTE_VALUES})`]: reporter,
|
|
96
99
|
}
|
|
97
100
|
},
|
|
98
101
|
}
|
|
@@ -1,10 +1,96 @@
|
|
|
1
1
|
# smarthr/a11y-heading-in-sectioning-content
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
HeadingコンポーネントをSectioningContent(Article, Aside, Nav, Section) のいずれかで囲むことを促すルールです。<br />
|
|
4
|
+
同時にSectioningContentはHeadingを内包しているか、もチェックします
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## なぜHeadingとSectioningContentはセットで記述する必要があるのか?
|
|
8
|
+
|
|
9
|
+
詳細は[SmartHR Tech Blog](https://tech.smarthr.jp/entry/2025/06/19/094801)を参照してください。<br />
|
|
10
|
+
メリットのみを記述すると以下のとおりです。
|
|
11
|
+
|
|
12
|
+
- article, aside, nav, section で Heading とHeadingの対象となる範囲を囲むとブラウザが正確に解釈できるようになるメリットがあります
|
|
13
|
+
- smarthr-ui/SectioningContentで smarthr-ui/Headingを囲むことで、Headingのレベル(h1~h6)を自動的に計算するメリットもあります
|
|
14
|
+
|
|
15
|
+
## SectioningContentとして扱うコンポーネントについて
|
|
16
|
+
|
|
17
|
+
このルールではsmarthr-ui/Layout系コンポーネント(Center, Reel, Sidebar, Stack)にas属性・forwardedAs属性で`section` `article` `aside` `nav` のいずれかの要素が指定されている場合、SectioningContentとして扱います。<br />
|
|
18
|
+
Layout系コンポーネントがSectioningContentとして扱われている場合、smarthr-uiの内部実装レベルでもSectioningContentとして扱われるため、前述のHeadingのレベルの自動計算が有効になります。
|
|
19
|
+
|
|
20
|
+
## section要素などbuildinのSectiongContentに属する要素の利用について
|
|
21
|
+
|
|
22
|
+
前述のHeadingレベルの自動計算はsmarthr-ui/SectiongContentとsmarthr-ui/Layoutでas・forwardedAs属性を指定した場合のみ有効になる機能です。<br />
|
|
23
|
+
buildinの `article` `aside` `nav` `section` 要素はheadingの対象となる範囲は正しく表せますが、Headingレベルの自動計算は行えません。
|
|
24
|
+
|
|
25
|
+
そのため、**`article` `aside` `nav` `section` 要素はsmarthr-uiの `Article` `Aside` `Nav` `Section` コンポーネントに置き換えてください。**
|
|
26
|
+
|
|
27
|
+
## PageHeadingのチェックについて
|
|
28
|
+
|
|
29
|
+
PageHeadingはh1要素を表現するコンポーネントです。<br />
|
|
30
|
+
h1要素は基本的に記述されてるhtml全体に対する見出しとなります。<br />
|
|
31
|
+
そのため**SectioningContentでh1要素を囲むと見出しの範囲外として解釈された範囲はどの見出しにも属さないことになる**ため、エラーとしています。
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
// h1をSectioningContentで囲むと...
|
|
35
|
+
...
|
|
36
|
+
<body>
|
|
37
|
+
<Section>
|
|
38
|
+
<PageHeading />
|
|
39
|
+
{anyContent}
|
|
40
|
+
</Section>
|
|
41
|
+
{/* ↓このコンポーネントはどの見出しにも属さないことになる */}
|
|
42
|
+
<OtherContent />
|
|
43
|
+
</body>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
上記例の場合、OtherContentがh1の範囲外になり、htmlのアウトラインが乱れてしまいます。<br />
|
|
47
|
+
PageHeadingをSectioningContentで囲まない場合、html全体の見出しとなるため、アウトラインは乱れません。
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
// h1をSectioningContentで囲まなければhtml全体のアウトラインが整う
|
|
51
|
+
...
|
|
52
|
+
<body>
|
|
53
|
+
<PageHeading />
|
|
54
|
+
{anyContent}
|
|
55
|
+
{/* ↓このコンポーネントはどの見出しにも属さないことになる */}
|
|
56
|
+
<OtherContent />
|
|
57
|
+
</body>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
また前述の通り、PageHeadingは記述されたhtml全体の見出しのため、基本的に1htmlにつき1つのみ記述出来ます。<br />
|
|
61
|
+
そのためこのルールでは**おなじコンポーネント内で複数のPageHeadingが存在する場合エラー**になります。
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
<>
|
|
65
|
+
<HogePageHeading />
|
|
66
|
+
...
|
|
67
|
+
<FugaPageHeading />
|
|
68
|
+
...
|
|
69
|
+
</>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
このチェックは条件分岐によって結果としては一つしかPageHeadingが出力されない場合でもエラーになるため注意が必要です。
|
|
73
|
+
|
|
74
|
+
```jsx
|
|
75
|
+
<>
|
|
76
|
+
{hoge ? (
|
|
77
|
+
<PageHeading>{hoge}</PageHeading>
|
|
78
|
+
) : (
|
|
79
|
+
<PageHeading>{fuga}</PageHeading>
|
|
80
|
+
)}
|
|
81
|
+
</>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
下記の様にPageHeadingは単一の記述になるようにまとめることを推奨します。
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
<>
|
|
88
|
+
<PageHeading>{hoge || fuga}</PageHeading>
|
|
89
|
+
</>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
8
94
|
|
|
9
95
|
## rules
|
|
10
96
|
|
|
@@ -19,73 +105,83 @@
|
|
|
19
105
|
## ❌ Incorrect
|
|
20
106
|
|
|
21
107
|
```jsx
|
|
108
|
+
// Headingがsmarthr-ui/SectioningContent(Article, Aside, Nav, Section)のいずれかで囲まれていないためNG
|
|
22
109
|
<div>
|
|
23
110
|
<Heading>
|
|
24
111
|
hoge
|
|
25
112
|
</Heading>
|
|
26
|
-
<Heading>
|
|
27
|
-
fuga
|
|
28
|
-
</Heading>
|
|
29
113
|
</div>
|
|
30
114
|
```
|
|
115
|
+
|
|
31
116
|
```jsx
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
117
|
+
// Headingに当たる要素がないためNG
|
|
118
|
+
<Aside>
|
|
119
|
+
<AnyContent />
|
|
120
|
+
</Aside>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
// buildinのSectioningContentではなくsmarthr-ui/SectioningContentで囲まなければ
|
|
125
|
+
// Headingレベルの自動計算が有効にならないためNG
|
|
37
126
|
<section>
|
|
38
127
|
<Heading>
|
|
39
|
-
|
|
128
|
+
hoge
|
|
40
129
|
</Heading>
|
|
41
130
|
</section>
|
|
42
131
|
```
|
|
43
132
|
|
|
44
133
|
```jsx
|
|
134
|
+
// PageHeadingはSectiongContentでラップするとoutlineが乱れる可能性があるためNG
|
|
45
135
|
<Section>
|
|
46
|
-
<PageHeading>
|
|
136
|
+
<PageHeading>
|
|
47
137
|
hoge
|
|
48
138
|
</PageHeading>
|
|
49
139
|
</Section>
|
|
50
140
|
```
|
|
51
141
|
|
|
52
142
|
```jsx
|
|
143
|
+
// 同じファイル内に複数のPageHeadingが存在するとNG
|
|
53
144
|
<>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
145
|
+
{hoge ? (
|
|
146
|
+
<PageHeading>
|
|
147
|
+
hoge
|
|
148
|
+
</PageHeading>
|
|
149
|
+
) : (
|
|
150
|
+
<PageHeading>
|
|
151
|
+
fuga
|
|
152
|
+
</PageHeading>
|
|
153
|
+
)}
|
|
60
154
|
</>
|
|
61
155
|
```
|
|
62
156
|
|
|
63
|
-
```jsx
|
|
64
|
-
<Aside> // Headingに当たる要素がないためNG
|
|
65
|
-
<AnyContent />
|
|
66
|
-
</Aside>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
157
|
## ✅ Correct
|
|
70
158
|
|
|
71
159
|
```jsx
|
|
160
|
+
// SectioningContentにはHeadingを含む必要がある
|
|
72
161
|
<Section>
|
|
73
|
-
<Heading>hoge</Heading>
|
|
162
|
+
<Heading>hoge</Heading>
|
|
74
163
|
<Section>
|
|
75
164
|
<Heading>fuga</Heading>
|
|
76
165
|
</Section>
|
|
77
166
|
</Section>
|
|
167
|
+
```
|
|
78
168
|
|
|
169
|
+
```jsx
|
|
170
|
+
// PageHeadingはSectioningContentで囲まない
|
|
79
171
|
<>
|
|
80
172
|
<PageHeading>Page Name.</PageHeading>
|
|
81
173
|
<Section>
|
|
82
174
|
<Heading>hoge</Heading>
|
|
83
175
|
</Section>
|
|
84
|
-
<StyledSection>
|
|
85
|
-
<Heading>fuga</Heading>
|
|
86
|
-
</StyledSection>
|
|
87
176
|
<Center as="aside">
|
|
88
177
|
<Heading>piyo</Heading>
|
|
89
178
|
</Center>
|
|
90
179
|
</>
|
|
91
180
|
```
|
|
181
|
+
|
|
182
|
+
```jsx
|
|
183
|
+
// PageHeadingはコンポーネント内で単一にする
|
|
184
|
+
<>
|
|
185
|
+
<PageHeading>{hoge || fuga}</PageHeading>
|
|
186
|
+
</>
|
|
187
|
+
```
|
|
@@ -33,11 +33,15 @@ const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Dow
|
|
|
33
33
|
const DELEGATE_REGEX = /(d|D)elegate/
|
|
34
34
|
|
|
35
35
|
const ARROW_ROLES = {
|
|
36
|
-
'(
|
|
37
|
-
'(^i|I)nput$': 'combobox',
|
|
38
|
-
'(^b|B)utton$': 'option',
|
|
36
|
+
'Check(b|B)ox$': ['switch'],
|
|
37
|
+
'(^i|I)nput$': ['switch', 'combobox'],
|
|
38
|
+
'(^b|B)utton$': ['option', 'menuitem'],
|
|
39
39
|
}
|
|
40
|
-
const NOT_ARROW_ROLE_ATTRIBUTES = Object.entries(ARROW_ROLES).reduce((prev, [key,
|
|
40
|
+
const NOT_ARROW_ROLE_ATTRIBUTES = Object.entries(ARROW_ROLES).reduce((prev, [key, vs]) => (
|
|
41
|
+
vs.reduce((p, v) => `${p}:not([parent.name.name=/${key}/][value.value="${v}"])`, prev)
|
|
42
|
+
),
|
|
43
|
+
''
|
|
44
|
+
)
|
|
41
45
|
|
|
42
46
|
const ELEMENT_HAS_ROLE_ATTRIBUTE = 'JSXOpeningElement:has(JSXAttribute[name.name="role"])'
|
|
43
47
|
const AS_FORM_PART_ATTRIBUTE = 'JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^f(orm|ieldset)$/]'
|
|
@@ -72,6 +72,7 @@ ruleTester.run('best-practice-for-interactive-element', rule, {
|
|
|
72
72
|
{ code: `<HogeInput role="switch" />` },
|
|
73
73
|
{ code: `<input role="combobox" />` },
|
|
74
74
|
{ code: `<FugaButton role="option" />` },
|
|
75
|
+
{ code: `<FugaButton role="menuitem" />` },
|
|
75
76
|
],
|
|
76
77
|
invalid: [
|
|
77
78
|
{ code: `<button role="presentation">...</button>`, errors: [{ message: interactiveError('button') }] },
|