eslint-plugin-smarthr 4.0.2 → 6.0.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 +23 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/rules/a11y-anchor-has-href-attribute/README.md +68 -15
- package/rules/a11y-clickable-element-has-text/README.md +76 -31
- package/rules/a11y-form-control-in-form/README.md +184 -41
- package/rules/best-practice-for-interactive-element/README.md +178 -0
- package/rules/best-practice-for-interactive-element/index.js +82 -0
- package/rules/best-practice-for-spread-syntax/README.md +5 -5
- package/test/best-practice-for-interactive-element.js +71 -0
- package/rules/a11y-delegate-element-has-role-presentation/README.md +0 -55
- package/rules/a11y-delegate-element-has-role-presentation/index.js +0 -225
- package/test/a11y-delegate-element-has-role-presentation.js +0 -76
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# smarthr/best-practice-for-interactive-element
|
|
2
|
+
|
|
3
|
+
インタラクティブな要素・コンポーネントの利用方法のベストプラクティスを定義するルールです。<br />
|
|
4
|
+
以下の内容をチェックします
|
|
5
|
+
|
|
6
|
+
- インタラクティブな要素にrole属性が設定されている場合エラーとします
|
|
7
|
+
- インタラクティブではないコンポーネントに対して、デフォルトで用意されているonXxx形式の属性を設定しようとするとエラーにします
|
|
8
|
+
- 例: `CrewDetail` コンポーネントに `onChange` を設定するとエラー、 `onChangeName` ならOK
|
|
9
|
+
|
|
10
|
+
## インタラクティブな要素・コンポーネントとは何か
|
|
11
|
+
|
|
12
|
+
ユーザーが操作・やり取りが可能な要素を指します。<br />
|
|
13
|
+
このルールに置いてはHTMLとしての要素以外にsmarthr-uiにおけるコンポーネントも含めます。
|
|
14
|
+
|
|
15
|
+
### 具体的な要素・コンポーネントの一覧
|
|
16
|
+
|
|
17
|
+
下記要素・コンポーネント名と一致、もしくはsuffixがコンポーネント名として指定されている場合、その要素はインタラクティブな要素として扱われます。<br />
|
|
18
|
+
また複数形(末尾にsを設定するなど)の場合も同様に扱われます。
|
|
19
|
+
|
|
20
|
+
#### HTMLの対象要素
|
|
21
|
+
|
|
22
|
+
- a
|
|
23
|
+
- button
|
|
24
|
+
- details
|
|
25
|
+
- dialog
|
|
26
|
+
- fieldset
|
|
27
|
+
- form
|
|
28
|
+
- input
|
|
29
|
+
- legend
|
|
30
|
+
- select(option)
|
|
31
|
+
- summary
|
|
32
|
+
- textarea
|
|
33
|
+
|
|
34
|
+
#### smarthr-uiの対象要素
|
|
35
|
+
|
|
36
|
+
- AccordionPanel
|
|
37
|
+
- Anchor
|
|
38
|
+
- Checkbox
|
|
39
|
+
- Date
|
|
40
|
+
- DatetimeLocal
|
|
41
|
+
- Dialog
|
|
42
|
+
- DropZone
|
|
43
|
+
- FormControl
|
|
44
|
+
- InputFile
|
|
45
|
+
- Link
|
|
46
|
+
- MonthPicker
|
|
47
|
+
- RadioButton
|
|
48
|
+
- RadioButtonPanel
|
|
49
|
+
- RemoteDialogTrigger
|
|
50
|
+
- SegmentedControl
|
|
51
|
+
- SideNav
|
|
52
|
+
- Switch
|
|
53
|
+
- TabItem
|
|
54
|
+
- TimePicker
|
|
55
|
+
- WarekiPicker
|
|
56
|
+
|
|
57
|
+
## チェックする内容について
|
|
58
|
+
|
|
59
|
+
### なぜrole属性を設定するべきではないのか
|
|
60
|
+
|
|
61
|
+
role属性はhtmlの要素の意味(role)を変更するための属性です。<br />
|
|
62
|
+
すべてのhtmlの要素はデフォルトの属性を持っており、**基本的にrole属性を利用することは推奨されません**。<br />
|
|
63
|
+
不用意に利用した場合、ブラウザが適切な解釈を行うことが出来ず、必要な機能が有効にならない、などの問題が発生する可能性があります。<br />
|
|
64
|
+
特に`role="presentation"`は **設定した要素は見た目やJSで利用するためのもので意味づけはない** という設定になるため大変強力な設定になります。
|
|
65
|
+
|
|
66
|
+
以上の理由からこのルールは **特に問題が起きやすい、インタラクティブな要素に対して`role`属性を使っていたら忠告する**ことを目的としたものになっています。
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
// button要素にrole="presentation"で意味付けを消してしまっていて大変危険なのでNG
|
|
70
|
+
<button role="presentation">...</button>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
additionalInteractiveComponentRegexオプションに独自コンポーネントの名称を正規表現で設定することで、インタラクティブな要素として判定することも可能です。
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
// additionalInteractiveComponentRegex: ['^InteractiveComponent%']
|
|
77
|
+
// インタラクティブなコンポーネントとして扱われるものに対してrole="presentation"を指定しており危険なのでNG
|
|
78
|
+
<InteractiveComponent role="presentation">...</InteractiveComponent>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### なぜインタラクティブではないコンポーネントに対してデフォルトのonXxx形式の属性を設定するべきではないのか
|
|
82
|
+
|
|
83
|
+
例として前述の `CrewDetail` を使って説明します。
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
<CrewDetail onChange={onChange} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
上記例の場合、**CrewDetailの何が変更された場合利用される属性なのか?** がわかりません。<br />
|
|
90
|
+
`CrewDetail` という名称から **従業員詳細の何かが変わった場合** に利用されることは予測出来ますが、不明瞭です。<br />
|
|
91
|
+
おそらく入力要素ではないか?ということは予想できるかもしれませんが、例えばURLやDBに保存されている値の変更などの可能性もありえます。<br />
|
|
92
|
+
そのため**どんな用途で利用されるものか**を明確にした名称にすることを推奨しています。
|
|
93
|
+
|
|
94
|
+
```jsx
|
|
95
|
+
<CrewDetail onChangeName={onChange} />
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
逆にインタラクティブな要素・コンポーネントはそれ自体にイベントハンドラを設定する場合が多いため問題なくonXxx形式の属性を設定出来ます。
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
<XxxInput onChange={onChange} />
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
このチェックはインタラクティブではないコンポーネントのみが対象のため、以下は対象外です。
|
|
105
|
+
|
|
106
|
+
- インタラクティブ・非インタラクティブを問わずすべての要素(a, divなどすべての要素)
|
|
107
|
+
- インタラクティブなコンポーネント(InputやXxxFormControlなどのコンポーネント)
|
|
108
|
+
|
|
109
|
+
対象となる属性は以下になります。
|
|
110
|
+
|
|
111
|
+
- onChange
|
|
112
|
+
- onInput
|
|
113
|
+
- onFocus
|
|
114
|
+
- onBlur
|
|
115
|
+
- onClick
|
|
116
|
+
- onDoubleClick
|
|
117
|
+
- onKeyDown
|
|
118
|
+
- onKeyUp
|
|
119
|
+
- onKeyPress
|
|
120
|
+
- onMouseEnter
|
|
121
|
+
- onMouseOver
|
|
122
|
+
- onMouseDown
|
|
123
|
+
- onMouseUp
|
|
124
|
+
- onMouseLeave
|
|
125
|
+
- onSelect
|
|
126
|
+
- onSubmit
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## rules
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
{
|
|
133
|
+
rules: {
|
|
134
|
+
'smarthr/best-practice-for-interactive-element': 'error', // 'warn', 'off'
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## ❌ Incorrect
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
// button要素にrole="presentation"で意味付けを消してしまっていて大変危険なのでNG
|
|
143
|
+
<button role="presentation">...</button>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```jsx
|
|
147
|
+
// additionalInteractiveComponentRegex: ['^InteractiveComponent%']
|
|
148
|
+
// インタラクティブなコンポーネントとして扱われるものに対してrole="presentation"を指定しており危険なのでNG
|
|
149
|
+
<InteractiveComponent role="presentation">...</InteractiveComponent>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```jsx
|
|
153
|
+
// 非インタラクティブなコンポーネントと推測されるものにonXxxx形式のデフォルトに存在する属性を設定しているためNG
|
|
154
|
+
<CrewDetail onChange={onChange} />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## ✅ Correct
|
|
158
|
+
|
|
159
|
+
```jsx
|
|
160
|
+
// role属性を設定していないのでOK
|
|
161
|
+
<button>...</button>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
// additionalInteractiveComponentRegex: ['^InteractiveComponent%']
|
|
166
|
+
// インタラクティブなコンポーネントとして扱われるものに対してroleを指定していないのでOK
|
|
167
|
+
<InteractiveComponent any={hoge}>...</InteractiveComponent>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```jsx
|
|
171
|
+
// 非インタラクティブなコンポーネントと推測されるものにonXxxx形式のデフォルト属性ではない名前のイベントハンドラを設定しているのでOK
|
|
172
|
+
<CrewDetail onChangeName={onChange} />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
// インタラクティブな要素なのでonXxx形式のデフォルト属性を設定してもOK
|
|
177
|
+
<XxxInput onChange={onChange} />
|
|
178
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const INTERACTIVE_COMPONENT_NAMES = `(${[
|
|
2
|
+
'(ActionDialogWith|RemoteDialog)Trigger(s)?',
|
|
3
|
+
'(B|b)utton(s)?',
|
|
4
|
+
'(Check|Combo)(B|b)ox(es|s)?',
|
|
5
|
+
'(Date(timeLocal)?|Time|Month|Wareki)Picker(s)?',
|
|
6
|
+
'(F|f)orm(Control|Group|Dialog)?(s)?',
|
|
7
|
+
'(I|i)nput(File)?(s)?',
|
|
8
|
+
'(L|l)egend(s)$',
|
|
9
|
+
'(S|s)elect(s)?',
|
|
10
|
+
'(T|t)extarea(s)?',
|
|
11
|
+
'AccordionPanel(s)?',
|
|
12
|
+
'Anchor',
|
|
13
|
+
'DropZone(s)?',
|
|
14
|
+
'Field(S|s)et(s)?',
|
|
15
|
+
'FilterDropdown(s)?',
|
|
16
|
+
'Link(s)?',
|
|
17
|
+
'Pagination(s)?',
|
|
18
|
+
'RadioButton(Panel)?(s)?',
|
|
19
|
+
'RemoteTrigger(.+)Dialog(s)?',
|
|
20
|
+
'RightFixedNote(s)?',
|
|
21
|
+
'SegmentedControl(s)?',
|
|
22
|
+
'SideNav(s)?',
|
|
23
|
+
'Switch(s)?',
|
|
24
|
+
'TabItem(s)?',
|
|
25
|
+
'^a',
|
|
26
|
+
'^details',
|
|
27
|
+
'^dialog',
|
|
28
|
+
'^option',
|
|
29
|
+
'^summary',
|
|
30
|
+
].join('|')})$`
|
|
31
|
+
const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/
|
|
32
|
+
|
|
33
|
+
const SCHEMA = [
|
|
34
|
+
{
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
additionalInteractiveComponentRegex: { type: 'array', items: { type: 'string' } },
|
|
38
|
+
},
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
45
|
+
*/
|
|
46
|
+
module.exports = {
|
|
47
|
+
meta: {
|
|
48
|
+
type: 'problem',
|
|
49
|
+
schema: SCHEMA,
|
|
50
|
+
},
|
|
51
|
+
create(context) {
|
|
52
|
+
const options = context.options[0]
|
|
53
|
+
const interactiveComponentRegex = new RegExp(`(${INTERACTIVE_COMPONENT_NAMES}${options?.additionalInteractiveComponentRegex ? `|${options.additionalInteractiveComponentRegex.join('|')}` : ''})`)
|
|
54
|
+
|
|
55
|
+
const interactiveAction = (node) => {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
message: `${node.name.name}にrole属性は指定しないでください。
|
|
59
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
[`JSXOpeningElement[name.name=${interactiveComponentRegex}]:has(JSXAttribute[name.name="role"])`]: interactiveAction,
|
|
65
|
+
'JSXOpeningElement:has(JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^f(orm|ieldset)$/]):has(JSXAttribute[name.name="role"])': interactiveAction,
|
|
66
|
+
[`JSXOpeningElement:not([name.name=${interactiveComponentRegex}]):has(JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}])`]: (node) => {
|
|
67
|
+
context.report({
|
|
68
|
+
node,
|
|
69
|
+
message: `${node.name.name}にデフォルトで用意されているonXxx形式の属性は設定しないでください
|
|
70
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element
|
|
71
|
+
- 対応方法1: 対象の属性がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
|
|
72
|
+
- 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
|
|
73
|
+
- 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
|
|
74
|
+
- 対応方法2: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
|
|
75
|
+
- "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに変更、もしくは名称を調整してください
|
|
76
|
+
- 対応方法3: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
module.exports.schema = SCHEMA;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# spread syntax を通常の属性より後に記述した場合に発生する問題
|
|
9
9
|
|
|
10
|
-
spread syntaxより先に通常の属性を記述した場合、**
|
|
10
|
+
spread syntaxより先に通常の属性を記述した場合、**spread syntaxで値が上書きされる** 可能性があります。
|
|
11
11
|
|
|
12
12
|
```jsx
|
|
13
13
|
const AnyComponent = (props: Props) => {
|
|
@@ -16,10 +16,10 @@ const AnyComponent = (props: Props) => {
|
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
上記例の場合、**
|
|
20
|
-
(idが必須の場合、Fugaにベタ書きしているidがそもそも不要になるため)
|
|
21
|
-
また`idのデフォルト値が"ABC", propsでidが指定された場合そちらを優先` というロジックが必要かどうかを該当コードの部分だけで判断出来ず、広範囲の確認が必要になりがちです。
|
|
22
|
-
特にidなど多用され、HTMLの要素が持つ属性と名前が被っている場合、意図せず上書きされてしまう問題が発生する可能性が高く危険です。
|
|
19
|
+
上記例の場合、**Props型がidを含むか? 含む場合必須か?などの確認** が必要になります。
|
|
20
|
+
(idが必須の場合、Fugaにベタ書きしているidがそもそも不要になるため)
|
|
21
|
+
また`idのデフォルト値が"ABC", propsでidが指定された場合そちらを優先` というロジックが必要かどうかを該当コードの部分だけで判断出来ず、広範囲の確認が必要になりがちです。
|
|
22
|
+
特にidなど多用され、HTMLの要素が持つ属性と名前が被っている場合、意図せず上書きされてしまう問題が発生する可能性が高く危険です。
|
|
23
23
|
そのため下記の様に記述することを推奨します。
|
|
24
24
|
|
|
25
25
|
```jsx
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-interactive-element')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const INTERACTIVE_COMPONENT_NAMES = `(${[
|
|
15
|
+
'(ActionDialogWith|RemoteDialog)Trigger(s)?',
|
|
16
|
+
'(B|b)utton(s)?',
|
|
17
|
+
'(Check|Combo)(B|b)ox(es|s)?',
|
|
18
|
+
'(Date(timeLocal)?|Time|Month|Wareki)Picker(s)?',
|
|
19
|
+
'(F|f)orm(Control|Group|Dialog)?(s)?',
|
|
20
|
+
'(I|i)nput(File)?(s)?',
|
|
21
|
+
'(L|l)egend(s)$',
|
|
22
|
+
'(S|s)elect(s)?',
|
|
23
|
+
'(T|t)extarea(s)?',
|
|
24
|
+
'AccordionPanel(s)?',
|
|
25
|
+
'Anchor',
|
|
26
|
+
'DropZone(s)?',
|
|
27
|
+
'Field(S|s)et(s)?',
|
|
28
|
+
'FilterDropdown(s)?',
|
|
29
|
+
'Link(s)?',
|
|
30
|
+
'Pagination(s)?',
|
|
31
|
+
'RadioButton(Panel)?(s)?',
|
|
32
|
+
'RemoteTrigger(.+)Dialog(s)?',
|
|
33
|
+
'RightFixedNote(s)?',
|
|
34
|
+
'SegmentedControl(s)?',
|
|
35
|
+
'SideNav(s)?',
|
|
36
|
+
'Switch(s)?',
|
|
37
|
+
'TabItem(s)?',
|
|
38
|
+
'^a',
|
|
39
|
+
'^details',
|
|
40
|
+
'^dialog',
|
|
41
|
+
'^option',
|
|
42
|
+
'^summary',
|
|
43
|
+
].join('|')})$`
|
|
44
|
+
const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/
|
|
45
|
+
|
|
46
|
+
const interactiveError = (name) => `${name}にrole属性は指定しないでください。
|
|
47
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`
|
|
48
|
+
const uninteractiveError = (name) => `${name}にデフォルトで用意されているonXxx形式の属性は設定しないでください
|
|
49
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element
|
|
50
|
+
- 対応方法1: 対象の属性がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
|
|
51
|
+
- 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
|
|
52
|
+
- 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
|
|
53
|
+
- 対応方法2: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
|
|
54
|
+
- "${new RegExp(`(${INTERACTIVE_COMPONENT_NAMES})`)}" の正規表現にmatchするコンポーネントに変更、もしくは名称を調整してください
|
|
55
|
+
- 対応方法3: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`
|
|
56
|
+
|
|
57
|
+
ruleTester.run('best-practice-for-interactive-element', rule, {
|
|
58
|
+
valid: [
|
|
59
|
+
{ code: `<button>...</button>` },
|
|
60
|
+
{ code: `<InteractiveComponent>...</InteractiveComponent>`, options: [{ additionalInteractiveComponentRegex: ['^InteractiveComponent%'] }] },
|
|
61
|
+
{ code: `<CrewDetail onChangeName={onChange} />` },
|
|
62
|
+
],
|
|
63
|
+
invalid: [
|
|
64
|
+
{ code: `<button role="presentation">...</button>`, errors: [{ message: interactiveError('button') }] },
|
|
65
|
+
{ code: `<Hoge as="form" role="menu" />`, errors: [{ message: interactiveError('Hoge') }] },
|
|
66
|
+
{ code: `<FormControl role="menu" />`, errors: [{ message: interactiveError('FormControl') }] },
|
|
67
|
+
{ code: `<InteractiveComponent role="group">...</InteractiveComponent>`, options: [{ additionalInteractiveComponentRegex: ['^Interactive'] }], errors: [{ message: interactiveError('InteractiveComponent') }] },
|
|
68
|
+
{ code: `<CrewDetail onChange={onChange} />`, errors: [{ message: uninteractiveError('CrewDetail') }] },
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# smarthr/a11y-delegate-element-has-role-presentation
|
|
2
|
-
|
|
3
|
-
- 'role="presentation"'を適切に設定することを促すルールです
|
|
4
|
-
- インタラクティブな要素に対して'role="presentation"'が設定されている場合、エラーになります
|
|
5
|
-
- インタラクティブな要素とは form, inputなどの入力要素、button, a などのクリッカブルな要素を指します
|
|
6
|
-
- インタラクティブな要素から発生するイベントを親要素でキャッチする場合、親要素に 'role="presentation"' を設定することを促します
|
|
7
|
-
- インタラクティブではない要素でイベントをキャッチしており、かつ'role="presentation"'を設定しているにも関わらず、子要素にインタラクティブな要素がない場合はエラーになります
|
|
8
|
-
- additionalInteractiveComponentRegexオプションに独自コンポーネントの名称を正規表現で設定することで、インタラクティブな要素として判定することが可能です
|
|
9
|
-
|
|
10
|
-
## rules
|
|
11
|
-
|
|
12
|
-
```js
|
|
13
|
-
{
|
|
14
|
-
rules: {
|
|
15
|
-
'smarthr/a11y-delegate-element-has-role-presentation': [
|
|
16
|
-
'error', // 'warn', 'off'
|
|
17
|
-
// { additionalInteractiveComponentRegex: ['^InteractiveComponent%'] }
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## ❌ Incorrect
|
|
24
|
-
|
|
25
|
-
```jsx
|
|
26
|
-
// インタラクティブな要素に対して role="presentation" は設定できない
|
|
27
|
-
<Button role="presentation">text.</Button>
|
|
28
|
-
<input type="text" role="presentation" />
|
|
29
|
-
|
|
30
|
-
// インタラクティブな要素で発生するイベントを非インタラクティブな要素でキャッチする場合
|
|
31
|
-
// role="presentation" を設定する必要がある
|
|
32
|
-
<div onClick={hoge}>
|
|
33
|
-
<Button>text.</Button>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
// 非インタラクティブな要素でイベントをキャッチする場合、
|
|
37
|
-
// 子要素にインタラクティブな要素がない場合はエラー
|
|
38
|
-
<div onClick={hoge} role="presentation">
|
|
39
|
-
<Text>hoge.</Text>
|
|
40
|
-
</div>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## ✅ Correct
|
|
44
|
-
|
|
45
|
-
```jsx
|
|
46
|
-
// インタラクティブな要素で発生するイベントを非インタラクティブな要素でキャッチする場合
|
|
47
|
-
// role="presentation" を設定する
|
|
48
|
-
<div onClick={hoge} role="presentation">
|
|
49
|
-
<Button>text.</Button>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<div onClick={hoge} role="presentation">
|
|
53
|
-
<AnyForm />
|
|
54
|
-
</div>
|
|
55
|
-
```
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
const INTERACTIVE_COMPONENT_NAMES = `(${[
|
|
2
|
-
'(B|b)utton(s)?',
|
|
3
|
-
'(Check|Combo)(B|b)ox(es|s)?',
|
|
4
|
-
'(Date(timeLocal)?|Time|Month|Wareki)Picker(s)?',
|
|
5
|
-
'(I|i)nput(File)?(s)?',
|
|
6
|
-
'(S|s)elect(s)?',
|
|
7
|
-
'(T|t)extarea(s)?',
|
|
8
|
-
'(ActionDialogWith|RemoteDialog)Trigger(s)?',
|
|
9
|
-
'AccordionPanel(s)?',
|
|
10
|
-
'^a',
|
|
11
|
-
'Anchor',
|
|
12
|
-
'Link(s)?',
|
|
13
|
-
'DropZone(s)?',
|
|
14
|
-
'Field(S|s)et(s)?',
|
|
15
|
-
'FilterDropdown(s)?',
|
|
16
|
-
'(F|f)orm(Control|Group|Dialog)?(s)?',
|
|
17
|
-
'Pagination(s)?',
|
|
18
|
-
'RadioButton(Panel)?(s)?',
|
|
19
|
-
'RemoteTrigger(.+)Dialog(s)?',
|
|
20
|
-
'RightFixedNote(s)?',
|
|
21
|
-
'SegmentedControl(s)?',
|
|
22
|
-
'SideNav(s)?',
|
|
23
|
-
'Switch(s)?',
|
|
24
|
-
'TabItem(s)?',
|
|
25
|
-
].join('|')})$`
|
|
26
|
-
const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/
|
|
27
|
-
const MEANED_ROLE_REGEX = /^(combobox|group|slider|toolbar)$/
|
|
28
|
-
const INTERACTIVE_NODE_TYPE_REGEX = /^(JSXElement|JSXExpressionContainer|ConditionalExpression)$/
|
|
29
|
-
const AS_REGEX = /^(as|forwardedAs)$/
|
|
30
|
-
const AS_VALUE_REGEX = /^(form|fieldset)$/
|
|
31
|
-
|
|
32
|
-
const messageNonInteractiveEventHandler = (nodeName, interactiveComponentRegex, onAttrs) => {
|
|
33
|
-
const onAttrsText = onAttrs.join(', ')
|
|
34
|
-
|
|
35
|
-
return `${nodeName} に${onAttrsText}を設定するとブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
|
|
36
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-delegate-element-has-role-presentation
|
|
37
|
-
- 方法1: ${nodeName}がinput、buttonやaなどのインタラクティブな要素の場合、コンポーネント名の末尾をインタラクティブなコンポーネントであることがわかる名称に変更してください
|
|
38
|
-
- "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに差し替える、もしくは名称を変更してください
|
|
39
|
-
- 方法2: ${onAttrsText} がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
|
|
40
|
-
- 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
|
|
41
|
-
- 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
|
|
42
|
-
- 方法3: インタラクティブな親要素、もしくは子要素が存在する場合、直接${onAttrsText}を設定することを検討してください
|
|
43
|
-
- 方法4: インタラクティブな親要素、もしくは子要素が存在しない場合、インタラクティブな要素を必ず持つようにマークアップを修正後、${onAttrsText}の設定要素を検討してください
|
|
44
|
-
- 方法5: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください
|
|
45
|
-
- 'role="presentation"' を設定した要素はマークアップとしての意味がなくなるため、div・span などマークアップとしての意味を持たない要素に設定してください
|
|
46
|
-
- 'role="presentation"' を設定する適切な要素が存在しない場合、div、またはspanでイベントが発生する要素を囲んだ上でrole属性を設定してください`
|
|
47
|
-
}
|
|
48
|
-
const messageRolePresentationNotHasInteractive = (nodeName, interactiveComponentRegex, onAttrs, roleMean) => `${nodeName}に 'role="${roleMean}"' が設定されているにも関わらず、子要素にinput、buttonやaなどのインタラクティブな要素が見つからないため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
|
|
49
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-delegate-element-has-role-presentation
|
|
50
|
-
- 方法1: 子要素にインタラクティブな要素が存在するにも関わらずこのエラーが表示されている場合、子要素の名称を変更してください
|
|
51
|
-
- "${interactiveComponentRegex}" の正規表現にmatchするよう、インタラクティブな子要素全てを差し替える、もしくは名称を変更してください
|
|
52
|
-
- 方法2: ${nodeName}自体がインタラクティブな要素の場合、'role="presentation"'を削除した上で名称を変更してください
|
|
53
|
-
- "${interactiveComponentRegex}" の正規表現にmatchするよう、${nodeName}の名称を変更してください
|
|
54
|
-
- 方法3: 子要素にインタラクティブな要素が存在し、${onAttrs.join(', ')}全属性をそれらの要素に移動させられる場合、'role="presentation"'を消した上で実施してください`
|
|
55
|
-
const messageInteractiveHasRolePresentation = (nodeName, interactiveComponentRegex) => `${nodeName}はinput、buttonやaなどのインタラクティブな要素にもかかわらず 'role="presentation"' が設定されているため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
|
|
56
|
-
- 方法1: 'role="presentation"' を削除してください
|
|
57
|
-
- 方法2: ${nodeName}の名称を "${interactiveComponentRegex}" とマッチしない名称に変更してください`
|
|
58
|
-
|
|
59
|
-
const SCHEMA = [
|
|
60
|
-
{
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
additionalInteractiveComponentRegex: { type: 'array', items: { type: 'string' } },
|
|
64
|
-
},
|
|
65
|
-
additionalProperties: false,
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
71
|
-
*/
|
|
72
|
-
module.exports = {
|
|
73
|
-
meta: {
|
|
74
|
-
type: 'problem',
|
|
75
|
-
schema: SCHEMA,
|
|
76
|
-
},
|
|
77
|
-
create(context) {
|
|
78
|
-
const options = context.options[0]
|
|
79
|
-
const interactiveComponentRegex = new RegExp(`(${INTERACTIVE_COMPONENT_NAMES}${options?.additionalInteractiveComponentRegex ? `|${options.additionalInteractiveComponentRegex.join('|')}` : ''})`)
|
|
80
|
-
const findInteractiveNode = (ec) => ec && INTERACTIVE_NODE_TYPE_REGEX.test(ec.type) && isHasInteractive(ec)
|
|
81
|
-
const isHasInteractive = (c) => {
|
|
82
|
-
switch (c.type) {
|
|
83
|
-
case 'JSXElement': {
|
|
84
|
-
const name = c.openingElement.name.name
|
|
85
|
-
|
|
86
|
-
if (name && interactiveComponentRegex.test(name)) {
|
|
87
|
-
return true
|
|
88
|
-
} else if (c.children.length > 0) {
|
|
89
|
-
return !!c.children.find(isHasInteractive)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
case 'JSXExpressionContainer':
|
|
93
|
-
case 'ConditionalExpression': {
|
|
94
|
-
let e = c
|
|
95
|
-
|
|
96
|
-
if (c.expression) {
|
|
97
|
-
e = c.expression
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return !![e.right, e.consequent, e.alternate].find(findInteractiveNode)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return false
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
JSXOpeningElement: (node) => {
|
|
109
|
-
const nodeName = node.name.name || '';
|
|
110
|
-
|
|
111
|
-
let onAttrs = []
|
|
112
|
-
let roleMean = undefined
|
|
113
|
-
let isRolePresentation = false
|
|
114
|
-
let isAsInteractive = false
|
|
115
|
-
|
|
116
|
-
node.attributes.forEach((a) => {
|
|
117
|
-
const aName = a.name?.name || ''
|
|
118
|
-
|
|
119
|
-
if (INTERACTIVE_ON_REGEX.test(aName)) {
|
|
120
|
-
onAttrs.push(aName)
|
|
121
|
-
} else if (AS_REGEX.test(aName) && AS_VALUE_REGEX.test(a.value?.value || '')) {
|
|
122
|
-
isAsInteractive = true
|
|
123
|
-
} else if (aName === 'role') {
|
|
124
|
-
const v = a.value?.value || ''
|
|
125
|
-
|
|
126
|
-
if (v === 'presentation') {
|
|
127
|
-
isRolePresentation = true
|
|
128
|
-
roleMean = v
|
|
129
|
-
} else if (MEANED_ROLE_REGEX.test(v)) {
|
|
130
|
-
roleMean = v
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
if (isAsInteractive || interactiveComponentRegex.test(nodeName)) {
|
|
136
|
-
if (isRolePresentation) {
|
|
137
|
-
context.report({
|
|
138
|
-
node,
|
|
139
|
-
message: messageInteractiveHasRolePresentation(nodeName, interactiveComponentRegex)
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
} else if (onAttrs.length > 0) {
|
|
143
|
-
// HINT: role="presentation"以外で意味があるroleが設定されている場合はエラーにしない
|
|
144
|
-
// 基本的にsmarthr-uiでroleの設定などは巻き取る && そもそもroleを設定するよりタグを適切にマークアップすることが優先されるため
|
|
145
|
-
// エラーなどには表示しない
|
|
146
|
-
if (!roleMean) {
|
|
147
|
-
context.report({
|
|
148
|
-
node,
|
|
149
|
-
message: messageNonInteractiveEventHandler(nodeName, interactiveComponentRegex, onAttrs),
|
|
150
|
-
});
|
|
151
|
-
// HINT: role='slider' はインタラクティブな要素扱いとするため除外する
|
|
152
|
-
} else if (roleMean !== 'slider') {
|
|
153
|
-
const searchChildren = (n) => {
|
|
154
|
-
switch (n.type) {
|
|
155
|
-
case 'BinaryExpression':
|
|
156
|
-
case 'Identifier':
|
|
157
|
-
case 'JSXEmptyExpression':
|
|
158
|
-
case 'JSXText':
|
|
159
|
-
case 'Literal':
|
|
160
|
-
case 'VariableDeclaration':
|
|
161
|
-
// これ以上childrenが存在しないため終了
|
|
162
|
-
return false
|
|
163
|
-
case 'JSXAttribute':
|
|
164
|
-
return n.value ? searchChildren(n.value) : false
|
|
165
|
-
case 'LogicalExpression':
|
|
166
|
-
return searchChildren(n.right)
|
|
167
|
-
case 'ArrowFunctionExpression':
|
|
168
|
-
return searchChildren(n.body)
|
|
169
|
-
case 'MemberExpression':
|
|
170
|
-
return searchChildren(n.property)
|
|
171
|
-
case 'ReturnStatement':
|
|
172
|
-
case 'UnaryExpression':
|
|
173
|
-
return searchChildren(n.argument)
|
|
174
|
-
case 'ChainExpression':
|
|
175
|
-
case 'JSXExpressionContainer':
|
|
176
|
-
return searchChildren(n.expression)
|
|
177
|
-
case 'BlockStatement':
|
|
178
|
-
return forInSearchChildren(n.body)
|
|
179
|
-
case 'ConditionalExpression':
|
|
180
|
-
return searchChildren(n.consequent) || searchChildren(n.alternate)
|
|
181
|
-
case 'CallExpression': {
|
|
182
|
-
return forInSearchChildren(n.arguments)
|
|
183
|
-
}
|
|
184
|
-
case 'JSXFragment':
|
|
185
|
-
break
|
|
186
|
-
case 'JSXElement': {
|
|
187
|
-
const name = n.openingElement.name.name || ''
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
interactiveComponentRegex.test(name) ||
|
|
191
|
-
forInSearchChildren(n.openingElement.attributes)
|
|
192
|
-
) {
|
|
193
|
-
return true
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
break
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return n.children ? forInSearchChildren(n.children) : false
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const forInSearchChildren = (ary) => {
|
|
204
|
-
for (const i in ary) {
|
|
205
|
-
if (searchChildren(ary[i])) {
|
|
206
|
-
return true
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return false
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (!forInSearchChildren(node.parent.children)) {
|
|
214
|
-
context.report({
|
|
215
|
-
node,
|
|
216
|
-
message: messageRolePresentationNotHasInteractive(nodeName, interactiveComponentRegex, onAttrs, roleMean)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
module.exports.schema = SCHEMA;
|