eslint-plugin-smarthr 6.0.2 → 6.2.0
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.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.1.0...eslint-plugin-smarthr-v6.2.0) (2026-01-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **best-practice-for-interactive-element:** 特定の要素・コンポーネントに対してrole属性の設定を共用する条件を追加 ([#1031](https://github.com/kufu/tamatebako/issues/1031)) ([1bf4c67](https://github.com/kufu/tamatebako/commit/1bf4c679e767edf169de53c89d7ac69c4873b1e1))
|
|
11
|
+
|
|
12
|
+
## [6.1.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.0.2...eslint-plugin-smarthr-v6.1.0) (2026-01-29)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **best-practice-for-interactive-element:** delegateでイベントを受け取ることを明確にするようコード修正を促すように修正 ([#1027](https://github.com/kufu/tamatebako/issues/1027)) ([476c858](https://github.com/kufu/tamatebako/commit/476c858d87a581a33e764ac1aa9aafee29805217))
|
|
18
|
+
|
|
5
19
|
## [6.0.2](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.0.1...eslint-plugin-smarthr-v6.0.2) (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.0
|
|
3
|
+
"version": "6.2.0",
|
|
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": "aed30ef2313795136134408db0ab4fb88dd49d02"
|
|
41
41
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
- インタラクティブな要素にrole属性が設定されている場合エラーとします
|
|
7
7
|
- インタラクティブではないコンポーネントに対して、デフォルトで用意されているonXxx形式の属性を設定しようとするとエラーにします
|
|
8
8
|
- 例: `CrewDetail` コンポーネントに `onChange` を設定するとエラー、 `onChangeName` ならOK
|
|
9
|
+
- 子要素で発生したイベントを親要素で処理すること(delegate)が目的の場合、イベントハンドラがdelegateを取り扱っていることがわかるように記述することを促します
|
|
9
10
|
|
|
10
11
|
## インタラクティブな要素・コンポーネントとは何か
|
|
11
12
|
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
- Date
|
|
40
41
|
- DatetimeLocal
|
|
41
42
|
- Dialog
|
|
43
|
+
- DisclosureTrigger
|
|
42
44
|
- DropZone
|
|
43
45
|
- FormControl
|
|
44
46
|
- InputFile
|
|
@@ -125,6 +127,42 @@ additionalInteractiveComponentRegexオプションに独自コンポーネント
|
|
|
125
127
|
- onSelect
|
|
126
128
|
- onSubmit
|
|
127
129
|
|
|
130
|
+
#### 子要素で発生したイベントを親要素で処理する場合(delegateしたい場合)
|
|
131
|
+
|
|
132
|
+
非インタラクティブな要素に対して、デフォルトのonXxx形式の属性を設定するパターンとして、delegateが存在します。
|
|
133
|
+
|
|
134
|
+
```jsx
|
|
135
|
+
// 子要素のbuttonから発生したclickイベントを親で受け取り処理する
|
|
136
|
+
<div onClick={onClick}>
|
|
137
|
+
<ButtonA />
|
|
138
|
+
<ButtonB />
|
|
139
|
+
<ButtonC />
|
|
140
|
+
</div>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
上記例の場合、このルールはエラーになりますが、**delegateを目的としたイベントハンドラであること** を分かる状態にすることで回避出来ます。
|
|
144
|
+
|
|
145
|
+
```jsx
|
|
146
|
+
// 設定するハンドラの名称に delegate, もしくはDelegateを含める
|
|
147
|
+
<div onClick={onDelegateClick}>
|
|
148
|
+
<ButtonA />
|
|
149
|
+
<ButtonB />
|
|
150
|
+
<ButtonC />
|
|
151
|
+
</div>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
無名関数を直接渡している場合、ハンドラの引数の名称にdelegate, もしくはDelegateを含めることで同様に回避出来ます。
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
<div onClick={(delegateEv) => onClick(delegateEv)}>
|
|
158
|
+
<ButtonA />
|
|
159
|
+
<ButtonB />
|
|
160
|
+
<ButtonC />
|
|
161
|
+
</div>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
上記のように修正することで **delegateを目的としたイベントハンドラであることが明確** になり、コードの可読性が向上します
|
|
165
|
+
|
|
128
166
|
|
|
129
167
|
## rules
|
|
130
168
|
|
|
@@ -154,6 +192,22 @@ additionalInteractiveComponentRegexオプションに独自コンポーネント
|
|
|
154
192
|
<CrewDetail onChange={onChange} />
|
|
155
193
|
```
|
|
156
194
|
|
|
195
|
+
```jsx
|
|
196
|
+
// 子要素のbuttonから発生したclickイベントを親で受け取り処理する場合
|
|
197
|
+
// イベントハンドラがdelegateを目的とするものであることが分かる必要がある
|
|
198
|
+
<div onClick={onClick}>
|
|
199
|
+
<ButtonA />
|
|
200
|
+
<ButtonB />
|
|
201
|
+
<ButtonC />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div onClick={(e) => onClick(e)}>
|
|
205
|
+
<ButtonA />
|
|
206
|
+
<ButtonB />
|
|
207
|
+
<ButtonC />
|
|
208
|
+
</div>
|
|
209
|
+
```
|
|
210
|
+
|
|
157
211
|
## ✅ Correct
|
|
158
212
|
|
|
159
213
|
```jsx
|
|
@@ -162,7 +216,7 @@ additionalInteractiveComponentRegexオプションに独自コンポーネント
|
|
|
162
216
|
```
|
|
163
217
|
|
|
164
218
|
```jsx
|
|
165
|
-
// additionalInteractiveComponentRegex: ['^InteractiveComponent
|
|
219
|
+
// additionalInteractiveComponentRegex: ['^InteractiveComponent$']
|
|
166
220
|
// インタラクティブなコンポーネントとして扱われるものに対してroleを指定していないのでOK
|
|
167
221
|
<InteractiveComponent any={hoge}>...</InteractiveComponent>
|
|
168
222
|
```
|
|
@@ -176,3 +230,18 @@ additionalInteractiveComponentRegexオプションに独自コンポーネント
|
|
|
176
230
|
// インタラクティブな要素なのでonXxx形式のデフォルト属性を設定してもOK
|
|
177
231
|
<XxxInput onChange={onChange} />
|
|
178
232
|
```
|
|
233
|
+
|
|
234
|
+
```jsx
|
|
235
|
+
// イベントハンドラがdelegateを目的とするものであることが明確なのでOK
|
|
236
|
+
<div onClick={onDelegateClick}>
|
|
237
|
+
<ButtonA />
|
|
238
|
+
<ButtonB />
|
|
239
|
+
<ButtonC />
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div onClick={(delegateEv) => onClick(delegateEv)}>
|
|
243
|
+
<ButtonA />
|
|
244
|
+
<ButtonB />
|
|
245
|
+
<ButtonC />
|
|
246
|
+
</div>
|
|
247
|
+
```
|
|
@@ -10,6 +10,7 @@ const INTERACTIVE_COMPONENT_NAMES = `(${[
|
|
|
10
10
|
'(T|t)extarea(s)?',
|
|
11
11
|
'AccordionPanel(s)?',
|
|
12
12
|
'Anchor',
|
|
13
|
+
'DisclosureTrigger?',
|
|
13
14
|
'DropZone(s)?',
|
|
14
15
|
'Field(S|s)et(s)?',
|
|
15
16
|
'FilterDropdown(s)?',
|
|
@@ -29,6 +30,14 @@ const INTERACTIVE_COMPONENT_NAMES = `(${[
|
|
|
29
30
|
'^summary',
|
|
30
31
|
].join('|')})$`
|
|
31
32
|
const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/
|
|
33
|
+
const DELEGATE_REGEX = /(d|D)elegate/
|
|
34
|
+
|
|
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',
|
|
39
|
+
}
|
|
40
|
+
const NOT_ARROW_ROLE_ATTRIBUTES = Object.entries(ARROW_ROLES).reduce((prev, [key, value]) => `${prev}:not([parent.name.name=/${key}/][value.value="${value}"])`, '')
|
|
32
41
|
|
|
33
42
|
const ELEMENT_HAS_ROLE_ATTRIBUTE = 'JSXOpeningElement:has(JSXAttribute[name.name="role"])'
|
|
34
43
|
const AS_FORM_PART_ATTRIBUTE = 'JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^f(orm|ieldset)$/]'
|
|
@@ -57,21 +66,37 @@ module.exports = {
|
|
|
57
66
|
const targetNameProp = `[name.name=${interactiveComponentRegex}]`
|
|
58
67
|
|
|
59
68
|
return {
|
|
60
|
-
[
|
|
69
|
+
[`JSXOpeningElement${targetNameProp}>JSXAttribute[name.name="role"]${NOT_ARROW_ROLE_ATTRIBUTES}`]: (node) => {
|
|
61
70
|
context.report({
|
|
62
|
-
node,
|
|
63
|
-
message: `${node.name.name}にrole属性は指定しないでください。
|
|
71
|
+
node: node.parent,
|
|
72
|
+
message: `${node.parent.name.name}にrole属性は指定しないでください。
|
|
64
73
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
|
|
65
74
|
});
|
|
66
75
|
},
|
|
67
|
-
[
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
[`JSXOpeningElement>${AS_FORM_PART_ATTRIBUTE}`]: (node) => {
|
|
77
|
+
if (node.parent.attributes.some((a) => a.type === 'JSXAttribute' && a.name?.name === 'role')) {
|
|
78
|
+
context.report({
|
|
79
|
+
node: node.parent,
|
|
80
|
+
message: `<${node.parent.name.name} ${context.sourceCode.getText(node)}>にrole属性は指定しないでください。
|
|
71
81
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
|
|
72
|
-
|
|
82
|
+
});
|
|
83
|
+
}
|
|
73
84
|
},
|
|
74
|
-
[`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE}))>JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}]`]: (node) => {
|
|
85
|
+
[`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE}))>JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}]:not([value.expression.name=${DELEGATE_REGEX}])`]: (node) => {
|
|
86
|
+
switch (node.value.expression.type) {
|
|
87
|
+
case 'MemberExpression':
|
|
88
|
+
if (DELEGATE_REGEX.test(context.sourceCode.getText(node.expression))) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case 'ArrowFunctionExpression':
|
|
93
|
+
if (node.value.expression.params.some((p) => DELEGATE_REGEX.test(p.name))) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
context.report({
|
|
76
101
|
node: node.parent,
|
|
77
102
|
message: `${node.parent.name.name}にデフォルトで用意されているonXxx形式の属性は設定しないでください
|
|
@@ -79,9 +104,12 @@ module.exports = {
|
|
|
79
104
|
- 対応方法1: 対象の属性がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
|
|
80
105
|
- 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
|
|
81
106
|
- 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
|
|
82
|
-
- 対応方法2:
|
|
107
|
+
- 対応方法2: 子要素で発生したイベントを受け取ること(delegate)が目的でonXxx属性を設定している場合、イベントハンドラがdelegateを目的としている事がわかるように修正してください
|
|
108
|
+
- 修正例1: "onClick={onClick}" を設定している場合、 "onClick={onDelegateClick}" のようにDelegate, もしくはdelegateを含む名称に変更する
|
|
109
|
+
- 修正例2: "onClick={(e) => { ... }}" を設定している場合、 "onClick={(delegateEvent) => { ... }}" のように引数をdelegate, もしくはDelegateを含む名称に変更する
|
|
110
|
+
- 対応方法3: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
|
|
83
111
|
- "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに変更、もしくは名称を調整してください
|
|
84
|
-
- 対応方法
|
|
112
|
+
- 対応方法4: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`,
|
|
85
113
|
});
|
|
86
114
|
}
|
|
87
115
|
};
|
|
@@ -23,6 +23,7 @@ const INTERACTIVE_COMPONENT_NAMES = `(${[
|
|
|
23
23
|
'(T|t)extarea(s)?',
|
|
24
24
|
'AccordionPanel(s)?',
|
|
25
25
|
'Anchor',
|
|
26
|
+
'DisclosureTrigger?',
|
|
26
27
|
'DropZone(s)?',
|
|
27
28
|
'Field(S|s)et(s)?',
|
|
28
29
|
'FilterDropdown(s)?',
|
|
@@ -50,9 +51,12 @@ const uninteractiveError = (name) => `${name}にデフォルトで用意され
|
|
|
50
51
|
- 対応方法1: 対象の属性がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
|
|
51
52
|
- 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
|
|
52
53
|
- 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
|
|
53
|
-
- 対応方法2:
|
|
54
|
+
- 対応方法2: 子要素で発生したイベントを受け取ること(delegate)が目的でonXxx属性を設定している場合、イベントハンドラがdelegateを目的としている事がわかるように修正してください
|
|
55
|
+
- 修正例1: "onClick={onClick}" を設定している場合、 "onClick={onDelegateClick}" のようにDelegate, もしくはdelegateを含む名称に変更する
|
|
56
|
+
- 修正例2: "onClick={(e) => { ... }}" を設定している場合、 "onClick={(delegateEvent) => { ... }}" のように引数をdelegate, もしくはDelegateを含む名称に変更する
|
|
57
|
+
- 対応方法3: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
|
|
54
58
|
- "${new RegExp(`(${INTERACTIVE_COMPONENT_NAMES})`)}" の正規表現にmatchするコンポーネントに変更、もしくは名称を調整してください
|
|
55
|
-
- 対応方法
|
|
59
|
+
- 対応方法4: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`
|
|
56
60
|
|
|
57
61
|
ruleTester.run('best-practice-for-interactive-element', rule, {
|
|
58
62
|
valid: [
|
|
@@ -61,6 +65,13 @@ ruleTester.run('best-practice-for-interactive-element', rule, {
|
|
|
61
65
|
{ code: `<CrewDetail onChangeName={onChange} />` },
|
|
62
66
|
{ code: `<Stack as="form" onSubmit={onSubmit} />` },
|
|
63
67
|
{ code: `<Stack any={<Button onClick={onClick} />} />` },
|
|
68
|
+
{ code: `<Stack onSubmit={onDelegateSubmit} />` },
|
|
69
|
+
{ code: `<Stack onSubmit={hoge.fuga.delegateAny.piyo} />` },
|
|
70
|
+
{ code: `<Stack onSubmit={(a, delegateEvent, b) => {}} />` },
|
|
71
|
+
{ code: `<HogeCheckbox role="switch" />` },
|
|
72
|
+
{ code: `<HogeInput role="switch" />` },
|
|
73
|
+
{ code: `<input role="combobox" />` },
|
|
74
|
+
{ code: `<FugaButton role="option" />` },
|
|
64
75
|
],
|
|
65
76
|
invalid: [
|
|
66
77
|
{ code: `<button role="presentation">...</button>`, errors: [{ message: interactiveError('button') }] },
|
|
@@ -70,6 +81,10 @@ ruleTester.run('best-practice-for-interactive-element', rule, {
|
|
|
70
81
|
{ code: `<InteractiveComponent role="group">...</InteractiveComponent>`, options: [{ additionalInteractiveComponentRegex: ['^Interactive'] }], errors: [{ message: interactiveError('InteractiveComponent') }] },
|
|
71
82
|
{ code: `<CrewDetail onChange={onChange} />`, errors: [{ message: uninteractiveError('CrewDetail') }] },
|
|
72
83
|
{ code: `<Stack onSubmit={onSubmit} />`, errors: [{ message: uninteractiveError('Stack') }] },
|
|
84
|
+
{ code: `<HogeCheckbox role="any" />`, errors: [{ message: interactiveError('HogeCheckbox') }] },
|
|
85
|
+
{ code: `<HogeInput role="any" />`, errors: [{ message: interactiveError('HogeInput') }] },
|
|
86
|
+
{ code: `<input role="any" />`, errors: [{ message: interactiveError('input') }] },
|
|
87
|
+
{ code: `<FugaButton role="any" />`, errors: [{ message: interactiveError('FugaButton') }] },
|
|
73
88
|
]
|
|
74
89
|
})
|
|
75
90
|
|