eslint-plugin-smarthr 0.3.23 → 0.3.25

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,16 @@
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.25](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.24...v0.3.25) (2024-01-23)
6
+
7
+
8
+ ### Features
9
+
10
+ * 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))
11
+ * 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))
12
+
13
+ ### [0.3.24](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.23...v0.3.24) (2024-01-19)
14
+
5
15
  ### [0.3.23](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.22...v0.3.23) (2024-01-16)
6
16
 
7
17
 
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)
@@ -33,8 +33,7 @@ const generateTagFormatter = ({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }) =>
33
33
  context.report({
34
34
  node,
35
35
  message: `${extended}を正規表現 "${e.toString()}" がmatchする名称に変更してください。${isImport ? `
36
- - ${base}が型の場合、'import type { ${base} as ${extended} }' もしくは 'import { type ${base} as ${extended} }' のように明示的に型であることを宣言してください。名称変更が不要になります
37
- ` : ''}`,
36
+ - ${base}が型の場合、'import type { ${base} as ${extended} }' もしくは 'import { type ${base} as ${extended} }' のように明示的に型であることを宣言してください。名称変更が不要になります` : ''}`,
38
37
  });
39
38
  }
40
39
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -3,6 +3,7 @@
3
3
  - Headingコンポーネントをsmarthr-ui/SectioningContent(Article, Aside, Nav, Section, SectioningFragment) のいずれかで囲むことを促すルールです
4
4
  - article, aside, nav, section で Heading とHeadingの対象となる範囲を囲むとブラウザが正確に解釈できるようになるメリットがあります
5
5
  - またsmarthr-ui/SectioningContentで smarthr-ui/Headingを囲むことで、Headingのレベル(h1~h6)を自動的に計算するメリットもあります
6
+ - Headingコンポーネントをsmarthr-ui/Layout(Center, Reel, Sidebar, Stack) のいずれかで囲んでおり、かつas, forwardedAsのいずれかの属性で 'section', 'article', 'aside', 'nav' が指定されている場合、SectioningContentで囲んでいるものとして扱われるようになります
6
7
 
7
8
  ## rules
8
9
 
@@ -59,20 +60,17 @@
59
60
  <Heading>fuga</Heading>
60
61
  </Section>
61
62
  </Section>
62
- ```
63
63
 
64
- ```jsx
65
64
  <>
66
65
  <PageHeading>Page Name.</PageHeading>
67
66
  <Section>
68
- <Heading>
69
- hoge
70
- </Heading>
67
+ <Heading>hoge</Heading>
71
68
  </Section>
72
69
  <StyledSection>
73
- <Heading>
74
- fuga
75
- </Heading>
70
+ <Heading>fuga</Heading>
76
71
  </StyledSection>
72
+ <Center as="aside">
73
+ <Heading>piyo</Heading>
74
+ </Center>
77
75
  </>
78
76
  ```
@@ -51,8 +51,9 @@ const sectioningRegex = /((A(rticle|side))|Nav|Section|^SectioningFragment)$/
51
51
  const bareTagRegex = /^(article|aside|nav|section)$/
52
52
  const modelessDialogRegex = /ModelessDialog$/
53
53
  const layoutComponentRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
54
+ const asRegex = /^(as|forwardedAs)$/
54
55
 
55
- const includeSectioningAsAttr = (a) => a.name?.name === 'as' && a.value.value.match(bareTagRegex)
56
+ const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && a.value.value.match(bareTagRegex)
56
57
 
57
58
  const noHeadingTagNames = ['span', 'legend']
58
59
  const ignoreHeadingCheckParentType = ['Program', 'ExportNamedDeclaration']
@@ -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