eslint-plugin-smarthr 6.0.1 → 6.1.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.1.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.0.2...eslint-plugin-smarthr-v6.1.0) (2026-01-29)
6
+
7
+
8
+ ### Features
9
+
10
+ * **best-practice-for-interactive-element:** delegateでイベントを受け取ることを明確にするようコード修正を促すように修正 ([#1027](https://github.com/kufu/tamatebako/issues/1027)) ([476c858](https://github.com/kufu/tamatebako/commit/476c858d87a581a33e764ac1aa9aafee29805217))
11
+
12
+ ## [6.0.2](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.0.1...eslint-plugin-smarthr-v6.0.2) (2026-01-29)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **best-practice-for-interactive-element:** 属性にインタラクティブなコンポーネントを設定している場合、onXxxが誤検知されてしまうバグを修正 ([#1023](https://github.com/kufu/tamatebako/issues/1023)) ([81563cd](https://github.com/kufu/tamatebako/commit/81563cd2fa13ae9f53306d592b2f86e17e1f790c))
18
+
5
19
  ## [6.0.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.0.0...eslint-plugin-smarthr-v6.0.1) (2026-01-28)
6
20
 
7
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.0.1",
3
+ "version": "6.1.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": "87bf718cd19e5f353c0b93cedcc2ca8ba250ba7c"
40
+ "gitHead": "1b4d368c3cdf3b637b8f4866638b2b8bc261f3ca"
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,7 @@ 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/
32
34
 
33
35
  const ELEMENT_HAS_ROLE_ATTRIBUTE = 'JSXOpeningElement:has(JSXAttribute[name.name="role"])'
34
36
  const AS_FORM_PART_ATTRIBUTE = 'JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^f(orm|ieldset)$/]'
@@ -57,31 +59,50 @@ module.exports = {
57
59
  const targetNameProp = `[name.name=${interactiveComponentRegex}]`
58
60
 
59
61
  return {
60
- [`${ELEMENT_HAS_ROLE_ATTRIBUTE}${targetNameProp}`]: (node) => {
62
+ [`JSXOpeningElement${targetNameProp}>JSXAttribute[name.name="role"]:not([parent.name.name=/(^c|C)heck(b|B)ox$/][value.value="switch"])`]: (node) => {
61
63
  context.report({
62
- node,
63
- message: `${node.name.name}にrole属性は指定しないでください。
64
+ node: node.parent,
65
+ message: `${node.parent.name.name}にrole属性は指定しないでください。
64
66
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
65
67
  });
66
68
  },
67
- [`${ELEMENT_HAS_ROLE_ATTRIBUTE} ${AS_FORM_PART_ATTRIBUTE}`]: (node) => {
68
- context.report({
69
- node: node.parent,
70
- message: `<${node.parent.name.name} ${context.sourceCode.getText(node)}>にrole属性は指定しないでください。
69
+ [`JSXOpeningElement>${AS_FORM_PART_ATTRIBUTE}`]: (node) => {
70
+ if (node.parent.attributes.some((a) => a.type === 'JSXAttribute' && a.name?.name === 'role')) {
71
+ context.report({
72
+ node: node.parent,
73
+ message: `<${node.parent.name.name} ${context.sourceCode.getText(node)}>にrole属性は指定しないでください。
71
74
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
72
- });
75
+ });
76
+ }
73
77
  },
74
- [`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE})):has(JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}])`]: (node) => {
78
+ [`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE}))>JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}]:not([value.expression.name=${DELEGATE_REGEX}])`]: (node) => {
79
+ switch (node.value.expression.type) {
80
+ case 'MemberExpression':
81
+ if (DELEGATE_REGEX.test(context.sourceCode.getText(node.expression))) {
82
+ return
83
+ }
84
+ break
85
+ case 'ArrowFunctionExpression':
86
+ if (node.value.expression.params.some((p) => DELEGATE_REGEX.test(p.name))) {
87
+ return
88
+ }
89
+
90
+ break
91
+ }
92
+
75
93
  context.report({
76
- node,
77
- message: `${node.name.name}にデフォルトで用意されているonXxx形式の属性は設定しないでください
94
+ node: node.parent,
95
+ message: `${node.parent.name.name}にデフォルトで用意されているonXxx形式の属性は設定しないでください
78
96
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element
79
97
  - 対応方法1: 対象の属性がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
80
98
  - 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
81
99
  - 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
82
- - 対応方法2: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
100
+ - 対応方法2: 子要素で発生したイベントを受け取ること(delegate)が目的でonXxx属性を設定している場合、イベントハンドラがdelegateを目的としている事がわかるように修正してください
101
+ - 修正例1: "onClick={onClick}" を設定している場合、 "onClick={onDelegateClick}" のようにDelegate, もしくはdelegateを含む名称に変更する
102
+ - 修正例2: "onClick={(e) => { ... }}" を設定している場合、 "onClick={(delegateEvent) => { ... }}" のように引数をdelegate, もしくはDelegateを含む名称に変更する
103
+ - 対応方法3: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
83
104
  - "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに変更、もしくは名称を調整してください
84
- - 対応方法3: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`,
105
+ - 対応方法4: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`,
85
106
  });
86
107
  }
87
108
  };
@@ -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
- - 対応方法3: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`
59
+ - 対応方法4: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`
56
60
 
57
61
  ruleTester.run('best-practice-for-interactive-element', rule, {
58
62
  valid: [
@@ -60,6 +64,11 @@ ruleTester.run('best-practice-for-interactive-element', rule, {
60
64
  { code: `<InteractiveComponent>...</InteractiveComponent>`, options: [{ additionalInteractiveComponentRegex: ['^InteractiveComponent%'] }] },
61
65
  { code: `<CrewDetail onChangeName={onChange} />` },
62
66
  { code: `<Stack as="form" onSubmit={onSubmit} />` },
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" />` },
63
72
  ],
64
73
  invalid: [
65
74
  { code: `<button role="presentation">...</button>`, errors: [{ message: interactiveError('button') }] },
@@ -69,6 +78,7 @@ ruleTester.run('best-practice-for-interactive-element', rule, {
69
78
  { code: `<InteractiveComponent role="group">...</InteractiveComponent>`, options: [{ additionalInteractiveComponentRegex: ['^Interactive'] }], errors: [{ message: interactiveError('InteractiveComponent') }] },
70
79
  { code: `<CrewDetail onChange={onChange} />`, errors: [{ message: uninteractiveError('CrewDetail') }] },
71
80
  { code: `<Stack onSubmit={onSubmit} />`, errors: [{ message: uninteractiveError('Stack') }] },
81
+ { code: `<HogeCheckbox role="any" />`, errors: [{ message: interactiveError('HogeCheckbox') }] },
72
82
  ]
73
83
  })
74
84