eslint-plugin-smarthr 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -0
- package/package.json +5 -5
- package/rules/a11y-delegate-element-has-role-presentation/index.js +1 -1
- package/rules/a11y-input-has-name-attribute/index.js +64 -25
- package/rules/a11y-input-in-form-control/README.md +4 -4
- package/rules/a11y-input-in-form-control/index.js +1 -1
- package/rules/a11y-prohibit-input-placeholder/index.js +2 -2
- package/rules/best-practice-for-tailwind-variants/README.md +53 -0
- package/rules/best-practice-for-tailwind-variants/index.js +78 -0
- package/test/a11y-delegate-element-has-role-presantation.js +2 -2
- package/test/a11y-input-has-name-attribute.js +4 -1
- package/test/a11y-input-in-form-control.js +6 -3
- package/test/a11y-prohhibit-input-placeholder.js +6 -1
- package/test/best-practice-for-tailwind-variants.js +28 -0
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
|
+
## [1.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.2.0...eslint-plugin-smarthr-v1.3.0) (2025-02-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* tailwind-variantsの使い方をチェックするルールを追加 ([#480](https://github.com/kufu/tamatebako/issues/480)) ([1501f05](https://github.com/kufu/tamatebako/commit/1501f05e84492e7671a4dc95ef08b15360a1c309))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* [a11y-input-has-name-attribute]Inputにreact-hook-formのregisterが指定されている場合はエラーにならないようにする ([#490](https://github.com/kufu/tamatebako/issues/490)) ([2fc6abe](https://github.com/kufu/tamatebako/commit/2fc6abe9515a06dbf7e823da556e045402298a1b))
|
|
16
|
+
|
|
17
|
+
## [1.2.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.1.0...eslint-plugin-smarthr-v1.2.0) (2025-01-09)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* a11y系のルールをsmarthr-ui/WarekiPickerに対応させる ([#471](https://github.com/kufu/tamatebako/issues/471)) ([a52995c](https://github.com/kufu/tamatebako/commit/a52995cb999de2942989eac00bbc299922cf6208))
|
|
23
|
+
|
|
5
24
|
## [1.1.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.0.0...eslint-plugin-smarthr-v1.1.0) (2025-01-08)
|
|
6
25
|
|
|
7
26
|
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
- [best-practice-for-date](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-date)
|
|
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-remote-trigger-dialog](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-remote-trigger-dialog)
|
|
24
|
+
- [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
|
|
24
25
|
- [design-system-guideline-prohibit-double-icons](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-double-icons)
|
|
25
26
|
- [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path)
|
|
26
27
|
- [format-translate-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-translate-component)
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=20.18.
|
|
9
|
+
"node": ">=20.18.2"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "jest"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+git@github.com:kufu/tamatebako.git"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://github.com/kufu/tamatebako/packages/eslint-plugin-smarthr",
|
|
18
|
+
"homepage": "https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr",
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"registry": "https://registry.npmjs.org"
|
|
21
21
|
},
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"json5": "^2.2.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"typescript-eslint": "^8.
|
|
29
|
+
"typescript-eslint": "^8.19.1"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"eslint": "^9"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "3c7f6f40f11714a9dd6cb88dc243d1b6973c6a93"
|
|
41
41
|
}
|
|
@@ -8,7 +8,7 @@ const EXPECTED_NAMES = {
|
|
|
8
8
|
'RadioButtonPanel$': 'RadioButtonPanel$',
|
|
9
9
|
'Check(b|B)ox$': 'CheckBox$',
|
|
10
10
|
'Combo(b|B)ox$': 'ComboBox$',
|
|
11
|
-
'
|
|
11
|
+
'(Date|Wareki)Picker$': '(Date|Wareki)Picker$',
|
|
12
12
|
'TimePicker$': 'TimePicker$',
|
|
13
13
|
'DropZone$': 'DropZone$',
|
|
14
14
|
'Switch$': 'Switch$',
|
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const JSON5 = require('json5')
|
|
4
|
+
|
|
5
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
6
|
+
|
|
7
|
+
const OPTION = (() => {
|
|
8
|
+
const file = `${process.cwd()}/package.json`
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(file)) {
|
|
11
|
+
return {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const json = JSON5.parse(fs.readFileSync(file))
|
|
15
|
+
const dependencies = [...Object.keys(json.dependencies || {}), ...Object.keys(json.devDependencies || {})]
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
react_hook_form: dependencies.includes('react-hook-form'),
|
|
19
|
+
}
|
|
20
|
+
})()
|
|
2
21
|
|
|
3
22
|
const EXPECTED_NAMES = {
|
|
4
23
|
'(i|I)nput$': 'Input$',
|
|
5
24
|
'(t|T)extarea$': 'Textarea$',
|
|
6
25
|
'(s|S)elect$': 'Select$',
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
26
|
+
InputFile$: 'InputFile$',
|
|
27
|
+
RadioButton$: 'RadioButton$',
|
|
28
|
+
RadioButtonPanel$: 'RadioButtonPanel$',
|
|
10
29
|
'Check(b|B)ox$': 'CheckBox$',
|
|
11
30
|
'Combo(b|B)ox$': 'ComboBox$',
|
|
12
|
-
'
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
'(Date|Wareki)Picker$': '(Date|Wareki)Picker$',
|
|
32
|
+
TimePicker$: 'TimePicker$',
|
|
33
|
+
DropZone$: 'DropZone$',
|
|
15
34
|
}
|
|
16
35
|
const TARGET_TAG_NAME_REGEX = new RegExp(`(${Object.keys(EXPECTED_NAMES).join('|')})`)
|
|
17
36
|
const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/
|
|
18
37
|
const INPUT_TAG_REGEX = /(i|I)nput$/
|
|
19
38
|
const RADIO_BUTTON_REGEX = /RadioButton(Panel)?$/
|
|
20
39
|
|
|
21
|
-
const findNameAttr = (a) => a?.name?.name === 'name'
|
|
22
|
-
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
23
|
-
const findRadioInput = (a) => a.name?.name === 'type' && a.value.value === 'radio'
|
|
24
|
-
|
|
25
40
|
const MESSAGE_PART_FORMAT = `"${INPUT_NAME_REGEX.toString()}"にmatchするフォーマットで命名してください`
|
|
26
41
|
const MESSAGE_UNDEFINED_NAME_PART = `
|
|
27
42
|
- ブラウザの自動補完が有効化されるなどのメリットがあります
|
|
@@ -38,7 +53,7 @@ const SCHEMA = [
|
|
|
38
53
|
checkType: { type: 'string', enum: ['always', 'allow-spread-attributes'], default: 'always' },
|
|
39
54
|
},
|
|
40
55
|
additionalProperties: false,
|
|
41
|
-
}
|
|
56
|
+
},
|
|
42
57
|
]
|
|
43
58
|
|
|
44
59
|
/**
|
|
@@ -56,25 +71,49 @@ module.exports = {
|
|
|
56
71
|
return {
|
|
57
72
|
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
58
73
|
JSXOpeningElement: (node) => {
|
|
59
|
-
const
|
|
74
|
+
const { name, attributes } = node
|
|
75
|
+
const nodeName = name.name || ''
|
|
60
76
|
|
|
61
77
|
if (nodeName.match(TARGET_TAG_NAME_REGEX)) {
|
|
62
|
-
|
|
78
|
+
let nameAttr = null
|
|
79
|
+
let hasSpreadAttr = false
|
|
80
|
+
let hasReactHookFormRegisterSpreadAttr = false
|
|
81
|
+
let hasRadioInput = false
|
|
82
|
+
|
|
83
|
+
attributes.forEach((a) => {
|
|
84
|
+
if (a.type === 'JSXSpreadAttribute') {
|
|
85
|
+
hasSpreadAttr = true
|
|
86
|
+
|
|
87
|
+
if (hasReactHookFormRegisterSpreadAttr === false && a.argument?.callee?.name === 'register') {
|
|
88
|
+
hasReactHookFormRegisterSpreadAttr = true
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
switch (a.name?.name) {
|
|
92
|
+
case 'name': {
|
|
93
|
+
nameAttr = a
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
case 'type': {
|
|
97
|
+
if (a.value.value === 'radio') {
|
|
98
|
+
hasRadioInput = true
|
|
99
|
+
}
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
63
105
|
|
|
64
106
|
if (!nameAttr) {
|
|
65
107
|
if (
|
|
66
|
-
|
|
67
|
-
checkType !== 'allow-spread-attributes' ||
|
|
68
|
-
!node.attributes.some(findSpreadAttr)
|
|
108
|
+
!(OPTION.react_hook_form && hasReactHookFormRegisterSpreadAttr) &&
|
|
109
|
+
(attributes.length === 0 || checkType !== 'allow-spread-attributes' || !hasSpreadAttr)
|
|
69
110
|
) {
|
|
70
|
-
const isRadio =
|
|
71
|
-
nodeName.match(RADIO_BUTTON_REGEX) ||
|
|
72
|
-
(nodeName.match(INPUT_TAG_REGEX) && node.attributes.some(findRadioInput));
|
|
111
|
+
const isRadio = nodeName.match(RADIO_BUTTON_REGEX) || (nodeName.match(INPUT_TAG_REGEX) && hasRadioInput)
|
|
73
112
|
|
|
74
113
|
context.report({
|
|
75
114
|
node,
|
|
76
115
|
message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
|
|
77
|
-
})
|
|
116
|
+
})
|
|
78
117
|
}
|
|
79
118
|
} else {
|
|
80
119
|
const nameValue = nameAttr.value?.value || ''
|
|
@@ -83,12 +122,12 @@ module.exports = {
|
|
|
83
122
|
context.report({
|
|
84
123
|
node,
|
|
85
124
|
message: `${nodeName} のname属性の値(${nameValue})${MESSAGE_NAME_FORMAT_SUFFIX}`,
|
|
86
|
-
})
|
|
125
|
+
})
|
|
87
126
|
}
|
|
88
127
|
}
|
|
89
128
|
}
|
|
90
129
|
},
|
|
91
|
-
}
|
|
130
|
+
}
|
|
92
131
|
},
|
|
93
|
-
}
|
|
94
|
-
module.exports.schema = SCHEMA
|
|
132
|
+
}
|
|
133
|
+
module.exports.schema = SCHEMA
|
|
@@ -70,16 +70,16 @@
|
|
|
70
70
|
</Fieldset>
|
|
71
71
|
|
|
72
72
|
<FormControl title="any heading" role="group">
|
|
73
|
-
<
|
|
73
|
+
<WarekiPicker />
|
|
74
74
|
~
|
|
75
|
-
<
|
|
75
|
+
<WarekiPicker />
|
|
76
76
|
</FormControl>
|
|
77
77
|
|
|
78
78
|
<Fieldset title="any heading">
|
|
79
79
|
<FormControl role="group">
|
|
80
|
-
<
|
|
80
|
+
<WarekiPicker />
|
|
81
81
|
~
|
|
82
|
-
<
|
|
82
|
+
<WarekiPicker />
|
|
83
83
|
</FormControl>>
|
|
84
84
|
</Fieldset>
|
|
85
85
|
|
|
@@ -15,7 +15,7 @@ const EXPECTED_INPUT_NAMES = {
|
|
|
15
15
|
'(S|^s)elect$': '(Select)$',
|
|
16
16
|
'InputFile$': '(InputFile)$',
|
|
17
17
|
'Combo(b|B)ox$': '(ComboBox)$',
|
|
18
|
-
'
|
|
18
|
+
'(Date|Wareki)Picker$': '((Date|Wareki)Picker)$',
|
|
19
19
|
'TimePicker$': '(TimePicker)$',
|
|
20
20
|
...EXPECTED_LABELED_INPUT_NAMES,
|
|
21
21
|
}
|
|
@@ -6,10 +6,10 @@ const EXPECTED_NAMES = {
|
|
|
6
6
|
'(t|T)extarea$': 'Textarea$',
|
|
7
7
|
'FieldSet$': 'FieldSet$',
|
|
8
8
|
'ComboBox$': 'ComboBox$',
|
|
9
|
-
'
|
|
9
|
+
'(Date|Wareki)Picker$': '(Date|Wareki)Picker$',
|
|
10
10
|
'TimePicker$': 'TimePicker$',
|
|
11
11
|
}
|
|
12
|
-
const INPUT_TAG_REGEX = /((i|I)nput|(t|T)extarea|FieldSet|ComboBox|(Date|Time)Picker)$/
|
|
12
|
+
const INPUT_TAG_REGEX = /((i|I)nput|(t|T)extarea|FieldSet|ComboBox|(Date|Wareki|Time)Picker)$/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# smarthr/best-practice-for-tailwind-variants
|
|
2
|
+
|
|
3
|
+
- tailwind-variantsの記法をチェックするルールです
|
|
4
|
+
- tailwind-variantsの使い方に一定のルールを課すことで、実装のブレを無くし、レビューコストなどを下げることが目的です
|
|
5
|
+
- tv関数の実行結果を格納する変数の名称を一定のルールにすることで可読性を向上させます
|
|
6
|
+
- また、上記関数を実行する際、一律useMemoを利用することを促します
|
|
7
|
+
- useMemo内にtailwind系の実行をまとめることで将来的にtailwind-variantsから乗り換える際の作業を楽にする、などmemo化以外の効果もあります
|
|
8
|
+
- まとめると現状以下のチェックを行います
|
|
9
|
+
- tailwind-variants(tv) のimport時の名称をtvに固定しているか (asなどでの名称変更の禁止)
|
|
10
|
+
- tv の実行結果を格納する変数名を統一 (classNameGenerator、もしくはxxxClassNameGenerator)
|
|
11
|
+
- tvで生成した関数の実行をuseMemo hook でメモ化しているか
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## rules
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
{
|
|
18
|
+
rules: {
|
|
19
|
+
'smarthr/best-practice-for-tailwind-variants': 'error', // 'warn', 'off'
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## ❌ Incorrect
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
import { tv as hoge } from 'tailwind-variants'
|
|
28
|
+
```
|
|
29
|
+
```jsx
|
|
30
|
+
import { tv } from 'tailwind-variants'
|
|
31
|
+
|
|
32
|
+
const xxx = tv({
|
|
33
|
+
...
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
const yyyy = xxx()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## ✅ Correct
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { tv } from 'tailwind-variants'
|
|
45
|
+
|
|
46
|
+
const classNameGenerator = tv({
|
|
47
|
+
...
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
const yyyy = useMemo(() => classNameGenerator(), [])
|
|
53
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const TV_COMPONENTS_METHOD = 'tv'
|
|
4
|
+
const TV_COMPONENTS = 'tailwind-variants'
|
|
5
|
+
const TV_RESULT_CONST_NAME_REGEX = /(C|c)lassNameGenerator$/
|
|
6
|
+
|
|
7
|
+
const findValidImportNameNode = (s) => s.type === 'ImportSpecifier' && s.local.name === TV_COMPONENTS_METHOD
|
|
8
|
+
|
|
9
|
+
const checkImportTailwindVariants = (node, context) => {
|
|
10
|
+
}
|
|
11
|
+
const findNodeHasId = (node) => {
|
|
12
|
+
if (node.id) {
|
|
13
|
+
return node
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (node.parent) {
|
|
17
|
+
return findNodeHasId(node.parent)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
const findNodeUseMemo = (node) => {
|
|
23
|
+
if (node.type === 'CallExpression' && node.callee.name === 'useMemo') {
|
|
24
|
+
return node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.parent) {
|
|
28
|
+
return findNodeUseMemo(node.parent)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
36
|
+
*/
|
|
37
|
+
module.exports = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'problem',
|
|
40
|
+
schema: SCHEMA,
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
return {
|
|
44
|
+
ImportDeclaration: (node) => {
|
|
45
|
+
if (node.source.value === TV_COMPONENTS) {
|
|
46
|
+
if (!node.specifiers.some(findValidImportNameNode)) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
message: `${TV_COMPONENTS} をimportする際は、名称が"${TV_COMPONENTS_METHOD}" となるようにしてください。例: "import { ${TV_COMPONENTS_METHOD} } from '${TV_COMPONENTS}'"`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
CallExpression: (node) => {
|
|
55
|
+
if (node.callee.name === TV_COMPONENTS_METHOD) {
|
|
56
|
+
const idNode = findNodeHasId(node.parent)
|
|
57
|
+
|
|
58
|
+
if (idNode && !TV_RESULT_CONST_NAME_REGEX.test(idNode.id.name)) {
|
|
59
|
+
context.report({
|
|
60
|
+
node: idNode,
|
|
61
|
+
message: `${TV_COMPONENTS_METHOD}の実行結果を格納する変数名は "${idNode.id.name}" ではなく "${TV_RESULT_CONST_NAME_REGEX}"にmatchする名称に統一してください。`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} else if (TV_RESULT_CONST_NAME_REGEX.test(node.callee.name)) {
|
|
65
|
+
const useMemoNode = findNodeUseMemo(node.parent)
|
|
66
|
+
|
|
67
|
+
if (!useMemoNode) {
|
|
68
|
+
context.report({
|
|
69
|
+
node,
|
|
70
|
+
message: `"${node.callee.name}" を実行する際、useMemoでラップし、メモ化してください`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
module.exports.schema = SCHEMA
|
|
@@ -11,7 +11,7 @@ const ruleTester = new RuleTester({
|
|
|
11
11
|
},
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
-
const defaultInteractiveRegex = '/((i|I)nput$|(t|T)extarea$|(s|S)elect$|InputFile$|RadioButtonPanel$|Check(b|B)ox$|Combo(b|B)ox$|
|
|
14
|
+
const defaultInteractiveRegex = '/((i|I)nput$|(t|T)extarea$|(s|S)elect$|InputFile$|RadioButtonPanel$|Check(b|B)ox$|Combo(b|B)ox$|(Date|Wareki)Picker$|TimePicker$|DropZone$|Switch$|SegmentedControl$|RightFixedNote$|FieldSet$|Fieldset$|FormControl$|FormGroup$|(b|B)utton$|Anchor$|Link$|TabItem$|^a$|(f|F)orm$|ActionDialogWithTrigger$|RemoteDialogTrigger$|RemoteTrigger(.+)Dialog$|FormDialog$|Pagination$|SideNav$|AccordionPanel$|FilterDropdown$)/'
|
|
15
15
|
const messageNonInteractiveEventHandler = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => {
|
|
16
16
|
const onAttrsText = onAttrs.join(', ')
|
|
17
17
|
|
|
@@ -66,7 +66,7 @@ ruleTester.run('smarthr/a11y-delegate-element-has-role-presentation', rule, {
|
|
|
66
66
|
{ code: '<div onClick={any} onSubmit={any2} role="presentation"><Hoge /></div>', errors: [ { message: messageRolePresentationNotHasInteractive('div', ['onClick', 'onSubmit']) } ] },
|
|
67
67
|
{ code: '<div onClick={any}><Link /></div>', errors: [ { message: messageNonInteractiveEventHandler('div', ['onClick']) } ] },
|
|
68
68
|
{ code: '<Wrapper onClick={any}><Link /></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick']) } ] },
|
|
69
|
-
{ code: '<Wrapper onSubmit={any}><Hoge /></Wrapper>', options: [{ additionalInteractiveComponentRegex: ['^Hoge$'] }], errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onSubmit'], '/((i|I)nput$|(t|T)extarea$|(s|S)elect$|InputFile$|RadioButtonPanel$|Check(b|B)ox$|Combo(b|B)ox$|
|
|
69
|
+
{ code: '<Wrapper onSubmit={any}><Hoge /></Wrapper>', options: [{ additionalInteractiveComponentRegex: ['^Hoge$'] }], errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onSubmit'], '/((i|I)nput$|(t|T)extarea$|(s|S)elect$|InputFile$|RadioButtonPanel$|Check(b|B)ox$|Combo(b|B)ox$|(Date|Wareki)Picker$|TimePicker$|DropZone$|Switch$|SegmentedControl$|RightFixedNote$|FieldSet$|Fieldset$|FormControl$|FormGroup$|(b|B)utton$|Anchor$|Link$|TabItem$|^a$|(f|F)orm$|ActionDialogWithTrigger$|RemoteDialogTrigger$|RemoteTrigger(.+)Dialog$|FormDialog$|Pagination$|SideNav$|AccordionPanel$|FilterDropdown$|^Hoge$)/') } ] },
|
|
70
70
|
{ code: '<Wrapper onClick={any} onChange={anyany}><any><Link /></any></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick', 'onChange']) } ] },
|
|
71
71
|
{ code: '<Wrapper onClick={any}>{any ? null : (hoge ? <AnyLink /> : null)}</Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick']) } ] },
|
|
72
72
|
],
|
|
@@ -30,6 +30,7 @@ ruleTester.run('a11y-input-has-name-attribute', rule, {
|
|
|
30
30
|
{ code: `import { CheckBox as FugaCheckBox } from './hoge'` },
|
|
31
31
|
{ code: `import { HogeComboBox as FugaComboBox } from './hoge'` },
|
|
32
32
|
{ code: `import { DatePicker as HogeDatePicker } from './hoge'` },
|
|
33
|
+
{ code: `import { WarekiPicker as HogeWarekiPicker } from './hoge'` },
|
|
33
34
|
{ code: `import { HogeDropZone as HogeFugaDropZone } from './hoge'` },
|
|
34
35
|
{ code: 'const HogeInput = styled.input``' },
|
|
35
36
|
{ code: 'const HogeInput = styled(Input)``' },
|
|
@@ -65,8 +66,10 @@ ruleTester.run('a11y-input-has-name-attribute', rule, {
|
|
|
65
66
|
- CheckBoxが型の場合、'import type { CheckBox as FugaCheckBoxHoge }' もしくは 'import { type CheckBox as FugaCheckBoxHoge }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
66
67
|
{ code: `import { HogeComboBox as ComboBoxFuga } from './hoge'`, errors: [ { message: `ComboBoxFugaを正規表現 "/ComboBox$/" がmatchする名称に変更してください。
|
|
67
68
|
- HogeComboBoxが型の場合、'import type { HogeComboBox as ComboBoxFuga }' もしくは 'import { type HogeComboBox as ComboBoxFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
68
|
-
{ code: `import { DatePicker as HogeDatePickerFuga } from './hoge'`, errors: [ { message: `HogeDatePickerFugaを正規表現 "/
|
|
69
|
+
{ code: `import { DatePicker as HogeDatePickerFuga } from './hoge'`, errors: [ { message: `HogeDatePickerFugaを正規表現 "/(Date|Wareki)Picker$/" がmatchする名称に変更してください。
|
|
69
70
|
- DatePickerが型の場合、'import type { DatePicker as HogeDatePickerFuga }' もしくは 'import { type DatePicker as HogeDatePickerFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
71
|
+
{ code: `import { WarekiPicker as HogeWarekiPickerFuga } from './hoge'`, errors: [ { message: `HogeWarekiPickerFugaを正規表現 "/(Date|Wareki)Picker$/" がmatchする名称に変更してください。
|
|
72
|
+
- WarekiPickerが型の場合、'import type { WarekiPicker as HogeWarekiPickerFuga }' もしくは 'import { type WarekiPicker as HogeWarekiPickerFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
70
73
|
{ code: `import { HogeDropZone as HogeFugaDropZoneAbc } from './hoge'`, errors: [ { message: `HogeFugaDropZoneAbcを正規表現 "/DropZone$/" がmatchする名称に変更してください。
|
|
71
74
|
- HogeDropZoneが型の場合、'import type { HogeDropZone as HogeFugaDropZoneAbc }' もしくは 'import { type HogeDropZone as HogeFugaDropZoneAbc }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
72
75
|
{ code: 'const Hoge = styled.input``', errors: [ { message: `Hogeを正規表現 "/Input$/" がmatchする名称に変更してください。` } ] },
|
|
@@ -13,13 +13,13 @@ const ruleTester = new RuleTester({
|
|
|
13
13
|
const noLabeledInput = (name) => `${name} を、smarthr-ui/FormControl もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
|
|
14
14
|
- FormControlで入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります
|
|
15
15
|
- ${name}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/とマッチするように修正してください
|
|
16
|
-
- ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|
|
|
16
|
+
- ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|(Date|Wareki)Picker$|TimePicker$|RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/とマッチするように修正してください
|
|
17
17
|
- 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を入力すれば良いのか" の説明を設定してください
|
|
18
18
|
- 例: <${name} title="姓を全角カタカナのみで入力してください" />`
|
|
19
19
|
const noLabeledSelect = (name) => `${name} を、smarthr-ui/FormControl もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
|
|
20
20
|
- FormControlで入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります
|
|
21
21
|
- ${name}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/とマッチするように修正してください
|
|
22
|
-
- ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|
|
|
22
|
+
- ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|(Date|Wareki)Picker$|TimePicker$|RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/とマッチするように修正してください
|
|
23
23
|
- 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を選択すれば良いのか" の説明を設定してください
|
|
24
24
|
- 例: <${name} title="検索対象を選択してください" />`
|
|
25
25
|
const invalidPureCheckboxInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。
|
|
@@ -74,7 +74,7 @@ const invalidChildreninFormControl = (children) => `FormControl が、${children
|
|
|
74
74
|
- FormControlではなく、smarthr-ui/Fieldset、もしくはsmarthr-ui/Section + smarthr-ui/Heading などでのマークアップを検討してください
|
|
75
75
|
- 方法2: 親要素であるFormControlがsmarthr-ui/FormControlを拡張したコンポーネントではない場合、コンポーネント名を/(Form(Control|Group))$/と一致しない名称に変更してください`
|
|
76
76
|
const requireMultiInputInFormControlWithRoleGroup = () => `HogeFormControl内に入力要素が2個以上存在しないため、'role=\"group\"'を削除してください。'role=\"group\"'は複数の入力要素を一つのグループとして扱うための属性です。
|
|
77
|
-
- HogeFormControl内に2つ以上の入力要素が存在する場合、入力要素を含むコンポーネント名全てを/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|
|
|
77
|
+
- HogeFormControl内に2つ以上の入力要素が存在する場合、入力要素を含むコンポーネント名全てを/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|(Date|Wareki)Picker$|TimePicker$|RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/、もしくは/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/にマッチする名称に変更してください`
|
|
78
78
|
|
|
79
79
|
ruleTester.run('a11y-input-in-form-control', rule, {
|
|
80
80
|
valid: [
|
|
@@ -90,6 +90,7 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
90
90
|
{ code: 'const HogeRadioButtonPanel = styled(FugaRadioButtonPanel)``' },
|
|
91
91
|
{ code: 'const HogeCheckBox = styled(FugaCheckbox)``' },
|
|
92
92
|
{ code: 'const DatePicker = styled(AnyDatePicker)``' },
|
|
93
|
+
{ code: 'const WarekiPicker = styled(AnyWarekiPicker)``' },
|
|
93
94
|
{ code: '<input type="hidden" />' },
|
|
94
95
|
{ code: '<input title="any"/>' },
|
|
95
96
|
{ code: '<HogeInput title="any"/>' },
|
|
@@ -100,6 +101,7 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
100
101
|
{ code: '<HogeInputFile title="any"/>' },
|
|
101
102
|
{ code: '<HogeComboBox title="any"/>' },
|
|
102
103
|
{ code: '<HogeDatePicker title="any"/>' },
|
|
104
|
+
{ code: '<HogeWarekiPicker title="any"/>' },
|
|
103
105
|
{ code: '<HogeFormGroup />' },
|
|
104
106
|
{ code: '<HogeFormControl />' },
|
|
105
107
|
{ code: '<HogeFieldset />' },
|
|
@@ -158,6 +160,7 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
158
160
|
{ code: '<HogeComboBox />', errors: [ { message: noLabeledInput('HogeComboBox') } ] },
|
|
159
161
|
{ code: '<HogeComboBox inputAttributes={{ any }} />', errors: [ { message: noLabeledInput('HogeComboBox') } ] },
|
|
160
162
|
{ code: '<HogeDatePicker />', errors: [ { message: noLabeledInput('HogeDatePicker') } ] },
|
|
163
|
+
{ code: '<HogeWarekiPicker />', errors: [ { message: noLabeledInput('HogeWarekiPicker') } ] },
|
|
161
164
|
{ code: '<HogeFormControl><Input type="checkbox" /><Input type="checkbox" /></HogeFormControl>', errors: [ { message: invalidPureCheckboxInFormControl('Input') } ] },
|
|
162
165
|
{ code: '<HogeFormControl><HogeCheckBox /><Input /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
|
|
163
166
|
{ code: '<HogeFormControl><HogeCheckBox /><HogeCheckBox /></HogeFormControl>', errors: [ { message: invalidCheckboxInFormControl('HogeCheckBox') } ] },
|
|
@@ -20,6 +20,7 @@ ruleTester.run('a11y-prohibit-input-placeholder', rule, {
|
|
|
20
20
|
{ code: `import { HogeTextarea as FugaHogeTextarea } from './hoge'` },
|
|
21
21
|
{ code: `import { ComboBox as FugaHogeComboBox } from './hoge'` },
|
|
22
22
|
{ code: `import { AbcDatePicker as StyledDatePicker } from './hoge'` },
|
|
23
|
+
{ code: `import { AbcWarekiPicker as StyledWarekiPicker } from './hoge'` },
|
|
23
24
|
{ code: 'const HogeInput = styled.input``' },
|
|
24
25
|
{ code: 'const HogeTextarea = styled.textarea``' },
|
|
25
26
|
{ code: 'const HogeInput = styled(Input)``' },
|
|
@@ -39,6 +40,7 @@ ruleTester.run('a11y-prohibit-input-placeholder', rule, {
|
|
|
39
40
|
{ code: `<CustomComboBox />` },
|
|
40
41
|
{ code: `<SearchInput />` },
|
|
41
42
|
{ code: `<DatePicker />` },
|
|
43
|
+
{ code: `<WarekiPicker />` },
|
|
42
44
|
{ code: `<TimePicker />` },
|
|
43
45
|
{ code: `<CustomSearchInput tooltipMessage="hoge" />` },
|
|
44
46
|
{ code: `<CustomSearchInput tooltipMessage="hoge" placeholder="fuga" />` },
|
|
@@ -57,8 +59,10 @@ ruleTester.run('a11y-prohibit-input-placeholder', rule, {
|
|
|
57
59
|
- HogeTextareaが型の場合、'import type { HogeTextarea as HogeTextareaFuga }' もしくは 'import { type HogeTextarea as HogeTextareaFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
58
60
|
{ code: `import { HogeComboBox as ComboBoxFuga } from './hoge'`, errors: [ { message: `ComboBoxFugaを正規表現 "/ComboBox$/" がmatchする名称に変更してください。
|
|
59
61
|
- HogeComboBoxが型の場合、'import type { HogeComboBox as ComboBoxFuga }' もしくは 'import { type HogeComboBox as ComboBoxFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
60
|
-
{ code: `import { DatePicker as HogeDatePickerFuga } from './hoge'`, errors: [ { message: `HogeDatePickerFugaを正規表現 "/
|
|
62
|
+
{ code: `import { DatePicker as HogeDatePickerFuga } from './hoge'`, errors: [ { message: `HogeDatePickerFugaを正規表現 "/(Date|Wareki)Picker$/" がmatchする名称に変更してください。
|
|
61
63
|
- DatePickerが型の場合、'import type { DatePicker as HogeDatePickerFuga }' もしくは 'import { type DatePicker as HogeDatePickerFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
64
|
+
{ code: `import { WarekiPicker as HogeWarekiPickerFuga } from './hoge'`, errors: [ { message: `HogeWarekiPickerFugaを正規表現 "/(Date|Wareki)Picker$/" がmatchする名称に変更してください。
|
|
65
|
+
- WarekiPickerが型の場合、'import type { WarekiPicker as HogeWarekiPickerFuga }' もしくは 'import { type WarekiPicker as HogeWarekiPickerFuga }' のように明示的に型であることを宣言してください。名称変更が不要になります` } ] },
|
|
62
66
|
{ code: 'const Hoge = styled.input``', errors: [ { message: `Hogeを正規表現 "/Input$/" がmatchする名称に変更してください。` } ] },
|
|
63
67
|
{ code: 'const Hoge = styled(StyledInput)``', errors: [ { message: `Hogeを正規表現 "/Input$/" がmatchする名称に変更してください。` } ] },
|
|
64
68
|
{ code: 'const Hoge = styled.textarea``', errors: [ { message: `Hogeを正規表現 "/Textarea$/" がmatchする名称に変更してください。` } ] },
|
|
@@ -78,6 +82,7 @@ ruleTester.run('a11y-prohibit-input-placeholder', rule, {
|
|
|
78
82
|
{ code: `<HogeTextarea placeholder="any" />`, errors: [ { message: `HogeTextarea にはplaceholder属性は設定せず、別途ヒント用要素の利用を検討してください。(例: '<div><HogeTextarea /><Hint>ヒント</Hint></div>')` } ] },
|
|
79
83
|
{ code: `<HogeFieldSet placeholder="any" />`, errors: [ { message: `HogeFieldSet にはplaceholder属性は設定せず、別途ヒント用要素の利用を検討してください。(例: '<div><HogeFieldSet /><Hint>ヒント</Hint></div>')` } ] },
|
|
80
84
|
{ code: `<HogeDatePicker placeholder="any" />`, errors: [ { message: `HogeDatePicker にはplaceholder属性は設定せず、別途ヒント用要素の利用を検討してください。(例: '<div><HogeDatePicker /><Hint>ヒント</Hint></div>')` } ] },
|
|
85
|
+
{ code: `<HogeWarekiPicker placeholder="any" />`, errors: [ { message: `HogeWarekiPicker にはplaceholder属性は設定せず、別途ヒント用要素の利用を検討してください。(例: '<div><HogeWarekiPicker /><Hint>ヒント</Hint></div>')` } ] },
|
|
81
86
|
{ code: `<HogeComboBox placeholder="any" />`, errors: [ { message: `HogeComboBox にはplaceholder属性は設定せず、以下のいずれか、もしくは組み合わせての対応を検討してください。
|
|
82
87
|
- 選択肢をどんな値で絞り込めるかの説明をしたい場合は dropdownHelpMessage 属性に変更してください。
|
|
83
88
|
- 空の値の説明のためにplaceholderを利用している場合は defaultItem 属性に変更してください。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-tailwind-variants')
|
|
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
|
+
ruleTester.run('best-practice-for-button-element', rule, {
|
|
15
|
+
valid: [
|
|
16
|
+
{ code: `import { tv } from 'tailwind-variants'` },
|
|
17
|
+
{ code: `const classNameGenerator = tv()` },
|
|
18
|
+
{ code: `const xxxClassNameGenerator = tv()` },
|
|
19
|
+
{ code: `const hoge = useMemo(() => classNameGenerator(), [])` },
|
|
20
|
+
{ code: `const xxx = useMemo(() => hogeClassNameGenerator(), [])` },
|
|
21
|
+
],
|
|
22
|
+
invalid: [
|
|
23
|
+
{ code: `import { tv as hoge } from 'tailwind-variants'`, errors: [ { message: `tailwind-variants をimportする際は、名称が"tv" となるようにしてください。例: "import { tv } from 'tailwind-variants'"` } ] },
|
|
24
|
+
{ code: `const hoge = tv()`, errors: [ { message: `tvの実行結果を格納する変数名は "hoge" ではなく "/(C|c)lassNameGenerator$/"にmatchする名称に統一してください。` } ] },
|
|
25
|
+
{ code: `const hoge = classNameGenerator()`, errors: [ { message: `"classNameGenerator" を実行する際、useMemoでラップし、メモ化してください` } ] },
|
|
26
|
+
{ code: `const hoge = hogeClassNameGenerator()`, errors: [ { message: `"hogeClassNameGenerator" を実行する際、useMemoでラップし、メモ化してください` } ] },
|
|
27
|
+
]
|
|
28
|
+
})
|