eslint-plugin-smarthr 5.0.0 → 6.0.1

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,25 @@
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.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.0.0...eslint-plugin-smarthr-v6.0.1) (2026-01-28)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **best-practice-for-interactive-element:** as属性でインタラクティブなコンポーネントに変更した場合、onXxxのチェックが誤検知する問題を修正 ([#1019](https://github.com/kufu/tamatebako/issues/1019)) ([8ba4343](https://github.com/kufu/tamatebako/commit/8ba4343b87b8acb8fee374946dd18c848839e6a9))
11
+
12
+ ## [6.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v5.0.0...eslint-plugin-smarthr-v6.0.0) (2026-01-28)
13
+
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ * **a11y-delegate-element-has-role-presentation:** ルールを削除します ([#1015](https://github.com/kufu/tamatebako/issues/1015))
18
+
19
+ ### Features
20
+
21
+ * **a11y-delegate-element-has-role-presentation:** ルールを削除します ([#1015](https://github.com/kufu/tamatebako/issues/1015)) ([4c5c7ab](https://github.com/kufu/tamatebako/commit/4c5c7ab43ab7a52f749963aa7b7cb359e6f623d5))
22
+ * best-practice-for-interactive-elementを追加 ([#1014](https://github.com/kufu/tamatebako/issues/1014)) ([e0a171b](https://github.com/kufu/tamatebako/commit/e0a171b056d8dbe775e3697633136e60c84afe5d))
23
+
5
24
  ## [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
25
 
7
26
 
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": "5.0.0",
3
+ "version": "6.0.1",
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": "67f6c84cfcabac6fd2550133ae18e351a443a16c"
40
+ "gitHead": "87bf718cd19e5f353c0b93cedcc2ca8ba250ba7c"
41
41
  }
@@ -1,14 +1,56 @@
1
1
  # smarthr/a11y-anchor-has-href-attribute
2
2
 
3
- - a, Anchor, Link コンポーネントに href 属性を設定することを促すルールです
4
- - href が設定されていないanchor要素は `遷移先が存在しない無効化されたリンク` という扱いになります
5
- - URLの変更を行わない場合、責務としても a より button が適切です
6
- - URL遷移を行う場合、hrefが設定されていないとキーボード操作やコンテキストメニューからの遷移ができなくなります
7
- - これらの操作は href属性を参照します
8
- - 無効化されたリンクであることを表したい場合 `href={undefined}` を設定してください
9
- - checkTypeオプションに 'allow-spread-attributes' を指定することで spread attributeが設定されている場合はcorrectに出来ます
10
- - react-router-dom packageを利用している場合、a要素にto属性が指定されている場合、href属性が指定されているものとして許容します
11
- - next/link コンポーネント直下のa要素にhref属性が指定されていないことを許容します
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
- // checkType: 'always'
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
- // nextを利用している場合
47
- <Link href={hoge}><a>any</a></Link>
48
-
49
- // react-router-domを利用している場合
93
+ ```jsx
94
+ // react-routerを利用している場合、かつto属性を設定しているためOK
50
95
  <Link to={hoge}>any</Link>
96
+ ```
51
97
 
52
- // checkType: 'allow-spread-attributes'
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
- - ButtonやAnchor,Link コンポーネントにテキスト要素が設定されていない場合、スクリーンリーダーで押したものが何だったのかわからない等の問題が発生する可能性を防ぐルールです
4
- - a要素やbutton要素の中身にtextがあることを担保するルール
5
- - 画像要素の場合は `visuallyHiddenText`や `alt`等代替テキストを設定する
6
- - SVGの場合はrole="img" と aria-labelを設定する
7
- - linkとかanchorのchildrenを含まない要素はチェックしない
8
- - 例) `<YyyAnchor />`
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
- <XxxAnchor>
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
- rules: {
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
- - fieldset, Fieldset, FormControl を利用する場合、form要素で囲むことを促すルールです
4
- - form要素で囲むことで以下のようなメリットがあります
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 AnyComponent = <>
26
- <FormControl />
27
- <HogeFieldset />
28
- <fieldset />
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で囲まれているためOK
36
- const AnyComponent = <StyledForm>
37
- <FormControl />
38
- <HogeFieldset />
39
- <fieldset />
40
- </StyledForm>
41
- const AnyComponent = <Hoge as="form">
42
- <FormControl />
43
- <HogeFieldset />
44
- <fieldset />
45
- </Hoge>
46
- const AnyComponent = <Hoge forwardedAs="form">
47
- <FormControl />
48
- <HogeFieldset />
49
- <fieldset />
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 AnyComponent = <FormDialog>
54
- <FugaFormControl />
55
- </FormDialog>
56
- const AnyComponent = <RemoteTriggerAnyFormDialog>
57
- <FugaFormControl />
58
- </RemoteTriggerAnyFormDialog>
59
-
60
- // 対象のFormControl、Fieldsetがコンポーネントの一要素であり、その親コンポーネント名がFormControl、もしくはFieldsetの場合OK
61
- const AnyFormControl = <>
62
- <StyledFormControl />
63
- </>
64
- const AnyFieldset = <>
65
- <StyledFieldset />
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,90 @@
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 ELEMENT_HAS_ROLE_ATTRIBUTE = 'JSXOpeningElement:has(JSXAttribute[name.name="role"])'
34
+ const AS_FORM_PART_ATTRIBUTE = 'JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^f(orm|ieldset)$/]'
35
+
36
+ const SCHEMA = [
37
+ {
38
+ type: 'object',
39
+ properties: {
40
+ additionalInteractiveComponentRegex: { type: 'array', items: { type: 'string' } },
41
+ },
42
+ additionalProperties: false,
43
+ }
44
+ ]
45
+
46
+ /**
47
+ * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
48
+ */
49
+ module.exports = {
50
+ meta: {
51
+ type: 'problem',
52
+ schema: SCHEMA,
53
+ },
54
+ create(context) {
55
+ const options = context.options[0]
56
+ const interactiveComponentRegex = new RegExp(`(${INTERACTIVE_COMPONENT_NAMES}${options?.additionalInteractiveComponentRegex ? `|${options.additionalInteractiveComponentRegex.join('|')}` : ''})`)
57
+ const targetNameProp = `[name.name=${interactiveComponentRegex}]`
58
+
59
+ return {
60
+ [`${ELEMENT_HAS_ROLE_ATTRIBUTE}${targetNameProp}`]: (node) => {
61
+ context.report({
62
+ node,
63
+ message: `${node.name.name}にrole属性は指定しないでください。
64
+ - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
65
+ });
66
+ },
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属性は指定しないでください。
71
+ - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
72
+ });
73
+ },
74
+ [`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE})):has(JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}])`]: (node) => {
75
+ context.report({
76
+ node,
77
+ message: `${node.name.name}にデフォルトで用意されているonXxx形式の属性は設定しないでください
78
+ - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element
79
+ - 対応方法1: 対象の属性がコンポーネント内の特定のインタラクティブな要素に設定される場合、名称を具体的なものに変更してください
80
+ - 属性名を"${INTERACTIVE_ON_REGEX}"に一致しないものに変更してください
81
+ - 例: 対象コンポーネント内に '追加ボタン' が存在する場合、'onClick' という属性名を 'onClickAddButton' に変更する
82
+ - 対応方法2: 対象の属性が設定されているコンポーネントがインタラクティブなコンポーネントの場合、名称を調整してください
83
+ - "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに変更、もしくは名称を調整してください
84
+ - 対応方法3: インタラクティブな親要素、もしくは子要素が存在する場合、onXxx属性を移動して設定することを検討してください`,
85
+ });
86
+ }
87
+ };
88
+ },
89
+ };
90
+ module.exports.schema = SCHEMA;
@@ -7,7 +7,7 @@
7
7
 
8
8
  # spread syntax を通常の属性より後に記述した場合に発生する問題
9
9
 
10
- spread syntaxより先に通常の属性を記述した場合、** 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
- 上記例の場合、** Props型がidを含むか? 含む場合必須か?などの確認 ** が必要になります。
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,74 @@
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
+ { code: `<Stack as="form" onSubmit={onSubmit} />` },
63
+ ],
64
+ invalid: [
65
+ { code: `<button role="presentation">...</button>`, errors: [{ message: interactiveError('button') }] },
66
+ { code: `<Hoge as="form" role="menu" />`, errors: [{ message: `<Hoge as="form">にrole属性は指定しないでください。
67
+ - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element` }] },
68
+ { code: `<FormControl role="menu" />`, errors: [{ message: interactiveError('FormControl') }] },
69
+ { code: `<InteractiveComponent role="group">...</InteractiveComponent>`, options: [{ additionalInteractiveComponentRegex: ['^Interactive'] }], errors: [{ message: interactiveError('InteractiveComponent') }] },
70
+ { code: `<CrewDetail onChange={onChange} />`, errors: [{ message: uninteractiveError('CrewDetail') }] },
71
+ { code: `<Stack onSubmit={onSubmit} />`, errors: [{ message: uninteractiveError('Stack') }] },
72
+ ]
73
+ })
74
+
@@ -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
- });