eslint-plugin-smarthr 5.0.0 → 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 +12 -0
- package/README.md +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v5.0.0...eslint-plugin-smarthr-v6.0.0) (2026-01-28)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* **a11y-delegate-element-has-role-presentation:** ルールを削除します ([#1015](https://github.com/kufu/tamatebako/issues/1015))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **a11y-delegate-element-has-role-presentation:** ルールを削除します ([#1015](https://github.com/kufu/tamatebako/issues/1015)) ([4c5c7ab](https://github.com/kufu/tamatebako/commit/4c5c7ab43ab7a52f749963aa7b7cb359e6f623d5))
|
|
15
|
+
* best-practice-for-interactive-elementを追加 ([#1014](https://github.com/kufu/tamatebako/issues/1014)) ([e0a171b](https://github.com/kufu/tamatebako/commit/e0a171b056d8dbe775e3697633136e60c84afe5d))
|
|
16
|
+
|
|
5
17
|
## [5.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v4.0.2...eslint-plugin-smarthr-v5.0.0) (2026-01-08)
|
|
6
18
|
|
|
7
19
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
- [a11y-anchor-has-href-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-anchor-has-href-attribute)
|
|
4
4
|
- [a11y-clickable-element-has-text](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-clickable-element-has-text)
|
|
5
|
-
- [a11y-delegate-element-has-role-presentation](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-delegate-element-has-role-presentation)
|
|
6
5
|
- [a11y-form-control-in-form](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-form-control-in-form)
|
|
7
6
|
- [a11y-heading-in-sectioning-content](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content)
|
|
8
7
|
- [a11y-help-link-with-support-href](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-help-link-with-support-href)
|
|
@@ -19,6 +18,7 @@
|
|
|
19
18
|
- [best-practice-for-button-element](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-button-element)
|
|
20
19
|
- [best-practice-for-data-test-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-data-test-attribute)
|
|
21
20
|
- [best-practice-for-date](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-date)
|
|
21
|
+
- [best-practice-for-interactive-element](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element)
|
|
22
22
|
- [best-practice-for-layouts](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-layouts)
|
|
23
23
|
- [best-practice-for-nested-attributes-array-index](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-nested-attributes-array-index)
|
|
24
24
|
- [best-practice-for-optional-chaining](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-optional-chaining)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.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": "d9b333ac3b44127f7db54408dfa84ce49682a59e"
|
|
41
41
|
}
|
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
# smarthr/a11y-anchor-has-href-attribute
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
a, Anchor, Link コンポーネントに href 属性を設定することを促すルールです
|
|
4
|
+
|
|
5
|
+
## なぜa要素にhref属性を設定するべきなのか
|
|
6
|
+
|
|
7
|
+
href属性が設定されていないa要素は**遷移先が存在しない無効化されたリンク**という扱いになります。
|
|
8
|
+
これはbutton要素で例えるなら**disalbed属性が設定された状態のbutton要素**と同等です。
|
|
9
|
+
|
|
10
|
+
またhref属性がないa要素ではtab移動の対象にならない・コンテキストメニュー(右クリックメニュー)から別タブで開く機能が使えない、などのデメリットが発生します。
|
|
11
|
+
これらの機能は **href属性が存在する == 有効なリンクである** ことから有効になります。
|
|
12
|
+
|
|
13
|
+
逆説的に**設定したいhref属性が存在しない場合、a要素ではなくbutton要素を利用する**ように心がけてください。
|
|
14
|
+
a要素はあくまでURL遷移を表現するための要素であるため、それ以外の処理のトリガーとして利用するならばbutton要素のほうが適切です。
|
|
15
|
+
|
|
16
|
+
また**遷移先が存在しない無効化されたリンク**を表現したい場合、明示的に`href={undefined}`を設定することを推奨しています。
|
|
17
|
+
|
|
18
|
+
## react-router, next/Linkを利用している場合
|
|
19
|
+
|
|
20
|
+
react-router, next/Linkを利用している場合、href属性と同等の機能を提供する別属性が存在するため、自動的にチェック方法が切り替わります。
|
|
21
|
+
利用しているか否かの判定にはpackage.jsonが利用され、dependenciesに `react-router` もしくは `next` が存在するかどうかで判断されます。
|
|
22
|
+
|
|
23
|
+
### react-routerを利用している場合
|
|
24
|
+
|
|
25
|
+
a要素にto属性が指定されている場合、href属性が指定されているものとして許容します。
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
// react-routerを利用している場合、かつto属性を設定しているためOK
|
|
29
|
+
<Link to={hoge}>any</Link>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### next/Linkを利用している場合
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
next/link コンポーネント直下のa要素にhref属性が指定されていないことを許容します。
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
// next/Linkを利用している場合、子のaにhref属性がなくてもOK
|
|
39
|
+
<Link href={hoge}><a>any</a></Link>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### spread-attributesが設定されているなら許容したい場合
|
|
43
|
+
|
|
44
|
+
下記の様にspread attributesが設定されていれば、href属性が設定されている扱いにしたい場合、lintのoptionとして `checkType` に `allow-spread-attributes` を設定してください。
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
// checkType: 'allow-spread-attributes'
|
|
48
|
+
<XxxAnchor {...args} />
|
|
49
|
+
<XxxLink {...args} any="any" />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
便利な設定ではありますが、**href属性が実際に設定されているかは判定出来ていないため、チェック漏れが発生する可能性があります。**
|
|
53
|
+
設定される場合は慎重に検討してください。
|
|
12
54
|
|
|
13
55
|
## rules
|
|
14
56
|
|
|
@@ -26,12 +68,15 @@
|
|
|
26
68
|
## ❌ Incorrect
|
|
27
69
|
|
|
28
70
|
```jsx
|
|
71
|
+
// a要素と思われるコンポーネントにhref属性が設定されていないためNG
|
|
29
72
|
<a>any</a>
|
|
30
73
|
<XxxAnchor>any</XxxAnchor>
|
|
31
74
|
<XxxLink>any</XxxLink>
|
|
32
75
|
<XxxLink href>any</XxxLink>
|
|
76
|
+
```
|
|
33
77
|
|
|
34
|
-
|
|
78
|
+
```jsx
|
|
79
|
+
// spread attributesでhref属性が含まれていてもデフォルト設定ではNGになる
|
|
35
80
|
<XxxAnchor {...args} />
|
|
36
81
|
<XxxLink {...args} any="any" />
|
|
37
82
|
```
|
|
@@ -39,17 +84,25 @@
|
|
|
39
84
|
## ✅ Correct
|
|
40
85
|
|
|
41
86
|
```jsx
|
|
87
|
+
// a要素と思われるコンポーネントにhref属性が設定されているのでOK
|
|
42
88
|
<a href="https://www.google.com/search">any</a>
|
|
43
89
|
<XxxAnchor href={hoge}>any</XxxAnchor>
|
|
44
90
|
<XxxLink href={undefined}>any</XxxLink>
|
|
91
|
+
```
|
|
45
92
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// react-router-domを利用している場合
|
|
93
|
+
```jsx
|
|
94
|
+
// react-routerを利用している場合、かつto属性を設定しているためOK
|
|
50
95
|
<Link to={hoge}>any</Link>
|
|
96
|
+
```
|
|
51
97
|
|
|
52
|
-
|
|
98
|
+
```jsx
|
|
99
|
+
// next/Linkを利用している場合、子のaにhref属性がなくてもOK
|
|
100
|
+
<Link href={hoge}><a>any</a></Link>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
// checkType: 'allow-spread-attributes' を指定している場合、
|
|
105
|
+
// 仮にspread attributes内にhrefが含まれていなくてもOKになるため注意
|
|
53
106
|
<XxxAnchor {...args} />
|
|
54
107
|
<XxxLink {...args} any="any" />
|
|
55
108
|
```
|
|
@@ -1,11 +1,73 @@
|
|
|
1
1
|
# smarthr/a11y-clickable-element-has-text
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
ButtonやAnchor,Link コンポーネントなどクリック可能(クリッカブル)な要素にテキストを設定することを促すルールです。
|
|
4
|
+
|
|
5
|
+
## なぜクリッカブルな要素にテキストが必要なのか
|
|
6
|
+
|
|
7
|
+
スクリーンリーダーなどの一部のブラウザで**クリックした対象物が何であるか?という情報が欠落することを防ぐ**目的があります。
|
|
8
|
+
閲覧可能なUI上では十分な情報が存在していても、テキスト読み上げなどでは情報が不足することがあるため、**クリックすることで起きる内容を説明する必要があります。**
|
|
9
|
+
|
|
10
|
+
## 適切なテキストの設定方法について
|
|
11
|
+
|
|
12
|
+
基本的にはchildrenにテキストを設定してください。
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
<button>アクション</button>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
変数を設定した場合もテキストを設定したものとして扱われます
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
<AnyAnchor>{hoge}</AnyAnchor>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 画像、もしくはそれに類する要素の場合
|
|
25
|
+
|
|
26
|
+
画像要素の場合は `alt` 等、代替テキストを設定してください。
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
// 代替テキストを設定した画像を含むためOK
|
|
30
|
+
<AnyLink>
|
|
31
|
+
<XxxImage alt="fuga" />
|
|
32
|
+
</AnyLink>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
svg要素など、画像ではないが画像として扱いたい要素の場合、`role="img"` `aria-label="任意の文字列"` を指定することで**代替テキストが設定された画像**と同等に扱われるようになります。
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
// svgだが代替テキストを設定した画像として扱うためOK
|
|
39
|
+
<AnyButton>
|
|
40
|
+
<svg role="img" aria-label="HOGE">...</svg>
|
|
41
|
+
</AnyButton>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 対象要素がそもそもテキストを内包している場合
|
|
45
|
+
|
|
46
|
+
a, button要素を拡張したコンポーネントがテキストを適切に内包している場合、childrenを設定しない状態で利用すればこのルールはcorrectとして扱います。
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
// childrenが存在しないため、コンポーネントがテキストを内包しているものとして扱うのでOK
|
|
50
|
+
<HogeAnchor />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
またchildrenにわたすコンポーネントがテキストを内包している場合、chilrenに渡すコンポーネントの名称のsuffixが `Text` になっていれば、correctとして扱われます。
|
|
54
|
+
|
|
55
|
+
```jsx
|
|
56
|
+
// children内に XxxText 形式のコンポーネントが渡されているのでOK
|
|
57
|
+
<AnyLink>
|
|
58
|
+
<HogeText />
|
|
59
|
+
</AnyLink>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
childrenにわたすコンポーネントの名称を変更することが難しい場合、lintのoptionとして `componentsWithText` に配列として完全一致するコンポーネント名を設定すれば、そのコンポーネントはテキストを含むものとして扱われます。
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
// componentsWithText: ['Hoge'] を設定している場合
|
|
66
|
+
// Hogeコンポーネントはテキストを含むものとして扱われるのでOK
|
|
67
|
+
<XxxButton>
|
|
68
|
+
<Hoge />
|
|
69
|
+
</XxxButton>
|
|
70
|
+
```
|
|
9
71
|
|
|
10
72
|
## rules
|
|
11
73
|
|
|
@@ -25,24 +87,16 @@
|
|
|
25
87
|
## ❌ Incorrect
|
|
26
88
|
|
|
27
89
|
```jsx
|
|
90
|
+
// テキストとみなされるものがchildrenに存在しないためNG
|
|
28
91
|
<XxxAnchor>
|
|
29
92
|
<Xxx />
|
|
30
93
|
</XxxAnchor>
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
```jsx
|
|
34
94
|
<XxxLink>
|
|
35
95
|
<Yyy />
|
|
36
96
|
</XxxLink>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
```jsx
|
|
40
97
|
<XxxButton>
|
|
41
98
|
<Zzz />
|
|
42
99
|
</XxxButton>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
```jsx
|
|
46
100
|
<XxxAnchor>
|
|
47
101
|
<XxxTextYyyy />
|
|
48
102
|
</XxxAnchor>
|
|
@@ -51,49 +105,40 @@
|
|
|
51
105
|
## ✅ Correct
|
|
52
106
|
|
|
53
107
|
```jsx
|
|
108
|
+
// テキストがchildrenに含まれるためOK
|
|
54
109
|
<XxxAnchor>
|
|
55
110
|
Hoge
|
|
56
111
|
</XxxAnchor>
|
|
57
112
|
```
|
|
58
113
|
```jsx
|
|
114
|
+
// テキスト以外が同時に含まれている場合もOK
|
|
59
115
|
<XxxLink>
|
|
60
116
|
<YyyIcon />
|
|
61
117
|
Fuga
|
|
62
118
|
</XxxLink>
|
|
63
119
|
```
|
|
64
120
|
```jsx
|
|
65
|
-
|
|
66
|
-
<YyyIcon visuallyHiddenText="hoge" />
|
|
67
|
-
</XxxAnchor>
|
|
68
|
-
```
|
|
69
|
-
```jsx
|
|
121
|
+
// 画像・Iconの場合、代替テキストが指定されていればテキストを含むものとして扱われるためOK
|
|
70
122
|
<XxxButton>
|
|
71
123
|
<YyyImage alt="fuga" />
|
|
72
124
|
</XxxButton>
|
|
73
125
|
```
|
|
74
126
|
|
|
75
127
|
```jsx
|
|
128
|
+
// childrenが存在しないコンポーネントの場合、テキストを内包するものとして扱われるためOK
|
|
76
129
|
<YyyAnchor />
|
|
77
130
|
```
|
|
78
131
|
|
|
79
132
|
```jsx
|
|
133
|
+
// childrenに含むコンポーネント名が、TextがsuffixになっているためOK
|
|
80
134
|
<XxxAnchor>
|
|
81
135
|
<XxxText />
|
|
82
136
|
</XxxAnchor>
|
|
83
137
|
```
|
|
84
138
|
|
|
85
139
|
```jsx
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'smarthr/a11y-clickable-element-has-text': [
|
|
89
|
-
'error',
|
|
90
|
-
{
|
|
91
|
-
componentsWithText: ['Hoge'],
|
|
92
|
-
},
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
*/
|
|
96
|
-
|
|
140
|
+
// componentsWithText: ['Hoge'] を設定している場合
|
|
141
|
+
// Hogeコンポーネントはテキストを含むものとして扱われるのでOK
|
|
97
142
|
<XxxButton>
|
|
98
143
|
<Hoge />
|
|
99
144
|
</XxxButton>
|
|
@@ -1,12 +1,117 @@
|
|
|
1
1
|
# smarthr/a11y-form-control-in-form
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
- 適切にマークアップできるようになり、フォームの範囲などがスクリーンリーダーに正しく伝わる
|
|
6
|
-
- 入力要素にfocusした状態でEnterを押せばフォームをsubmitできる
|
|
7
|
-
- inputのrequired属性、pattern属性を利用した入力チェックをブラウザの機能として実行できる
|
|
8
|
-
- smarthr/a11y-input-in-form-control と組み合わせることでより厳密なフォームのマークアップを行えます
|
|
3
|
+
fieldset, Fieldset, FormControl を利用する場合、form要素で囲むことを促すルールです。
|
|
4
|
+
このルールは[smarthr/a11y-input-in-form-control](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-input-in-form-control) と組み合わせることでより厳密なチェックを行えます。
|
|
9
5
|
|
|
6
|
+
## なぜform要素で囲むことを推奨するのか
|
|
7
|
+
|
|
8
|
+
form要素で対象コンポーネントを囲むことで `formの範囲が明確になる` 以外に以下のメリットが存在します。
|
|
9
|
+
|
|
10
|
+
### 入力要素にfocusした状態でEnterキーなどでフォームを送信(submit)できる
|
|
11
|
+
|
|
12
|
+
ブラウザの標準機能として入力要素にfocusした状態でEnterキー、またはそれに準ずるキーボード操作などをした場合、formを送信(submit)することができるようになります。
|
|
13
|
+
この機能は**ブラウザの標準機能として存在するため、submitできることを期待する利用者は多い**ため、form要素を利用したマークアップをすることを推奨しています。
|
|
14
|
+
|
|
15
|
+
状況によっては**ユーザーの誤操作を防止するためEnterなどでsubmitさせたくない**場合もありえますが、この挙動は**ブラウザの標準機能のため、無効にする場合は慎重に判断してください。**
|
|
16
|
+
|
|
17
|
+
### input要素のrequired属性、pattern属性を利用した入力チェックが有効になる
|
|
18
|
+
|
|
19
|
+
input要素のrequired属性、pattern属性はform要素で囲んでいない場合でも設定自体は出来ますが、submit時のチェック自体は発火しません。
|
|
20
|
+
これらの属性はform要素で囲まれる事により、はじめて有効になります。
|
|
21
|
+
|
|
22
|
+
ブラウザの機能として入力チェックを行うため、jsなどで同等の機能を実装した場合と比較して非常に高速なチェックが可能です。
|
|
23
|
+
ぜひ導入を検討してください。
|
|
24
|
+
|
|
25
|
+
## FormControl、Fieldsetを内包するコンポーネントの名称について
|
|
26
|
+
|
|
27
|
+
FormControl・Fieldsetを内包するコンポーネントを命名する場合、名称のsuffixにFormControl、Fieldset、もしくはFormControls, Fieldsetsのいずれかを設定してください。
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
// FormControl、Fieldsetが内包されるコンポーネントの場合、名称のsuffixを
|
|
31
|
+
// FormControl、Fieldset、もしくはFormControls, Fieldsetsのいずれかにする
|
|
32
|
+
const SampleFormControls = () => (
|
|
33
|
+
<>
|
|
34
|
+
<StyledFormControl name="field1" />
|
|
35
|
+
<StyledFormControl name="field2" />
|
|
36
|
+
<StyledFormControl name="field3" />
|
|
37
|
+
</>
|
|
38
|
+
)
|
|
39
|
+
const SampleFieldset = (props) => (
|
|
40
|
+
<Fieldset {...props}>
|
|
41
|
+
<Any />
|
|
42
|
+
</>
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
上記ルールは **条件によってはFormControl, Fieldsetが表示されない場合がある** 際にも適用してください。
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
// AnyFieldsetは条件次第で表示されない場合があるが、それとは無関係にコンポーネント名は `XxxFieldset` とすること
|
|
50
|
+
const SampleFieldset = () => (
|
|
51
|
+
<>
|
|
52
|
+
<Hoge />
|
|
53
|
+
{condition && (
|
|
54
|
+
<AnyFieldset />
|
|
55
|
+
)}
|
|
56
|
+
<Fuga name="field3" />
|
|
57
|
+
</>
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
このような名称にすることで、対象のコンポーネントがFormの入力要素関連のコンポーネントであることが明示され、lintによるチェックが正常に動作するようになります。
|
|
62
|
+
名称を決定するルールは以下の通りです。
|
|
63
|
+
|
|
64
|
+
### Fieldsetを含まず、FormControlを単一で含む場合 -> `XxxFormControl`
|
|
65
|
+
|
|
66
|
+
```jsx
|
|
67
|
+
const SampleFormControl = (props) => (
|
|
68
|
+
<FormControl {...props} title="Sample" />
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### FormControlを含まず、Fieldsetを単一で含む場合 -> `XxxFieldset`
|
|
73
|
+
|
|
74
|
+
```jsx
|
|
75
|
+
const SampleFieldset = (props) => (
|
|
76
|
+
<Fieldset {...props} title="Sample" />
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Fieldsetを含まず、FormControlを複数含む可能性がある場合 -> `XxxFormControls`
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
const SampleFormControls = () => (
|
|
84
|
+
<>
|
|
85
|
+
<AnyFormControl name="field1" />
|
|
86
|
+
<AnyFormControl name="field2" />
|
|
87
|
+
<AnyFormControl name="field3" />
|
|
88
|
+
</>
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### FormControlを含まず、Fieldsetを複数含む可能性がある場合 -> `XxxFieldsets`
|
|
93
|
+
|
|
94
|
+
```jsx
|
|
95
|
+
const SampleFieldsets = () => (
|
|
96
|
+
<>
|
|
97
|
+
<AnyFieldset name="field1" />
|
|
98
|
+
<AnyFieldset name="field2" />
|
|
99
|
+
<AnyFieldset name="field3" />
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### FormControl, Fieldsetが複数混ざって存在する場合 -> `XxxFieldsets`
|
|
105
|
+
|
|
106
|
+
```jsx
|
|
107
|
+
const SampleFieldsets = () => (
|
|
108
|
+
<>
|
|
109
|
+
<AnyFormControl name="field1" />
|
|
110
|
+
<AnyFieldset name="field2" />
|
|
111
|
+
<AnyFormControl name="field3" />
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
```
|
|
10
115
|
|
|
11
116
|
## rules
|
|
12
117
|
|
|
@@ -22,46 +127,84 @@
|
|
|
22
127
|
|
|
23
128
|
```jsx
|
|
24
129
|
// formで囲まれていないためNG
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
130
|
+
const Sample = () => (
|
|
131
|
+
<>
|
|
132
|
+
<FormControl />
|
|
133
|
+
<HogeFieldset />
|
|
134
|
+
<fieldset />
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
// FormControl、Fieldsetを内包するコンポーネントの場合、名称のsuffixが
|
|
141
|
+
// FormControl、Fieldset、もしくはFormControls, Fieldsetsのいずれかである必要があるためNG
|
|
142
|
+
const Sample1 = () => (
|
|
143
|
+
<>
|
|
144
|
+
<StyledFormControl name="field1" />
|
|
145
|
+
<StyledFormControl name="field2" />
|
|
146
|
+
<StyledFormControl name="field3" />
|
|
147
|
+
</>
|
|
148
|
+
)
|
|
149
|
+
const Sample2 = (props) => (
|
|
150
|
+
<Fieldset {...props}>
|
|
151
|
+
<Any />
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
30
154
|
```
|
|
31
155
|
|
|
32
156
|
## ✅ Correct
|
|
33
157
|
|
|
34
158
|
```jsx
|
|
35
|
-
// form
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</Hoge>
|
|
159
|
+
// form要素で囲まれているならOK
|
|
160
|
+
const Sample1 = () => (
|
|
161
|
+
// form要素と推測されるコンポーネントならOK
|
|
162
|
+
<StyledForm>
|
|
163
|
+
<FormControl />
|
|
164
|
+
<HogeFieldset />
|
|
165
|
+
<fieldset />
|
|
166
|
+
</StyledForm>
|
|
167
|
+
)
|
|
168
|
+
const Sample2 = () => (
|
|
169
|
+
// as, forwardedAsでform要素にされているコンポーネントの場合もOK
|
|
170
|
+
<Hoge as="form">
|
|
171
|
+
<FormControl />
|
|
172
|
+
<HogeFieldset />
|
|
173
|
+
<fieldset />
|
|
174
|
+
</Hoge>
|
|
175
|
+
)
|
|
176
|
+
```
|
|
51
177
|
|
|
178
|
+
```jsx
|
|
52
179
|
// Dialogの場合、FormDialog・RemoteTriggerFormDialogで囲めばOK
|
|
53
|
-
const
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
180
|
+
const SampleFormDialog = () => (
|
|
181
|
+
<FormDialog>
|
|
182
|
+
<FugaFormControl />
|
|
183
|
+
</FormDialog>
|
|
184
|
+
)
|
|
185
|
+
const SampleRemoteTriggerFormDialog = () => (
|
|
186
|
+
<RemoteTriggerAnyFormDialog>
|
|
187
|
+
<FugaFormControl />
|
|
188
|
+
</RemoteTriggerAnyFormDialog>
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```jsx
|
|
193
|
+
// FormControl、Fieldsetを内包するコンポーネントの場合、名称のsuffixが
|
|
194
|
+
// FormControl、Fieldset、もしくはFormControls, Fieldsetsのいずれかの場合OK
|
|
195
|
+
const SampleFormControls = () => (
|
|
196
|
+
<>
|
|
197
|
+
<StyledFormControl name="field1" />
|
|
198
|
+
<StyledFormControl name="field2" />
|
|
199
|
+
<StyledFormControl name="field3" />
|
|
200
|
+
</>
|
|
201
|
+
)
|
|
202
|
+
const SampleFieldset = (props) => (
|
|
203
|
+
<Fieldset {...props}>
|
|
204
|
+
<Any />
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// コンポーネント名を上記の様に調整することで
|
|
209
|
+
// これらのコンポーネントを利用する別コンポーネントでも正しくチェックが行えます
|
|
67
210
|
```
|
|
@@ -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;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
const rule = require('../rules/a11y-delegate-element-has-role-presentation');
|
|
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 defaultInteractiveMessage = '((B|b)utton(s)?|(Check|Combo)(B|b)ox(es|s)?|(Date(timeLocal)?|Time|Month|Wareki)Picker(s)?|(I|i)nput(File)?(s)?|(S|s)elect(s)?|(T|t)extarea(s)?|(ActionDialogWith|RemoteDialog)Trigger(s)?|AccordionPanel(s)?|^a|Anchor|Link(s)?|DropZone(s)?|Field(S|s)et(s)?|FilterDropdown(s)?|(F|f)orm(Control|Group|Dialog)?(s)?|Pagination(s)?|RadioButton(Panel)?(s)?|RemoteTrigger(.+)Dialog(s)?|RightFixedNote(s)?|SegmentedControl(s)?|SideNav(s)?|Switch(s)?|TabItem(s)?)$'
|
|
15
|
-
const defaultInteractiveRegex = `/(${defaultInteractiveMessage})/`
|
|
16
|
-
const messageNonInteractiveEventHandler = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => {
|
|
17
|
-
const onAttrsText = onAttrs.join(', ')
|
|
18
|
-
|
|
19
|
-
return `${nodeName} に${onAttrsText}を設定するとブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
|
|
20
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-delegate-element-has-role-presentation
|
|
21
|
-
- 方法1: ${nodeName}がinput、buttonやaなどのインタラクティブな要素の場合、コンポーネント名の末尾をインタラクティブなコンポーネントであることがわかる名称に変更してください
|
|
22
|
-
- "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに差し替える、もしくは名称を変更してください
|
|
23
|
-
- 方法2: ${onAttrsText} がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
|
|
24
|
-
- 属性名を"/^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/"に一致しないものに変更してください
|
|
25
|
-
- 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
|
|
26
|
-
- 方法3: インタラクティブな親要素、もしくは子要素が存在する場合、直接${onAttrsText}を設定することを検討してください
|
|
27
|
-
- 方法4: インタラクティブな親要素、もしくは子要素が存在しない場合、インタラクティブな要素を必ず持つようにマークアップを修正後、${onAttrsText}の設定要素を検討してください
|
|
28
|
-
- 方法5: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください
|
|
29
|
-
- 'role="presentation"' を設定した要素はマークアップとしての意味がなくなるため、div・span などマークアップとしての意味を持たない要素に設定してください
|
|
30
|
-
- 'role="presentation"' を設定する適切な要素が存在しない場合、div、またはspanでイベントが発生する要素を囲んだ上でrole属性を設定してください`
|
|
31
|
-
}
|
|
32
|
-
const messageRolePresentationNotHasInteractive = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => `${nodeName}に 'role="presentation"' が設定されているにも関わらず、子要素にinput、buttonやaなどのインタラクティブな要素が見つからないため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
|
|
33
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-delegate-element-has-role-presentation
|
|
34
|
-
- 方法1: 子要素にインタラクティブな要素が存在するにも関わらずこのエラーが表示されている場合、子要素の名称を変更してください
|
|
35
|
-
- "${interactiveComponentRegex}" の正規表現にmatchするよう、インタラクティブな子要素全てを差し替える、もしくは名称を変更してください
|
|
36
|
-
- 方法2: ${nodeName}自体がインタラクティブな要素の場合、'role="presentation"'を削除した上で名称を変更してください
|
|
37
|
-
- "${interactiveComponentRegex}" の正規表現にmatchするよう、${nodeName}の名称を変更してください
|
|
38
|
-
- 方法3: 子要素にインタラクティブな要素が存在し、${onAttrs.join(', ')}全属性をそれらの要素に移動させられる場合、'role="presentation"'を消した上で実施してください`
|
|
39
|
-
const messageInteractiveHasRolePresentation = (nodeName, interactiveComponentRegex = defaultInteractiveRegex) => `${nodeName}はinput、buttonやaなどのインタラクティブな要素にもかかわらず 'role="presentation"' が設定されているため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
|
|
40
|
-
- 方法1: 'role="presentation"' を削除してください
|
|
41
|
-
- 方法2: ${nodeName}の名称を "${interactiveComponentRegex}" とマッチしない名称に変更してください`
|
|
42
|
-
|
|
43
|
-
ruleTester.run('smarthr/a11y-delegate-element-has-role-presentation', rule, {
|
|
44
|
-
valid: [
|
|
45
|
-
{ code: '<Input />' },
|
|
46
|
-
{ code: '<HogeForm>any</HogeForm>' },
|
|
47
|
-
{ code: '<FugaButton>any</FugaButton>' },
|
|
48
|
-
{ code: '<Link />' },
|
|
49
|
-
{ code: '<div onClick={any} role="presentation"><Link /></div>' },
|
|
50
|
-
{ code: '<div onClick={any} role="presentation"><button /></div>' },
|
|
51
|
-
{ code: '<Wrapper onClick={any} role="presentation"><Link /></Wrapper>' },
|
|
52
|
-
{ code: '<Wrapper onClick={any} role="presentation"><Hoge /></Wrapper>', options: [{ additionalInteractiveComponentRegex: ['^Hoge$'] }] },
|
|
53
|
-
{ code: '<Wrapper onClick={any} role="presentation"><any><Link /></any></Wrapper>' },
|
|
54
|
-
{ code: '<Wrapper onClick={any} role="presentation">{any && <AnyButton />}</Wrapper>' },
|
|
55
|
-
{ code: '<Wrapper onClick={any} role="presentation">{any || <AnyButtons />}</Wrapper>' },
|
|
56
|
-
{ code: '<Wrapper onClick={any} role="presentation">{any1 && (any2 || any3) && <AnyButton />}</Wrapper>' },
|
|
57
|
-
{ code: '<Wrapper onClick={any} role="presentation">{any ? <AnyButton /> : null}</Wrapper>' },
|
|
58
|
-
{ code: '<Wrapper onClick={any} role="presentation">{any1 ? (any2 ? <HogeLink /> : null) : null}</Wrapper>' },
|
|
59
|
-
{ code: '<Wrapper onClick={any} role="presentation">{any ? null : (hoge ? <AnyLink /> : null)}</Wrapper>' },
|
|
60
|
-
{ code: '<Wrapper onClick={any} role="slider">Hoge</Wrapper>' },
|
|
61
|
-
{ code: '<Wrapper onSubmit={any} as="form" />' },
|
|
62
|
-
{ code: '<Wrapper onSubmit={any} forwardedAs="fieldset">any</Wrapper>' },
|
|
63
|
-
],
|
|
64
|
-
invalid: [
|
|
65
|
-
{ code: '<Input role="presentation" />', errors: [ { message: messageInteractiveHasRolePresentation('Input') } ] },
|
|
66
|
-
{ code: '<HogeForm role="presentation">any</HogeForm>', errors: [ { message: messageInteractiveHasRolePresentation('HogeForm') } ] },
|
|
67
|
-
{ code: '<FugaButton role="presentation">any</FugaButton>', errors: [ { message: messageInteractiveHasRolePresentation('FugaButton') } ] },
|
|
68
|
-
{ code: '<Link role="presentation" />', errors: [ { message: messageInteractiveHasRolePresentation('Link') } ] },
|
|
69
|
-
{ code: '<div onClick={any} onSubmit={any2} role="presentation"><Hoge /></div>', errors: [ { message: messageRolePresentationNotHasInteractive('div', ['onClick', 'onSubmit']) } ] },
|
|
70
|
-
{ code: '<div onClick={any}><Link /></div>', errors: [ { message: messageNonInteractiveEventHandler('div', ['onClick']) } ] },
|
|
71
|
-
{ code: '<Wrapper onClick={any}><Link /></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick']) } ] },
|
|
72
|
-
{ code: '<Wrapper onSubmit={any}><Hoge /></Wrapper>', options: [{ additionalInteractiveComponentRegex: ['^Hoge$'] }], errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onSubmit'], `/(${defaultInteractiveMessage}|^Hoge$)/`) } ] },
|
|
73
|
-
{ code: '<Wrapper onClick={any} onChange={anyany}><any><Link /></any></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick', 'onChange']) } ] },
|
|
74
|
-
{ code: '<Wrapper onClick={any}>{any ? null : (hoge ? <AnyLink /> : null)}</Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick']) } ] },
|
|
75
|
-
],
|
|
76
|
-
});
|