eslint-plugin-smarthr 0.5.0 → 0.5.2
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 +9 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/rules/a11y-form-control-in-form/README.md +67 -0
- package/rules/a11y-form-control-in-form/index.js +94 -0
- package/rules/a11y-heading-in-sectioning-content/index.js +1 -1
- package/rules/a11y-prohibit-input-placeholder/index.js +1 -1
- package/test/a11y-form-control-in-form.js +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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.5.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.1...v0.5.2) (2024-03-17)
|
|
6
|
+
|
|
7
|
+
### [0.5.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.0...v0.5.1) (2024-03-17)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* a11y-form-control-in-formルールを追加 ([#121](https://github.com/kufu/eslint-plugin-smarthr/issues/121)) ([a6270f9](https://github.com/kufu/eslint-plugin-smarthr/commit/a6270f9f22832a8ccaeef3b63e35da84b6e13e68))
|
|
13
|
+
|
|
5
14
|
## [0.5.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.4.2...v0.5.0) (2024-03-11)
|
|
6
15
|
|
|
7
16
|
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
- [a11y-anchor-has-href-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-anchor-has-href-attribute)
|
|
4
4
|
- [a11y-clickable-element-has-text](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-clickable-element-has-text)
|
|
5
5
|
- [a11y-delegate-element-has-role-presentation](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-delegate-element-has-role-presentation)
|
|
6
|
+
- [a11y-form-control-in-form](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-form-control-in-form)
|
|
6
7
|
- [a11y-heading-in-sectioning-content](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-heading-in-sectioning-content)
|
|
7
8
|
- [a11y-image-has-alt-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-image-has-alt-attribute)
|
|
8
9
|
- [a11y-input-has-name-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-has-name-attribute)
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# smarthr/a11y-form-control-in-form
|
|
2
|
+
|
|
3
|
+
- fieldset, Fieldset, FormControl を利用する場合、form要素で囲むことを促すルールです
|
|
4
|
+
- form要素で囲むことで以下のようなメリットがあります
|
|
5
|
+
- 適切にマークアップできるようになり、フォームの範囲などがスクリーンリーダーに正しく伝わる
|
|
6
|
+
- 入力要素にfocusした状態でEnterを押せばフォームをsubmitできる
|
|
7
|
+
- inputのrequired属性、pattern属性を利用した入力チェックをブラウザの機能として実行できる
|
|
8
|
+
- smarthr/a11y-input-in-form-control と組み合わせることでより厳密なフォームのマークアップを行えます
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## rules
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
{
|
|
15
|
+
rules: {
|
|
16
|
+
'smarthr/a11y-form-control-in-form': 'error', // 'warn', 'off'
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ❌ Incorrect
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
// formで囲まれていないためNG
|
|
25
|
+
const AnyComponent = <>
|
|
26
|
+
<FormControl />
|
|
27
|
+
<HogeFieldset />
|
|
28
|
+
<fieldset />
|
|
29
|
+
</>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## ✅ Correct
|
|
33
|
+
|
|
34
|
+
```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>
|
|
51
|
+
|
|
52
|
+
// 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
|
+
</>
|
|
67
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const FIELDSET_EXPECTED_NAMES = {
|
|
4
|
+
'((F|^f)ieldset)$': '(Fieldset)$',
|
|
5
|
+
'(Fieldsets)$': '(Fieldsets)$',
|
|
6
|
+
}
|
|
7
|
+
const FORM_CONTROL_EXPECTED_NAMES = {
|
|
8
|
+
...FIELDSET_EXPECTED_NAMES,
|
|
9
|
+
'(FormGroup)$': '(FormGroup)$',
|
|
10
|
+
'(FormControl)$': '(FormControl)$',
|
|
11
|
+
'(FormControls)$': '(FormControls)$',
|
|
12
|
+
}
|
|
13
|
+
const FORM_EXPECTED_NAMES = {
|
|
14
|
+
'((F|^f)orm)$': '(Form)$',
|
|
15
|
+
'(FormDialog)$': '(FormDialog)$',
|
|
16
|
+
'RemoteTrigger(.*)FormDialog$': 'RemoteTrigger(.*)FormDialog$',
|
|
17
|
+
}
|
|
18
|
+
const EXPECTED_NAMES = {
|
|
19
|
+
...FORM_CONTROL_EXPECTED_NAMES,
|
|
20
|
+
...FORM_EXPECTED_NAMES,
|
|
21
|
+
}
|
|
22
|
+
const UNEXPECTED_NAMES = EXPECTED_NAMES
|
|
23
|
+
|
|
24
|
+
const targetRegex = new RegExp(`(${Object.keys(FORM_CONTROL_EXPECTED_NAMES).join('|')})`)
|
|
25
|
+
const wrapperRegex = new RegExp(`(${Object.keys(EXPECTED_NAMES).join('|')})`)
|
|
26
|
+
const ignoreCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
|
|
27
|
+
const messageFieldset = `(${Object.values(FORM_CONTROL_EXPECTED_NAMES).join('|')})`
|
|
28
|
+
const declaratorTargetRegex = new RegExp(messageFieldset)
|
|
29
|
+
const asRegex = /^(as|forwardedAs)$/
|
|
30
|
+
const bareTagRegex = /^(form|fieldset)$/
|
|
31
|
+
|
|
32
|
+
const includeAsAttrFormOrFieldset = (a) => a.name?.name.match(asRegex) && a.value.value.match(bareTagRegex)
|
|
33
|
+
|
|
34
|
+
const searchBubbleUp = (node) => {
|
|
35
|
+
switch (node.type) {
|
|
36
|
+
case 'Program':
|
|
37
|
+
// rootまで検索した場合は確定でエラーにする
|
|
38
|
+
return null
|
|
39
|
+
case 'JSXElement':
|
|
40
|
+
// formかFieldsetでラップされていればOK
|
|
41
|
+
if (node.openingElement.name.name && (wrapperRegex.test(node.openingElement.name.name) || node.openingElement.attributes.some(includeAsAttrFormOrFieldset))) {
|
|
42
|
+
return node
|
|
43
|
+
}
|
|
44
|
+
break
|
|
45
|
+
case 'VariableDeclarator':
|
|
46
|
+
// FormControl系コンポーネントの拡張の場合は対象外
|
|
47
|
+
if (ignoreCheckParentTypeRegex.test(node.parent.parent?.type) && declaratorTargetRegex.test(node.id.name)) {
|
|
48
|
+
return node
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
break
|
|
52
|
+
case 'FunctionDeclaration':
|
|
53
|
+
case 'ClassDeclaration':
|
|
54
|
+
// FormControl系コンポーネントの拡張の場合は対象外
|
|
55
|
+
if (ignoreCheckParentTypeRegex.test(node.parent.type) && declaratorTargetRegex.test(node.id.name)) {
|
|
56
|
+
return node
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return searchBubbleUp(node.parent)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
meta: {
|
|
67
|
+
type: 'problem',
|
|
68
|
+
schema: [],
|
|
69
|
+
},
|
|
70
|
+
create(context) {
|
|
71
|
+
return {
|
|
72
|
+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
73
|
+
JSXOpeningElement: (node) => {
|
|
74
|
+
const elementName = node.name.name
|
|
75
|
+
|
|
76
|
+
if (elementName && targetRegex.test(elementName)) {
|
|
77
|
+
const result = searchBubbleUp(node.parent.parent)
|
|
78
|
+
|
|
79
|
+
if (!result) {
|
|
80
|
+
context.report({
|
|
81
|
+
node,
|
|
82
|
+
message: `${elementName}をform要素で囲むようにマークアップしてください。
|
|
83
|
+
- form要素で囲むことでスクリーンリーダーに入力フォームであることが正しく伝わる、入力要素にfocusした状態でEnterを押せばsubmitできる、inputのpattern属性を利用できるなどのメリットがあります
|
|
84
|
+
- 以下のいずれかの方法で修正をおこなってください
|
|
85
|
+
- 方法1: form要素で ${elementName} を囲んでください。smarthr-ui/ActionDialog、もしくはsmarthr-ui/RemoteTriggerActionDialogを利用している場合、smarthr-ui/FormDialog、smarthr-ui/RemoteTriggerFormDialogに置き換えてください
|
|
86
|
+
- 方法2: ${elementName} がコンポーネント内の一要素であり、かつその親コンポーネントがFormControl、もしくはFieldsetを表現するものである場合、親コンポーネント名を "${messageFieldset}" とマッチするものに変更してください`,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const rule = require('../rules/a11y-form-control-in-form')
|
|
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 generateErrorText = (elementName) => `${elementName}をform要素で囲むようにマークアップしてください。
|
|
16
|
+
- form要素で囲むことでスクリーンリーダーに入力フォームであることが正しく伝わる、入力要素にfocusした状態でEnterを押せばsubmitできる、inputのpattern属性を利用できるなどのメリットがあります
|
|
17
|
+
- 以下のいずれかの方法で修正をおこなってください
|
|
18
|
+
- 方法1: form要素で ${elementName} を囲んでください。smarthr-ui/ActionDialog、もしくはsmarthr-ui/RemoteTriggerActionDialogを利用している場合、smarthr-ui/FormDialog、smarthr-ui/RemoteTriggerFormDialogに置き換えてください
|
|
19
|
+
- 方法2: ${elementName} がコンポーネント内の一要素であり、かつその親コンポーネントがFormControl、もしくはFieldsetを表現するものである場合、親コンポーネント名を "((Fieldset)$|(Fieldsets)$|(FormGroup)$|(FormControl)$|(FormControls)$)" とマッチするものに変更してください`
|
|
20
|
+
|
|
21
|
+
ruleTester.run('a11y-form-control-in-form', rule, {
|
|
22
|
+
valid: [
|
|
23
|
+
{ code: '<input />' },
|
|
24
|
+
{ code: '<Select />' },
|
|
25
|
+
{ code: '<form><FormControl /></form>' },
|
|
26
|
+
{ code: '<Form><fieldset /></Form>' },
|
|
27
|
+
{ code: '<StyledForm><AnyFieldset /></StyledForm>' },
|
|
28
|
+
{ code: 'const HogeFormControl = <><AnyFormControl /></>' },
|
|
29
|
+
{ code: 'const HogeFieldset = <><AnyFieldset /></>' },
|
|
30
|
+
],
|
|
31
|
+
invalid: [
|
|
32
|
+
{ code: '<FormControl />', errors: [ { message: generateErrorText('FormControl') } ] },
|
|
33
|
+
{ code: '<fieldset />', errors: [ { message: generateErrorText('fieldset') } ] },
|
|
34
|
+
{ code: '<AnyFieldset />', errors: [ { message: generateErrorText('AnyFieldset') } ] },
|
|
35
|
+
{ code: 'const Hoge = <><AnyFormControl /></>', errors: [ { message: generateErrorText('AnyFormControl') } ] },
|
|
36
|
+
]
|
|
37
|
+
})
|