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 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.0",
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": "aed30ef2313795136134408db0ab4fb88dd49d02"
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(${HREF_ATTRIBUTE}:matches([value=null],:has(>Literal[value=/^(|#)$/]),:has(>JSXExpressionContainer[expression.value=/^(|#)$/])))`]: reporter,
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
- - Headingコンポーネントをsmarthr-ui/SectioningContent(Article, Aside, Nav, Section) のいずれかで囲むことを促すルールです
4
- - article, aside, nav, section で Heading とHeadingの対象となる範囲を囲むとブラウザが正確に解釈できるようになるメリットがあります
5
- - またsmarthr-ui/SectioningContentで smarthr-ui/Headingを囲むことで、Headingのレベル(h1~h6)を自動的に計算するメリットもあります
6
- - このルールではSectiongContentがHeadingを内包しているかもチェックします
7
- - Headingコンポーネントをsmarthr-ui/Layout(Center, Reel, Sidebar, Stack) のいずれかで囲んでおり、かつas, forwardedAsのいずれかの属性で 'section', 'article', 'aside', 'nav' が指定されている場合、SectioningContentで囲んでいるものとして扱われるようになります
3
+ HeadingコンポーネントをSectioningContent(Article, Aside, Nav, Section) のいずれかで囲むことを促すルールです。<br />
4
+ 同時にSectioningContentはHeadingを内包しているか、もチェックします
5
+
6
+
7
+ ## なぜHeadingSectioningContentはセットで記述する必要があるのか?
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
- <section> // styled-components の sectionをそのまま利用するとNG
33
- <Heading>
34
- hoge
35
- </Heading>
36
- </section>
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
- fuga
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> // PageHeadingはSectiongContentでラップするとoutlineが狂う可能性があるためNG
136
+ <PageHeading>
47
137
  hoge
48
138
  </PageHeading>
49
139
  </Section>
50
140
  ```
51
141
 
52
142
  ```jsx
143
+ // 同じファイル内に複数のPageHeadingが存在するとNG
53
144
  <>
54
- <PageHeading> // 同じファイル内に複数のPageHeadingが存在するとNG
55
- hoge
56
- </PageHeading>
57
- <PageHeading>
58
- fuga
59
- </PageHeading>
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> // SectioningContentには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
- '((^i|I)nput|(^c|C)heck(b|B)ox)$': 'switch',
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, value]) => `${prev}:not([parent.name.name=/${key}/][value.value="${value}"])`, '')
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') }] },