eslint-plugin-smarthr 0.3.24 → 0.3.26

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,21 @@
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
+ ### [0.3.26](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.25...v0.3.26) (2024-01-24)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * a11y-prohibit-useless-sectioning-fragmentの属性チェックのバグを修正 ([#107](https://github.com/kufu/eslint-plugin-smarthr/issues/107)) ([b42bdd9](https://github.com/kufu/eslint-plugin-smarthr/commit/b42bdd9e11258a6a32e13647c0c764065b5bac64))
11
+
12
+ ### [0.3.25](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.24...v0.3.25) (2024-01-23)
13
+
14
+
15
+ ### Features
16
+
17
+ * a11y-input-in-form-controlを追加する ([#98](https://github.com/kufu/eslint-plugin-smarthr/issues/98)) ([fb7e77d](https://github.com/kufu/eslint-plugin-smarthr/commit/fb7e77d8c3ac73f57425d094a240d998ff78a51d))
18
+ * a11y-prohibit-useless-sectioning-fragmentを追加する ([#106](https://github.com/kufu/eslint-plugin-smarthr/issues/106)) ([994c040](https://github.com/kufu/eslint-plugin-smarthr/commit/994c04027892a5fe50fb71ba8b5941401f12c02c))
19
+
5
20
  ### [0.3.24](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.23...v0.3.24) (2024-01-19)
6
21
 
7
22
  ### [0.3.23](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.22...v0.3.23) (2024-01-16)
package/README.md CHANGED
@@ -6,10 +6,11 @@
6
6
  - [a11y-heading-in-sectioning-content](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-heading-in-sectioning-content)
7
7
  - [a11y-image-has-alt-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-image-has-alt-attribute)
8
8
  - [a11y-input-has-name-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-has-name-attribute)
9
+ - [a11y-input-in-form-control](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-in-form-control)
9
10
  - [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
10
11
  - [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
11
12
  - [best-practice-for-date](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-date)
12
- - [best-practice-for-remote-trigger-action-dialog](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-remote-trigger-action-dialog)
13
+ - [best-practice-for-remote-trigger-dialog](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-remote-trigger-dialog)
13
14
  - [format-import-path](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/format-import-path)
14
15
  - [format-translate-component](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/format-translate-component)
15
16
  - [jsx-start-with-spread-attributes](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/jsx-start-with-spread-attributes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -15,7 +15,7 @@ const EXPECTED_NAMES = {
15
15
  const TARGET_TAG_NAME_REGEX = new RegExp(`(${Object.keys(EXPECTED_NAMES).join('|')})`)
16
16
  const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/
17
17
  const INPUT_TAG_REGEX = /(i|I)nput$/
18
- const RADIO_BUTTON_REGEX = /RadioButton$/
18
+ const RADIO_BUTTON_REGEX = /RadioButton(Panel)?$/
19
19
 
20
20
  const findNameAttr = (a) => a?.name?.name === 'name'
21
21
  const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
@@ -0,0 +1,103 @@
1
+ # smarthr/a11y-input-in-form-control
2
+
3
+ - 入力要素をsmarthr-ui/FormControl、もしくはsmarthr-ui/Fieldsetで囲むことを促すルールです
4
+ - SectioningContent + Heading でのマークアップではなく、FormControl・Fieldsetを使うように促します
5
+ - FormControl・Fieldsetを使うことでlabelと入力要素が適切に紐づいたり、RadioButtonなどが適切にグルーピングされるようになり、アクセシビリティ的メリットが得られます
6
+
7
+ ## rules
8
+
9
+ ```js
10
+ {
11
+ rules: {
12
+ 'smarthr/a11y-input-in-form-control': [
13
+ 'error', // 'warn', 'off',
14
+ // {
15
+ // additionalInputComponents: ['^HogeSelector$'], // 単一の入力要素として扱いたいコンポーネント名を正規表現で入力する
16
+ // additionalMultiInputComponents: ['Inputs$'], // 複数の入力要素として扱いたいコンポーネント名を正規表現で入力する
17
+ // }
18
+ ]
19
+ },
20
+ }
21
+ ```
22
+
23
+ ## ❌ Incorrect
24
+
25
+ ```jsx
26
+ // FormControlで囲まれていないためNG
27
+ <Input />
28
+
29
+ // FormControl・FieldsetではなくSectionでマークアップされているためNG
30
+ <Section>
31
+ <Heading />
32
+ <Select />
33
+ </Section>
34
+
35
+ // RadioButton, CheckBoxはFieldsetでグルーピングする必要があるためNG
36
+ <FormControl title="any heading">
37
+ <RadioButton>{a.label}</RadioButton>
38
+ </FormControl>
39
+
40
+ // FormControlが複数の入力要素を持ってしまっているのでNG
41
+ <FormControl title="any heading">
42
+ <Input />
43
+ <ComboBox />
44
+ </FormControl>
45
+
46
+
47
+ // FormControlがネストしてしまっているのでNG
48
+ <FormControl>
49
+ <SubFormControl>
50
+ <CheckBox />
51
+ </SubFormControl>
52
+ </FormControl>
53
+
54
+
55
+ // Fieldsetには role="group" がデフォルトで設定されているのでNG
56
+ <Fieldset role="group" />
57
+ ```
58
+
59
+ ## ✅ Correct
60
+
61
+ ```jsx
62
+ <FormControl title="any heading">
63
+ <Input />
64
+ </FormControl>
65
+
66
+ <Fieldset title="any heading">
67
+ {radios.map((a) => (
68
+ <RadioButton>{a.label}</RadioButton>
69
+ ))}
70
+ </Fieldset>
71
+
72
+ <FormControl title="any heading" role="group">
73
+ <DatePicker />
74
+ ~
75
+ <DatePicker />
76
+ </Fieldset>
77
+
78
+ <Fieldset title="any heading">
79
+ <FormControl role="group">
80
+ <DatePicker />
81
+ ~
82
+ <DatePicker />
83
+ </FormControl>>
84
+ </Fieldset>
85
+
86
+ // childrenを持たないFieldset、FormControlは入力要素として扱うためOK
87
+ <Fieldset title="any heading">
88
+ <HogeFieldset />
89
+ <FugaFormControl />
90
+ </Fieldset>
91
+
92
+ // Sectionより先にFormControl・Fieldsetで囲んでいるためOK
93
+ <Section>
94
+ <Heading />
95
+ <FormControl title="any heading">
96
+ <Input />
97
+ </FormControl>
98
+ </Section>
99
+
100
+ // smarthr-ui/CheckBox はlabelを含むため、なんの入力要素かが単独で伝えられるので
101
+ // FormControl・Fieldsetで囲む必要はない (囲んでも問題はない)
102
+ <CheckBox />
103
+ ```
@@ -0,0 +1,441 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
+
3
+ const EXPECTED_LABELED_INPUT_NAMES = {
4
+ 'RadioButton$': '(RadioButton)$',
5
+ 'RadioButtonPanel$': '(RadioButtonPanel)$',
6
+ 'Check(B|b)ox$': '(CheckBox)$',
7
+ }
8
+ const EXPECTED_INPUT_NAMES = {
9
+ '(I|^i)nput$': '(Input)$',
10
+ 'SearchInput$': '(SearchInput)$',
11
+ '(T|^t)extarea$': '(Textarea)$',
12
+ '(S|^s)elect$': '(Select)$',
13
+ 'InputFile$': '(InputFile)$',
14
+ 'Combo(b|B)ox$': '(ComboBox)$',
15
+ 'DatePicker$': '(DatePicker)$',
16
+ ...EXPECTED_LABELED_INPUT_NAMES,
17
+ }
18
+
19
+ const EXPECTED_FORM_CONTROL_NAMES = {
20
+ '(FormGroup)$': '(FormGroup)$',
21
+ '(FormControl)$': '(FormControl)$',
22
+ '((F|^f)ieldset)$': '(Fieldset)$',
23
+ }
24
+
25
+ const EXPECTED_NAMES = {
26
+ ...EXPECTED_INPUT_NAMES,
27
+ ...EXPECTED_FORM_CONTROL_NAMES,
28
+ '(A|^a)rticle$': '(Article)$',
29
+ '(A|^a)side$': '(Aside)$',
30
+ '(N|^n)av$': '(Nav)$',
31
+ '(S|^s)ection$': '(Section)$',
32
+ 'Center$': '(Center)$',
33
+ 'Reel$': '(Reel)$',
34
+ 'Sidebar$': '(Sidebar)$',
35
+ 'Stack$': '(Stack)$',
36
+
37
+ }
38
+
39
+ const UNEXPECTED_NAMES = EXPECTED_NAMES
40
+
41
+ const FORM_CONTROL_INPUTS_REGEX = new RegExp(`(${Object.keys(EXPECTED_INPUT_NAMES).join('|')})`)
42
+ const LABELED_INPUTS_REGEX = new RegExp(`(${Object.keys(EXPECTED_LABELED_INPUT_NAMES).join('|')})`)
43
+ const SEARCH_INPUT_REGEX = /SearchInput$/
44
+ const INPUT_REGEX = /(i|I)nput$/
45
+ const RADIO_BUTTONS_REGEX = /RadioButton(Panel)?$/
46
+ const CHECKBOX_REGEX = /Check(B|b)ox?$/
47
+ const SELECT_REGEX = /(S|s)elect?$/
48
+ const FROM_CONTROLS_REGEX = new RegExp(`(${Object.keys(EXPECTED_FORM_CONTROL_NAMES).join('|')})`)
49
+ const FORM_CONTROL_REGEX = /(Form(Control|Group))$/
50
+ const FIELDSET_REGEX = /Fieldset$/
51
+ const DIALOG_REGEX = /Dialog(WithTrigger)?$/
52
+ const SECTIONING_REGEX = /(((A|^a)(rticle|side))|(N|^n)av|(S|^s)ection|^SectioningFragment)$/
53
+ const BARE_SECTIONING_TAG_REGEX = /^(article|aside|nav|section)$/
54
+ const LAYOUT_COMPONENT_REGEX = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
55
+ const AS_REGEX = /^(as|forwardedAs)$/
56
+
57
+ const IGNORE_INPUT_CHECK_PARENT_TYPE = /^(Program|ExportNamedDeclaration)$/
58
+
59
+ const findRoleGroup = (a) => a.name?.name === 'role' && a.value.value === 'group'
60
+ const findAsSectioning = (a) => a.name?.name.match(AS_REGEX) && a.value.value.match(BARE_SECTIONING_TAG_REGEX)
61
+
62
+ const SCHEMA = [
63
+ {
64
+ type: 'object',
65
+ properties: {
66
+ additionalInputComponents: { type: 'array', items: { type: 'string' }, default: [] },
67
+ additionalMultiInputComponents: { type: 'array', items: { type: 'string' }, default: [] },
68
+ },
69
+ additionalProperties: false,
70
+ }
71
+ ]
72
+
73
+ module.exports = {
74
+ meta: {
75
+ type: 'problem',
76
+ schema: SCHEMA,
77
+ },
78
+ create(context) {
79
+ const option = context.options[0] || {}
80
+ const additionalInputComponents = option.additionalInputComponents?.length > 0 ? new RegExp(`(${option.additionalInputComponents.join('|')})`) : null
81
+ const additionalMultiInputComponents = option.additionalMultiInputComponents?.length > 0 ? new RegExp(`(${option.additionalMultiInputComponents.join('|')})`) : null
82
+
83
+ const checkAdditionalInputComponents = (name) => additionalInputComponents && name.match(additionalInputComponents)
84
+ const checkAdditionalMultiInputComponents = (name) => additionalMultiInputComponents && name.match(additionalMultiInputComponents)
85
+
86
+ let formControls = []
87
+ let checkboxFormControls = []
88
+
89
+ return {
90
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
91
+ JSXOpeningElement: (node) => {
92
+ const nodeName = node.name.name || '';
93
+ const isFormControlInput = nodeName.match(FORM_CONTROL_INPUTS_REGEX)
94
+ const isAdditionalMultiInput = checkAdditionalMultiInputComponents(nodeName)
95
+
96
+ if (isFormControlInput || isAdditionalMultiInput || checkAdditionalInputComponents(nodeName)) {
97
+ let isInMap = false
98
+
99
+ // HINT: 検索ボックスの場合、UIの関係上labelを設定出来ないことが多い & smarthr-ui/SearchInputはa11y対策してあるため無視
100
+ if (nodeName.match(SEARCH_INPUT_REGEX)) {
101
+ return
102
+ }
103
+
104
+ const isPureInput = nodeName.match(INPUT_REGEX)
105
+ let isPseudoLabel = false
106
+ let isTypeRadio = false
107
+ let isTypeCheck = false
108
+
109
+ if (isFormControlInput) {
110
+ for (const i of node.attributes) {
111
+ if (i.name) {
112
+ // HINT: idが設定されている場合、htmlForでlabelと紐づく可能性が高いため無視する
113
+ // HINT: titleが設定されている場合、なんの入力要素かはわかるため無視する
114
+ switch (i.name.name) {
115
+ case 'id':
116
+ case 'title':
117
+ isPseudoLabel = true
118
+ break
119
+ case 'type':
120
+ switch (i.value.value) {
121
+ case 'radio':
122
+ isTypeRadio = true
123
+ break
124
+ case 'checkbox':
125
+ isTypeCheck = true
126
+ break
127
+ }
128
+
129
+ break
130
+ }
131
+ }
132
+ }
133
+ }
134
+ const isRadio = (isPureInput && isTypeRadio) || nodeName.match(RADIO_BUTTONS_REGEX);
135
+ const isCheckbox = !isRadio && (isPureInput && isTypeCheck || nodeName.match(CHECKBOX_REGEX));
136
+
137
+ const wrapComponentName = isRadio ? 'Fieldset' : 'FormControl'
138
+ const search = (n) => {
139
+ switch (n.type) {
140
+ case 'JSXElement': {
141
+ const openingElement = n.openingElement
142
+ const name = openingElement.name.name
143
+
144
+ if (name) {
145
+ if (name.match(FROM_CONTROLS_REGEX)) {
146
+ const hit = formControls.includes(n)
147
+
148
+ if (!hit) {
149
+ formControls.push(n)
150
+
151
+ if (isCheckbox) {
152
+ checkboxFormControls.push(n)
153
+ }
154
+ }
155
+
156
+ const isMultiInput = isAdditionalMultiInput || hit || isInMap
157
+ const matcherFormControl = name.match(FORM_CONTROL_REGEX)
158
+
159
+ if (matcherFormControl) {
160
+ if (isRadio || isCheckbox && (isInMap || hit && checkboxFormControls.includes(n))) {
161
+ const convertComp = isRadio ? 'smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel' : 'smarthr-ui/Checkbox'
162
+
163
+ context.report({
164
+ node: n,
165
+ message: `${name} が ${nodeName} を含んでいます。smarthr-ui/${matcherFormControl[1]} を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。${isRadio ? `
166
+ - Fieldsetで同じname属性のラジオボタン全てを囲むことで正しくグループ化され、適切なタイトル・説明を追加出来ます` : ''}${isPureInput ? `
167
+ - 可能なら${nodeName}は${convertComp}への変更を検討してください。難しい場合は ${nodeName} と結びつくlabel要素が必ず存在するよう、マークアップする必要があることに注意してください。` : ''}`,
168
+ });
169
+ } else if (isMultiInput && !openingElement.attributes.find(findRoleGroup)) {
170
+ context.report({
171
+ node: n,
172
+ message: `${name} が複数の入力要素を含んでいます。ラベルと入力要素の紐づけが正しく行われない可能性があるため、以下の方法のいずれかで修正してください。
173
+ - 方法1: 入力要素ごとにラベルを設定できる場合、${name}をsmarthr-ui/Fieldset、もしくはそれを拡張したコンポーネントに変更した上で、入力要素を一つずつsmarthr-ui/FormControlで囲むようにマークアップを変更してください
174
+ - 方法2: 郵便番号や電話番号など、本来一つの概念の入力要素を分割して複数の入力要素にしている場合、一つの入力要素にまとめることを検討してください
175
+ - コピーアンドペーストがしやすくなる、ブラウザの自動補完などがより適切に反映されるなど多大なメリットがあります
176
+ - 方法3: ${name} が smarthr-ui/${matcherFormControl[1]}、もしくはそれを拡張しているコンポーネントではない場合、名称を ${FROM_CONTROLS_REGEX} にマッチしないものに変更してください
177
+ - 方法4: 上記方法のいずれも対応出来ない場合、${name} に 'role="group"' 属性を設定してください`,
178
+ });
179
+ }
180
+ // HINT: 擬似的にラベルが設定されている場合、無視する
181
+ } else if (!isRadio && !isCheckbox && !isPseudoLabel) {
182
+ context.report({
183
+ node: n,
184
+ message: `${name} が ラベルを持たない入力要素(${nodeName})を含んでいます。入力要素が何であるかを正しく伝えるため、以下の方法のいずれかで修正してください。
185
+ - 方法1: ${name} を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
186
+ - 方法2: ${nodeName} がlabel要素を含むコンポーネントである場合、名称を${FORM_CONTROL_REGEX}にマッチするものに変更してください
187
+ - smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
188
+ - 方法3: ${nodeName} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を${LABELED_INPUTS_REGEX}にマッチするものに変更してください
189
+ - smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
190
+ - 方法4: ${name} が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を ${FIELDSET_REGEX} にマッチしないものに変更してください`,
191
+ });
192
+ }
193
+
194
+ return
195
+ } else {
196
+ const isSection = name.match(SECTIONING_REGEX)
197
+ const layoutSectionAttribute = !isSection && name.match(LAYOUT_COMPONENT_REGEX) && openingElement.attributes.find(findAsSectioning)
198
+
199
+ if (isSection || layoutSectionAttribute) {
200
+ // HINT: smarthr-ui/CheckBoxはlabelを単独で持つため、FormControl系でラップをする必要はない
201
+ // HINT: 擬似的にラベルが設定されている場合、無視する
202
+ if (!isCheckbox && !isPseudoLabel) {
203
+ const actualName = isSection ? name : `<${name} ${layoutSectionAttribute.name.name}="${layoutSectionAttribute.value.value}">`
204
+ const isSelect = !isRadio && nodeName.match(SELECT_REGEX)
205
+
206
+ context.report({
207
+ node,
208
+ message: `${nodeName}は${actualName}より先に、smarthr-ui/${wrapComponentName}が入力要素を囲むようマークアップを以下のいずれかの方法で変更してください。
209
+ - 方法1: ${actualName} を${wrapComponentName}、もしくはそれを拡張したコンポーネントに変更してください
210
+ - ${actualName} 内のHeading要素は${wrapComponentName}のtitle属性に変更してください
211
+ - 方法2: ${actualName} と ${nodeName} の間に ${wrapComponentName} が存在するようにマークアップを変更してください${isRadio ? '' : `
212
+ - 方法3: 上記のいずれの方法も適切では場合、${nodeName}のtitle属性に "どんな値を${isSelect ? '選択' : '入力'}すれば良いのか" の説明を設定してください
213
+ - 例: <${nodeName} title="${isSelect ? '検索対象を選択してください' : '姓を全角カタカナのみで入力してください'}" />`}`,
214
+ });
215
+ }
216
+
217
+ return
218
+ }
219
+ }
220
+ }
221
+
222
+ break
223
+ }
224
+ case 'VariableDeclarator': {
225
+ if (n.parent.parent?.type && n.parent.parent.type.match(IGNORE_INPUT_CHECK_PARENT_TYPE)) {
226
+ const name = n.id.name
227
+
228
+ // 入力要素系コンポーネントの拡張なので対象外
229
+ if (name.match(FORM_CONTROL_INPUTS_REGEX) || checkAdditionalMultiInputComponents(name) || checkAdditionalInputComponents(name)) {
230
+ return
231
+ }
232
+ }
233
+
234
+ break
235
+ }
236
+ case 'Program': {
237
+ // HINT: smarthr-ui/CheckBoxはlabelを単独で持つため、FormControl系でラップをする必要はない
238
+ // HINT: 擬似的にラベルが設定されている場合、無視する
239
+ if (!isCheckbox && !isPseudoLabel) {
240
+ const isSelect = !isRadio && nodeName.match(SELECT_REGEX)
241
+
242
+ context.report({
243
+ node,
244
+ message: `${nodeName} を、smarthr-ui/${wrapComponentName} もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
245
+ - ${wrapComponentName}で入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります${isRadio ? `
246
+ - FieldsetでRadioButtonを囲むことでグループ化された入力要素に対して適切なタイトル・説明を追加出来ます` : ``}
247
+ - ${nodeName}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を${FROM_CONTROLS_REGEX}とマッチするように修正してください
248
+ - ${nodeName}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を${FORM_CONTROL_INPUTS_REGEX}とマッチするように修正してください${isRadio ? '' : `
249
+ - 上記のいずれの方法も適切では場合、${nodeName}のtitle属性に "どんな値を${isSelect ? '選択' : '入力'}すれば良いのか" の説明を設定してください
250
+ - 例: <${nodeName} title="${isSelect ? '検索対象を選択してください' : '姓を全角カタカナのみで入力してください'}" />`}`,
251
+ });
252
+ }
253
+
254
+ return
255
+ }
256
+ case 'CallExpression':
257
+ if (n.callee.property?.name === 'map') {
258
+ isInMap = true
259
+ }
260
+
261
+ break
262
+ }
263
+
264
+ return search(n.parent)
265
+ }
266
+
267
+ return search(node.parent.parent)
268
+ }
269
+
270
+ const formControlMatcher = nodeName.match(FROM_CONTROLS_REGEX)
271
+
272
+ if (formControlMatcher) {
273
+ const isRoleGrouop = node.attributes.find(findRoleGroup)
274
+
275
+ if (!nodeName.match(FORM_CONTROL_REGEX) && isRoleGrouop) {
276
+ const component = formControlMatcher[1]
277
+ const actualComponent = component[0].match(/[a-z]/) ? component : `smarthr-ui/${component}`
278
+
279
+ const message = `${nodeName}に 'role="group" が設定されています。${actualComponent} をつかってマークアップする場合、'role="group"' は不要です
280
+ - ${nodeName} が ${actualComponent}、もしくはそれを拡張しているコンポーネントではない場合、名称を ${FROM_CONTROLS_REGEX} にマッチしないものに変更してください`
281
+ context.report({
282
+ node,
283
+ message,
284
+ });
285
+
286
+ return
287
+ }
288
+
289
+ const searchParent = (n) => {
290
+ switch (n.type) {
291
+ case 'JSXElement': {
292
+ const name = n.openingElement.name.name || ''
293
+
294
+ // Fieldset > Dialog > Fieldset のようにDialogを挟んだFormControl系のネストは許容する(Portalで実際にはネストしていないため)
295
+ if (name.match(DIALOG_REGEX)) {
296
+ return
297
+ }
298
+
299
+ const matcher = name.match(FROM_CONTROLS_REGEX)
300
+ if (matcher) {
301
+ // FormControl > FormControl や FormControl > Fieldset のように複数のFormControl系コンポーネントがネストしてしまっているためエラーにする
302
+ // Fieldset > Fieldset や Fieldset > FormControl のようにFieldsetが親の場合は許容する
303
+ if (name.match(FORM_CONTROL_REGEX)) {
304
+ context.report({
305
+ node: n,
306
+ message: `${name} が、${nodeName} を子要素として持っており、マークアップとして正しくない状態になっています。以下のいずれかの方法で修正を試みてください。
307
+ - 方法1: 親要素である${name}をsmarthr-ui/${matcher[1]}、もしくはそれを拡張していないコンポーネントでマークアップしてください
308
+ - ${matcher[1]}ではなく、smarthr-ui/Fieldset、もしくはsmarthr-ui/Section + smarthr-ui/Heading などでのマークアップを検討してください
309
+ - 方法2: 親要素である${name}がsmarthr-ui/${matcher[1]}を拡張したコンポーネントではない場合、コンポーネント名を${FORM_CONTROL_REGEX}と一致しない名称に変更してください`,
310
+ });
311
+ }
312
+
313
+ return
314
+ }
315
+
316
+ break
317
+ }
318
+ case 'Program': {
319
+ return
320
+ }
321
+ }
322
+
323
+ return searchParent(n.parent)
324
+ }
325
+
326
+ searchParent(node.parent.parent)
327
+
328
+ if (!node.selfClosing && nodeName.match(FORM_CONTROL_REGEX) && isRoleGrouop) {
329
+ const searchChildren = (n, count = 0) => {
330
+ switch (n.type) {
331
+ case 'BinaryExpression':
332
+ case 'Identifier':
333
+ case 'JSXEmptyExpression':
334
+ case 'JSXText':
335
+ case 'Literal':
336
+ case 'VariableDeclaration':
337
+ // これ以上childrenが存在しないため終了
338
+ return count
339
+ case 'JSXAttribute':
340
+ return n.value ? searchChildren(n.value, count) : count
341
+ case 'LogicalExpression':
342
+ return searchChildren(n.right, count)
343
+ case 'ArrowFunctionExpression':
344
+ return searchChildren(n.body, count)
345
+ case 'MemberExpression':
346
+ return searchChildren(n.property, count)
347
+ case 'ReturnStatement':
348
+ case 'UnaryExpression':
349
+ return searchChildren(n.argument, count)
350
+ case 'ChainExpression':
351
+ case 'JSXExpressionContainer':
352
+ return searchChildren(n.expression, count)
353
+ case 'BlockStatement': {
354
+ return forInSearchChildren(n.body, count)
355
+ }
356
+ case 'ConditionalExpression': {
357
+ const conCount = searchChildren(n.consequent, count)
358
+
359
+ if (conCount > 1) {
360
+ return conCount
361
+ }
362
+
363
+ const altCount = searchChildren(n.alternate, count)
364
+
365
+ return conCount > altCount ? conCount : altCount
366
+ }
367
+ case 'CallExpression': {
368
+ const nextCount = forInSearchChildren(n.arguments, count)
369
+
370
+ if (nextCount > count && n.callee.property?.name === 'map') {
371
+ return Infinity
372
+ }
373
+
374
+ return nextCount
375
+ }
376
+ case 'JSXFragment':
377
+ break
378
+ case 'JSXElement': {
379
+ const name = n.openingElement.name.name || ''
380
+
381
+ if (name.match(FIELDSET_REGEX) || checkAdditionalMultiInputComponents(name)) {
382
+ // 複数inputが存在する可能性のあるコンポーネントなので無限カウントとする
383
+ return Infinity
384
+ }
385
+
386
+
387
+ let nextCount = forInSearchChildren(n.openingElement.attributes, count)
388
+
389
+ if (nextCount > 1) {
390
+ return nextCount
391
+ }
392
+
393
+ if (
394
+ name.match(FORM_CONTROL_INPUTS_REGEX) ||
395
+ name.match(FORM_CONTROL_REGEX) ||
396
+ checkAdditionalInputComponents(name)
397
+ ) {
398
+ nextCount = nextCount + 1
399
+ }
400
+
401
+ if (nextCount > count) {
402
+ return nextCount
403
+ }
404
+
405
+ break
406
+ }
407
+ }
408
+
409
+ return n.children ? forInSearchChildren(n.children, count) : count
410
+ }
411
+
412
+ const forInSearchChildren = (ary, initCount) => {
413
+ let r = initCount
414
+
415
+ for (const i in ary) {
416
+ r += searchChildren(ary[i])
417
+
418
+ if (r > 1) {
419
+ break
420
+ }
421
+ }
422
+
423
+ return r
424
+ }
425
+
426
+ const result = forInSearchChildren(node.parent.children, 0)
427
+
428
+ if (result < 2) {
429
+ context.report({
430
+ node,
431
+ message: `${nodeName}内に入力要素が2個以上存在しないため、'role="group"'を削除してください。'role="group"'は複数の入力要素を一つのグループとして扱うための属性です。
432
+ - ${nodeName}内に2つ以上の入力要素が存在する場合、入力要素を含むコンポーネント名全てを${FORM_CONTROL_INPUTS_REGEX}、もしくは${FROM_CONTROLS_REGEX}にマッチする名称に変更してください`,
433
+ });
434
+ }
435
+ }
436
+ }
437
+ },
438
+ }
439
+ },
440
+ }
441
+ module.exports.schema = SCHEMA
@@ -0,0 +1,60 @@
1
+ # smarthr/a11y-prohibit-useless-sectioning-fragment
2
+
3
+ - Headingレベルの自動計算用のコンポーネントであるSectioningFragmentが不必要に利用されている場合を検知し、修正を促します
4
+ - Sectioninigされるコンポーネントを直接SectioningFragmentで囲んでいる場合エラーになります
5
+
6
+ ## rules
7
+
8
+ ```js
9
+ {
10
+ rules: {
11
+ 'smarthr/a11y-prohibit-useless-sectioning-fragment': 'error', // 'warn', 'off',
12
+ },
13
+ }
14
+ ```
15
+
16
+ ## ❌ Incorrect
17
+
18
+ ```jsx
19
+ <SectioninigFragment>
20
+ <Section>
21
+ any
22
+ </Section>
23
+ </SectioninigFragment>
24
+
25
+ <SectioninigFragment>
26
+ <Stack as="aside">
27
+ any
28
+ </Stack>
29
+ </SectioninigFragment>
30
+
31
+ <SectioninigFragment>
32
+ <HogeCenter forwardedas="nav">
33
+ any
34
+ </HogeCenter>
35
+ </SectioninigFragment>
36
+ ```
37
+
38
+ ## ✅ Correct
39
+
40
+ ```jsx
41
+ <Section>
42
+ any
43
+ </Section>
44
+
45
+ <Stack as="aside">
46
+ any
47
+ </Stack>
48
+
49
+ <HogeCenter forwardedas="nav">
50
+ any
51
+ </HogeCenter>
52
+
53
+ <SectioningFragment>
54
+ <Any />
55
+ </SectioningFragment>
56
+
57
+ <Aside>
58
+ <SectioningFragment>{any}</SectioningFragment>>
59
+ </Aside>
60
+ ```
@@ -0,0 +1,74 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
+
3
+ const EXPECTED_NAMES = {
4
+ 'Article$': '(Article)$',
5
+ 'Aside$': '(Aside)$',
6
+ 'Nav$': '(Nav)$',
7
+ 'Section$': '(Section)$',
8
+ 'Center$': '(Center)$',
9
+ 'Reel$': '(Reel)$',
10
+ 'Sidebar$': '(Sidebar)$',
11
+ 'Stack$': '(Stack)$',
12
+ }
13
+
14
+ const UNEXPECTED_NAMES = EXPECTED_NAMES
15
+
16
+ const BARE_SECTIONING_TAG_REGEX = /^(article|aside|nav|section)$/
17
+ const SECTIONING_REGEX = /((A(rticle|side))|Nav|Section)$/
18
+ const SECTIONING_FRAGMENT_REGEX = /^SectioningFragment$/
19
+ const LAYOUT_REGEX = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
20
+ const AS_REGEX = /^(as|forwardedAs)$/
21
+
22
+ const includeSectioningAsAttr = (a) => a.name?.name?.match(AS_REGEX) && a.value.value?.match(BARE_SECTIONING_TAG_REGEX)
23
+
24
+ const searchSectioningFragment = (node) => {
25
+ switch (node.type) {
26
+ case 'JSXElement':
27
+ return node.openingElement.name?.name?.match(SECTIONING_FRAGMENT_REGEX) ? node.openingElement : null
28
+ case 'Program':
29
+ return null
30
+ }
31
+
32
+ return searchSectioningFragment(node.parent)
33
+ }
34
+
35
+ const SCHEMA = []
36
+
37
+ module.exports = {
38
+ meta: {
39
+ type: 'problem',
40
+ schema: SCHEMA,
41
+ },
42
+ create(context) {
43
+ return {
44
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
45
+ JSXOpeningElement: (node) => {
46
+ const name = node.name?.name || ''
47
+ let hit = null
48
+ let asAttr = null
49
+
50
+ if (name.match(SECTIONING_REGEX)) {
51
+ hit = true
52
+ } else {
53
+ asAttr = name.match(LAYOUT_REGEX) && node.attributes.find(includeSectioningAsAttr)
54
+
55
+ if (asAttr) {
56
+ hit = true
57
+ }
58
+ }
59
+
60
+ if (hit) {
61
+ result = searchSectioningFragment(node.parent.parent)
62
+
63
+ if (result) {
64
+ context.report({
65
+ node: result,
66
+ message: `無意味なSectioningFragmentが記述されています。子要素である<${name}${asAttr ? ` ${asAttr.name.name}="${asAttr.value.value}"` : ''}>で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
67
+ })
68
+ }
69
+ }
70
+ },
71
+ }
72
+ },
73
+ }
74
+ module.exports.schema = SCHEMA
@@ -0,0 +1,155 @@
1
+ const rule = require('../rules/a11y-input-in-form-control')
2
+ const RuleTester = require('eslint').RuleTester
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 2018,
7
+ ecmaFeatures: {
8
+ experimentalObjectRestSpread: true,
9
+ jsx: true,
10
+ },
11
+ sourceType: 'module',
12
+ },
13
+ })
14
+
15
+ const noLabeledInput = (name) => `${name} を、smarthr-ui/FormControl もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
16
+ - FormControlで入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります
17
+ - ${name}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/とマッチするように修正してください
18
+ - ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/とマッチするように修正してください
19
+ - 上記のいずれの方法も適切では場合、${name}のtitle属性に "どんな値を入力すれば良いのか" の説明を設定してください
20
+ - 例: <${name} title="姓を全角カタカナのみで入力してください" />`
21
+ const noLabeledSelect = (name) => `${name} を、smarthr-ui/FormControl もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
22
+ - FormControlで入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります
23
+ - ${name}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/とマッチするように修正してください
24
+ - ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/とマッチするように修正してください
25
+ - 上記のいずれの方法も適切では場合、${name}のtitle属性に "どんな値を選択すれば良いのか" の説明を設定してください
26
+ - 例: <${name} title="検索対象を選択してください" />`
27
+ const invalidPureCheckboxInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。
28
+ - 可能なら${name}はsmarthr-ui/Checkboxへの変更を検討してください。難しい場合は ${name} と結びつくlabel要素が必ず存在するよう、マークアップする必要があることに注意してください。`
29
+ const invalidCheckboxInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。`
30
+ const invalidPureRadioInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。
31
+ - Fieldsetで同じname属性のラジオボタン全てを囲むことで正しくグループ化され、適切なタイトル・説明を追加出来ます
32
+ - 可能なら${name}はsmarthr-ui/RadioButton、smarthr-ui/RadioButtonPanelへの変更を検討してください。難しい場合は ${name} と結びつくlabel要素が必ず存在するよう、マークアップする必要があることに注意してください。`
33
+ const invalidRadioInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。
34
+ - Fieldsetで同じname属性のラジオボタン全てを囲むことで正しくグループ化され、適切なタイトル・説明を追加出来ます`
35
+ const invalidMultiInputsInFormControl = () => `HogeFormControl が複数の入力要素を含んでいます。ラベルと入力要素の紐づけが正しく行われない可能性があるため、以下の方法のいずれかで修正してください。
36
+ - 方法1: 入力要素ごとにラベルを設定できる場合、HogeFormControlをsmarthr-ui/Fieldset、もしくはそれを拡張したコンポーネントに変更した上で、入力要素を一つずつsmarthr-ui/FormControlで囲むようにマークアップを変更してください
37
+ - 方法2: 郵便番号や電話番号など、本来一つの概念の入力要素を分割して複数の入力要素にしている場合、一つの入力要素にまとめることを検討してください
38
+ - コピーアンドペーストがしやすくなる、ブラウザの自動補完などがより適切に反映されるなど多大なメリットがあります
39
+ - 方法3: HogeFormControl が smarthr-ui/FormControl、もしくはそれを拡張しているコンポーネントではない場合、名称を /((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/ にマッチしないものに変更してください
40
+ - 方法4: 上記方法のいずれも対応出来ない場合、HogeFormControl に 'role="group"' 属性を設定してください`
41
+ const noLabeledInputInFieldset = (name) => `HogeFieldset が ラベルを持たない入力要素(${name})を含んでいます。入力要素が何であるかを正しく伝えるため、以下の方法のいずれかで修正してください。
42
+ - 方法1: HogeFieldset を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
43
+ - 方法2: ${name} がlabel要素を含むコンポーネントである場合、名称を/(Form(Control|Group))$/にマッチするものに変更してください
44
+ - smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
45
+ - 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/にマッチするものに変更してください
46
+ - smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
47
+ - 方法4: HogeFieldset が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を /Fieldset$/ にマッチしないものに変更してください`
48
+ const useFormControlInsteadOfSection = (name, section) => `${name}は${section}より先に、smarthr-ui/FormControlが入力要素を囲むようマークアップを以下のいずれかの方法で変更してください。
49
+ - 方法1: ${section} をFormControl、もしくはそれを拡張したコンポーネントに変更してください
50
+ - ${section} 内のHeading要素はFormControlのtitle属性に変更してください
51
+ - 方法2: ${section} と ${name} の間に FormControl が存在するようにマークアップを変更してください
52
+ - 方法3: 上記のいずれの方法も適切では場合、${name}のtitle属性に "どんな値を入力すれば良いのか" の説明を設定してください
53
+ - 例: <${name} title="姓を全角カタカナのみで入力してください" />`
54
+ const useFormControlInsteadOfSectionInRadio = (name, section) => `${name}は${section}より先に、smarthr-ui/Fieldsetが入力要素を囲むようマークアップを以下のいずれかの方法で変更してください。
55
+ - 方法1: ${section} をFieldset、もしくはそれを拡張したコンポーネントに変更してください
56
+ - ${section} 内のHeading要素はFieldsetのtitle属性に変更してください
57
+ - 方法2: ${section} と ${name} の間に Fieldset が存在するようにマークアップを変更してください`
58
+ const invalidFieldsetHasRoleGroup = (fieldset, base) => `${fieldset}に 'role="group" が設定されています。${base} をつかってマークアップする場合、'role="group"' は不要です
59
+ - ${fieldset} が ${base}、もしくはそれを拡張しているコンポーネントではない場合、名称を /((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/ にマッチしないものに変更してください`
60
+ const invalidChildreninFormControl = (children) => `FormControl が、${children} を子要素として持っており、マークアップとして正しくない状態になっています。以下のいずれかの方法で修正を試みてください。
61
+ - 方法1: 親要素であるFormControlをsmarthr-ui/FormControl、もしくはそれを拡張していないコンポーネントでマークアップしてください
62
+ - FormControlではなく、smarthr-ui/Fieldset、もしくはsmarthr-ui/Section + smarthr-ui/Heading などでのマークアップを検討してください
63
+ - 方法2: 親要素であるFormControlがsmarthr-ui/FormControlを拡張したコンポーネントではない場合、コンポーネント名を/(Form(Control|Group))$/と一致しない名称に変更してください`
64
+ const requireMultiInputInFormControlWithRoleGroup = () => `HogeFormControl内に入力要素が2個以上存在しないため、'role=\"group\"'を削除してください。'role=\"group\"'は複数の入力要素を一つのグループとして扱うための属性です。
65
+ - HogeFormControl内に2つ以上の入力要素が存在する場合、入力要素を含むコンポーネント名全てを/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/、もしくは/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/にマッチする名称に変更してください`
66
+
67
+ ruleTester.run('a11y-input-in-form-control', rule, {
68
+ valid: [
69
+ { code: `import styled from 'styled-components'` },
70
+ { code: `import styled, { css } from 'styled-components'` },
71
+ { code: `import { css } from 'styled-components'` },
72
+ { code: `import { HogeAInput as FugaInput } from './hoge'` },
73
+ { code: `import { Textarea as HogeTextarea } from './hoge'` },
74
+ { code: 'const HogeInput = styled.input``' },
75
+ { code: 'const HogeTextarea = styled.textarea``' },
76
+ { code: 'const HogeSelect = styled(Select)``' },
77
+ { code: 'const HogeRadioButton = styled(FugaRadioButton)``' },
78
+ { code: 'const HogeRadioButtonPanel = styled(FugaRadioButtonPanel)``' },
79
+ { code: 'const HogeCheckBox = styled(FugaCheckbox)``' },
80
+ { code: 'const DatePicker = styled(AnyDatePicker)``' },
81
+ { code: '<input title="any"/>' },
82
+ { code: '<HogeInput title="any"/>' },
83
+ { code: '<textarea title="any"/>' },
84
+ { code: '<HogeTextarea title="any"/>' },
85
+ { code: '<select title="any"/>' },
86
+ { code: '<HogeSelect title="any"/>' },
87
+ { code: '<HogeInputFile title="any"/>' },
88
+ { code: '<HogeComboBox title="any"/>' },
89
+ { code: '<HogeDatePicker title="any"/>' },
90
+ { code: '<HogeFormGroup />' },
91
+ { code: '<HogeFormControl />' },
92
+ { code: '<HogeFieldset />' },
93
+ { code: '<HogeFormGroup><input /></HogeFormGroup>' },
94
+ { code: '<HogeFormGroup><input title="any" /></HogeFormGroup>' },
95
+ { code: '<HogeFormGroup><Input type="checkbox" /></HogeFormGroup>' },
96
+ { code: '<HogeFormGroup><CheckBox /></HogeFormGroup>' },
97
+ { code: '<HogeFormControl><HogeSelect /></HogeFormControl>' },
98
+ { code: '<HogeFormControl><HogeComboBox title="any" /></HogeFormControl>' },
99
+ { code: '<HogeFieldset><Input type="checkbox" /><Input type="checkbox" /></HogeFieldset>' },
100
+ { code: '<HogeFieldset><HogeCheckBox /><HogeCheckBox /></HogeFieldset>' },
101
+ { code: '<HogeFieldset><input type="radio" /></HogeFieldset>' },
102
+ { code: '<HogeFieldset><RadioButton /></HogeFieldset>' },
103
+ { code: '<HogeFieldset><HogeRadioButtonPanel /></HogeFieldset>' },
104
+ { code: '<HogeFormControl role="group"><HogeInput /><HogeSelect /></HogeFormControl>' },
105
+ { code: '<FugaSection><HogeFormControl><HogeInput /></HogeFormControl></FugaSection>' },
106
+ { code: '<Stack as="section"><HogeFormControl><HogeInput /></HogeFormControl></Stack>' },
107
+ { code: `const AnyComboBox = () => <input />` },
108
+ { code: `<Fieldset><HogeFieldset /><HogeFormControl /></Fieldset>` },
109
+ { code: '<HogeFieldset><HogeCheckBox /><HogeInput id="any" /></HogeFieldset>' },
110
+ { code: '<FugaSection><HogeInput id="any" /></FugaSection>' },
111
+ { code: '<HogeTextarea id="any" />' },
112
+ { code: '<HogeFieldset><HogeCheckBox /><HogeInput title="any" /></HogeFieldset>' },
113
+ { code: '<FugaSection><HogeInput title="any" /></FugaSection>' },
114
+ { code: '<HogeTextarea title="any" />' },
115
+ ],
116
+ invalid: [
117
+ { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
118
+ { code: `import { ComboBox as ComboBoxHoge } from './hoge'`, errors: [ { message: `ComboBoxHogeを正規表現 "/(ComboBox)$/" がmatchする名称に変更してください。
119
+ - ComboBoxが型の場合、'import type { ComboBox as ComboBoxHoge }' もしくは 'import { type ComboBox as ComboBoxHoge }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
120
+ { code: 'const RadioButton = styled(FugaRadioButtonPanel)``', errors: [
121
+ { message: `RadioButtonを正規表現 "/(RadioButtonPanel)$/" がmatchする名称に変更してください。` },
122
+ { message: `RadioButton は /RadioButton$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
123
+ - RadioButton の名称の末尾が"RadioButton" という文字列ではない状態にしつつ、"FugaRadioButtonPanel"を継承していることをわかる名称に変更してください
124
+ - もしくは"FugaRadioButtonPanel"を"RadioButton"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
125
+ - 修正例1: const Xxxx = styled(FugaRadioButtonPanel)
126
+ - 修正例2: const RadioButtonXxxx = styled(FugaRadioButtonPanel)
127
+ - 修正例3: const RadioButton = styled(XxxxRadioButton)` } ] },
128
+ { code: `<input />`, errors: [ { message: noLabeledInput('input') } ] },
129
+ { code: `<HogeInput />`, errors: [ { message: noLabeledInput('HogeInput') } ] },
130
+ { code: '<textarea />', errors: [ { message: noLabeledInput('textarea') } ] },
131
+ { code: '<HogeTextarea />', errors: [ { message: noLabeledInput('HogeTextarea') } ] },
132
+ { code: '<select />', errors: [ { message: noLabeledSelect('select') } ] },
133
+ { code: '<HogeSelect />', errors: [ { message: noLabeledSelect('HogeSelect') } ] },
134
+ { code: '<HogeInputFile />', errors: [ { message: noLabeledInput('HogeInputFile') } ] },
135
+ { code: '<HogeComboBox />', errors: [ { message: noLabeledInput('HogeComboBox') } ] },
136
+ { code: '<HogeDatePicker />', errors: [ { message: noLabeledInput('HogeDatePicker') } ] },
137
+ { code: '<HogeFormControl><Input type="checkbox" /><Input type="checkbox" /></HogeFormControl>', errors: [ { message: invalidPureCheckboxInFormControl('Input') } ] },
138
+ { code: '<HogeFormControl><HogeCheckBox /><Input /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
139
+ { code: '<HogeFormControl><HogeCheckBox /><HogeCheckBox /></HogeFormControl>', errors: [ { message: invalidCheckboxInFormControl('HogeCheckBox') } ] },
140
+ { code: '<HogeFormControl><input type="radio" /></HogeFormControl>', errors: [ { message: invalidPureRadioInFormControl('input') } ] },
141
+ { code: '<HogeFormControl><RadioButton /></HogeFormControl>', errors: [ { message: invalidRadioInFormControl('RadioButton') } ] },
142
+ { code: '<HogeFormControl><HogeRadioButtonPanel /></HogeFormControl>', errors: [ { message: invalidRadioInFormControl('HogeRadioButtonPanel') } ] },
143
+ { code: '<HogeFieldset><HogeCheckBox /><HogeInput /></HogeFieldset>', errors: [ { message: noLabeledInputInFieldset('HogeInput') } ] },
144
+ { code: '<FugaSection><HogeInput /></FugaSection>', errors: [ { message: useFormControlInsteadOfSection('HogeInput', 'FugaSection') } ] },
145
+ { code: '<Stack as="section"><HogeInput /></Stack>', errors: [ { message: useFormControlInsteadOfSection('HogeInput', '<Stack as="section">') } ] },
146
+ { code: '<Center forwardedAs="aside"><HogeInput /></Center>', errors: [ { message: useFormControlInsteadOfSection('HogeInput', '<Center forwardedAs="aside">') } ] },
147
+ { code: '<FugaSection><HogeRadioButton /></FugaSection>', errors: [ { message: useFormControlInsteadOfSectionInRadio('HogeRadioButton', 'FugaSection') } ] },
148
+ { code: `const AnyHoge = () => <input />`, errors: [ { message: noLabeledInput('input') } ] },
149
+ { code: '<HogeFieldset role="group"><HogeFormControl /><HogeRadioButton /></HogeFieldset>', errors: [ { message: invalidFieldsetHasRoleGroup('HogeFieldset', 'smarthr-ui/Fieldset') } ] },
150
+ { code: '<fieldset role="group"><HogeFormControl /><HogeRadioButton /></fieldset>', errors: [ { message: invalidFieldsetHasRoleGroup('fieldset', 'fieldset') } ] },
151
+ { code: '<FormControl><HogeFieldset /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFieldset') } ] },
152
+ { code: '<FormControl><HogeFormControl /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFormControl') } ] },
153
+ { code: '<HogeFormControl role="group"><HogeInput /></HogeFormControl>', errors: [ { message: requireMultiInputInFormControlWithRoleGroup() } ] },
154
+ ]
155
+ })
@@ -0,0 +1,31 @@
1
+ const rule = require('../rules/a11y-prohibit-useless-sectioning-fragment')
2
+ const RuleTester = require('eslint').RuleTester
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 2018,
7
+ ecmaFeatures: {
8
+ experimentalObjectRestSpread: true,
9
+ jsx: true,
10
+ },
11
+ sourceType: 'module',
12
+ },
13
+ })
14
+
15
+ const error = (tag) => `無意味なSectioningFragmentが記述されています。子要素である${tag}で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
16
+
17
+ ruleTester.run('a11y-prohibit-useless-sectioning-fragment', rule, {
18
+ valid: [
19
+ { code: `<SectioningFragment>hoge</SectioningFragment>` },
20
+ { code: `<Section><SectioningFragment>hoge</SectioningFragment></Section>` },
21
+ { code: `<AnyAside><SectioningFragment>hoge</SectioningFragment></AnyAside>` },
22
+ { code: `<AnyNav>hoge</AnyNav>` },
23
+ { code: `<AnyArticle>hoge</AnyArticle>` },
24
+ ],
25
+ invalid: [
26
+ { code: `<SectioningFragment><AnySection /></SectioningFragment>`, errors: [ { message: error('<AnySection>') } ] },
27
+ { code: `<SectioningFragment><AnyAside>hoge</AnyAside></SectioningFragment>`, errors: [ { message: error('<AnyAside>') } ] },
28
+ { code: `<SectioningFragment><HogeStack as="aside">hoge</HogeStack></SectioningFragment>`, errors: [ { message: error('<HogeStack as="aside">') } ] },
29
+ { code: `<SectioningFragment><HogeReel forwardedAs="nav">hoge</HogeReel></SectioningFragment>`, errors: [ { message: error('<HogeReel forwardedAs="nav">') } ] },
30
+ ]
31
+ })