eslint-plugin-smarthr 0.5.16 → 0.5.17

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/.node-version ADDED
@@ -0,0 +1 @@
1
+ 20.11.1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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.17](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.16...v0.5.17) (2024-11-04)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-prohibit-heading-in-formを追加 ([#148](https://github.com/kufu/eslint-plugin-smarthr/issues/148)) ([973f681](https://github.com/kufu/eslint-plugin-smarthr/commit/973f681fd3538224e7312b6eb38ea43e6fb17cc5))
11
+
5
12
  ### [0.5.16](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.15...v0.5.16) (2024-10-25)
6
13
 
7
14
  ### [0.5.15](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.14...v0.5.15) (2024-09-10)
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - [a11y-numbered-text-within-ol](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-numbered-text-within-ol)
12
12
  - [a11y-prohibit-input-maxlength-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-maxlength-attribute)
13
13
  - [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
14
+ - [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-sectioning-content-in-form)
14
15
  - [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
15
16
  - [a11y-replace-unreadable-symbol](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-replace-unreadable-symbol)
16
17
  - [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -0,0 +1,62 @@
1
+ # smarthr/a11y-prohibit-sectioning-content-in-form
2
+
3
+ - form, fieldset, smarthr-ui/Fieldset 以下でSectioningContent(section, aside, article, nav)が利用されている場合、smarthr-ui/Fieldsetに置き換えることを促すルールです
4
+ - このルールを適用することで以下のようなメリットがあります
5
+ - form要素内からHeadingが取り除かれ、Fieldsetに統一されることにより、見出しを表現する要素がlegend, label要素のみになります
6
+ - これによってマークアップのルールが統一され、スクリーンリーダーのジャンプ機能などの利便性が向上します
7
+ - ジャンプ機能ではheading, legendは区別されるため統一されることで操作方法が単純化されます
8
+ - a11y-form-control-in-form と組み合わせることでより厳密なフォームのマークアップを行えます
9
+
10
+
11
+ ## rules
12
+
13
+ ```js
14
+ {
15
+ rules: {
16
+ 'smarthr/a11y-prohibit-sectioning-content-in-form': 'error', // 'warn', 'off'
17
+ },
18
+ }
19
+ ```
20
+
21
+ ## ❌ Incorrect
22
+
23
+ ```jsx
24
+ // form要素以下にSectionが存在するためNG
25
+ const AnyComponent = <form>
26
+ <Section>
27
+ <Heading>ANY TITLE.</Heading>
28
+ </Section>
29
+ </form>
30
+
31
+ // fieldset要素以下にAsideが存在するためNG
32
+ const AnyComponent = <Fieldset>
33
+ <Aside>
34
+ <Heading>ANY TITLE.</Heading>
35
+ </Aside>
36
+ </Fieldset>
37
+
38
+ // ファイル名、もしくは所属するディレクトリがform, fieldsetなどフォームに関連する名称になっている場合
39
+ // 内部でArticleを使っているとNG
40
+ const AnyComponent = <>
41
+ <Article>
42
+ <Heading>ANY TITLE.</Heading>
43
+ </Article>
44
+ </>
45
+ ```
46
+
47
+ ## ✅ Correct
48
+
49
+ ```jsx
50
+ // form内でSectioningContentを利用していないのでOK
51
+ const AnyComponent = <form>
52
+ <Fieldset title="ANY TITLE.">
53
+ Hoge.
54
+ <Fieldset title="ANY TITLE.">
55
+ Fuga.
56
+ <FormControl title="ANY TITLE.">
57
+ Piyo.
58
+ </FormControl>
59
+ </Fieldset>
60
+ </Fieldset>
61
+ </form>
62
+ ```
@@ -0,0 +1,196 @@
1
+ const { rootPath } = require('../../libs/common')
2
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
3
+
4
+ const SECTIONING_CONTENT_EXPECTED_NAMES = {
5
+ '(A|^a)rticle$': '(Article)$',
6
+ '(A|^a)side$': '(Aside)$',
7
+ '(N|^n)av$': '(Nav)$',
8
+ '(S|^s)ection$': '(Section)$',
9
+ }
10
+ const FIELDSET_EXPECTED_NAMES = {
11
+ '(FormControl)$': '(FormControl)$',
12
+ '(FormControls)$': '(FormControls)$',
13
+ '((F|^f)ieldset)$': '(Fieldset)$',
14
+ '(Fieldsets)$': '(Fieldsets)$',
15
+ }
16
+ const FORM_EXPECTED_NAMES = {
17
+ '((F|^f)orm)$': '(Form)$',
18
+ '(FormDialog)$': '(FormDialog)$',
19
+ 'RemoteTrigger(.*)FormDialog$': '(RemoteTrigger(.*)FormDialog)$',
20
+ }
21
+
22
+ const WRAPPER_EXPECTED_NAMES = {
23
+ ...FIELDSET_EXPECTED_NAMES,
24
+ ...FORM_EXPECTED_NAMES,
25
+ }
26
+
27
+ const EXPECTED_NAMES = {
28
+ ...SECTIONING_CONTENT_EXPECTED_NAMES,
29
+ ...WRAPPER_EXPECTED_NAMES,
30
+ 'SideNav$': '(SideNav)$',
31
+ 'IndexNav$': '(IndexNav)$',
32
+ }
33
+
34
+ const UNEXPECTED_NAMES = EXPECTED_NAMES
35
+
36
+ const asRegex = /^(as|forwardedAs)$/
37
+ const asFormRegex = /^(form|fieldset)$/
38
+ const asSectioningContentRegex = /^(article|aside|nav|section)$/
39
+
40
+ const includeAsAttrFormOrFieldset = (a) => a.name?.name.match(asRegex) && asFormRegex.test(a.value.value)
41
+ const includeAsAttrSectioningContent = (a) => a.name?.name.match(asRegex) && asSectioningContentRegex.test(a.value.value)
42
+ const includeWrapper = (fn) => wrapperRegex.test(fn)
43
+
44
+ const sectioningContentRegex = new RegExp(`(${Object.keys(SECTIONING_CONTENT_EXPECTED_NAMES).join('|')})`)
45
+ const wrapperRegex = new RegExp(`(${Object.keys(WRAPPER_EXPECTED_NAMES).join('|')})`)
46
+ const formControlRegex = new RegExp(`(${Object.keys(FIELDSET_EXPECTED_NAMES).join('|')})`)
47
+ const extRegex = /\.[a-z0-9]+?$/
48
+ const ignoreNavRegex = /(Side|Index)Nav$/
49
+ const formPartCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
50
+
51
+ const rootPathSlashed = `${rootPath}/`
52
+
53
+ const searchBubbleUpForSectioningContent = (node) => {
54
+ switch (node.type) {
55
+ case 'Program':
56
+ // rootまで検索した場合はOK
57
+ return null
58
+ case 'JSXElement':
59
+ const openingElement = node.openingElement
60
+ const elementName = openingElement.name.name
61
+
62
+ if (elementName) {
63
+ // formかFieldsetでラップされていればNG
64
+ if (wrapperRegex.test(elementName) || openingElement.attributes.some(includeAsAttrFormOrFieldset)) {
65
+ return node
66
+ } else if ((sectioningContentRegex.test(elementName) && !ignoreNavRegex.test(elementName)) || openingElement.attributes.some(includeAsAttrSectioningContent)) {
67
+ // 他のSectioningContentに到達した場合、同じチェックを繰り返すことになるため終了する
68
+ return null
69
+ }
70
+ }
71
+
72
+ break
73
+ // Form系コンポーネントの拡張なのでNG
74
+ case 'VariableDeclarator':
75
+ if (node.parent.parent?.type.match(formPartCheckParentTypeRegex) && wrapperRegex.test(node.id.name)) {
76
+ return node
77
+ }
78
+
79
+ break
80
+ // Form系コンポーネントの拡張なのでNG
81
+ case 'FunctionDeclaration':
82
+ if (formPartCheckParentTypeRegex.test(node.parent.type) && wrapperRegex.test(node.id.name)) {
83
+ return node
84
+ }
85
+
86
+ break
87
+ }
88
+
89
+ return searchBubbleUpForSectioningContent(node.parent)
90
+ }
91
+
92
+ const searchBubbleUpForFormControl = (node) => {
93
+ switch (node.type) {
94
+ case 'Program':
95
+ // rootまで検索した場合はOK
96
+ return null
97
+ case 'JSXElement':
98
+ const openingElement = node.openingElement
99
+ const elementName = openingElement.name.name
100
+
101
+ if (elementName) {
102
+ // SectioningContentでラップされていればNG
103
+ if (sectioningContentRegex.test(elementName) && !ignoreNavRegex.test(elementName)) {
104
+ return { node: openingElement, elementName }
105
+ } else {
106
+ const attr = openingElement.attributes.find(includeAsAttrSectioningContent)
107
+
108
+ if (attr) {
109
+ return { node: openingElement, elementName: `${attr.name.name}="${attr.value.value}"` }
110
+ } else if (wrapperRegex.test(elementName) || openingElement.attributes.some(includeAsAttrFormOrFieldset)) {
111
+ // 他のFormControl か Fieldsetに到達した場合、同じチェックを繰り返すことになるため終了する
112
+ return null
113
+ }
114
+ }
115
+ }
116
+
117
+ break
118
+ case 'VariableDeclarator':
119
+ if (node.parent.parent?.type.match(formPartCheckParentTypeRegex) && wrapperRegex.test(node.id.name)) {
120
+ return null
121
+ }
122
+
123
+ break
124
+ case 'FunctionDeclaration':
125
+ if (formPartCheckParentTypeRegex.test(node.parent.type) && wrapperRegex.test(node.id.name)) {
126
+ return null
127
+ }
128
+
129
+ break
130
+ }
131
+
132
+ return searchBubbleUpForFormControl(node.parent)
133
+ }
134
+
135
+ /**
136
+ * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
137
+ */
138
+ module.exports = {
139
+ meta: {
140
+ type: 'problem',
141
+ schema: [],
142
+ },
143
+ create(context) {
144
+ const filenames = context.getFilename().replace(rootPathSlashed, '').replace(extRegex, '').split('/')
145
+ const isInnerForm = filenames.some(includeWrapper)
146
+ const notified = []
147
+
148
+ return {
149
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
150
+ JSXOpeningElement: (node) => {
151
+ const elementName = node.name.name
152
+
153
+ if (elementName) {
154
+ // HINT: smarthr-ui/SideNav,IndexNav は対象外とする
155
+ if (ignoreNavRegex.test(elementName)) {
156
+ return
157
+ }
158
+
159
+ const isSection = sectioningContentRegex.test(elementName)
160
+ const asAttr = isSection ? false : node.attributes.find(includeAsAttrSectioningContent)
161
+
162
+ if ((isSection || asAttr)) {
163
+ if (isInnerForm || searchBubbleUpForSectioningContent(node.parent.parent)) {
164
+ if (!notified.includes(node)) {
165
+ notified.push(node)
166
+ context.report({
167
+ node,
168
+ message: `${isSection ? elementName : `${asAttr.name.name}="${asAttr.value.value}"`}とその内部に存在するHeadingをsmarthr-ui/Fieldsetに置き換えてください
169
+ - form内の見出しとなる要素をlegend, labelのみに統一することでスクリーンリーダーのジャンプ機能などの利便性が向上します
170
+ - smarthr-ui/Fieldset が利用できない場合、fieldset要素とlegend要素を使ったマークアップに修正してください
171
+ - その際、fieldset要素の直下にlegend要素が存在するようにしてください。他要素がfieldsetとlegendの間に存在すると、正しく紐づけが行われない場合があります`,
172
+ })
173
+ }
174
+ }
175
+ } else if (formControlRegex.test(elementName)) {
176
+ const sectioningContent = searchBubbleUpForFormControl(node.parent.parent)
177
+
178
+ if (sectioningContent) {
179
+ if (!notified.includes(sectioningContent.node)) {
180
+ notified.push(sectioningContent.node)
181
+ context.report({
182
+ node: sectioningContent.node,
183
+ message: `${sectioningContent.elementName}とその内部に存在するHeadingをsmarthr-ui/Fieldsetに置き換えてください
184
+ - form内の見出しとなる要素をlegend, labelのみに統一することでスクリーンリーダーのジャンプ機能などの利便性が向上します
185
+ - smarthr-ui/Fieldset が利用できない場合、fieldset要素とlegend要素を使ったマークアップに修正してください
186
+ - その際、fieldset要素の直下にlegend要素が存在するようにしてください。他要素がfieldsetとlegendの間に存在すると、正しく紐づけが行われない場合があります`,
187
+ })
188
+ }
189
+ }
190
+ }
191
+ }
192
+ },
193
+ }
194
+ },
195
+ }
196
+ module.exports.schema = []