eslint-plugin-smarthr 0.5.17 → 0.5.19

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,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
+ ### [0.5.19](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.18...v0.5.19) (2024-11-25)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * エラーメッセージ生成ミスを修正 ([4ea11f1](https://github.com/kufu/eslint-plugin-smarthr/commit/4ea11f18028de2fe87842476f92325f48f8bbf49))
11
+
12
+ ### [0.5.18](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.17...v0.5.18) (2024-11-25)
13
+
14
+
15
+ ### Features
16
+
17
+ * a11y-requried-layout-as-attribute ルールを追加する ([#150](https://github.com/kufu/eslint-plugin-smarthr/issues/150)) ([96e0970](https://github.com/kufu/eslint-plugin-smarthr/commit/96e09702ea59cfcd5a173e187300b9054047b65c))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * a11y-form-control-in-form でFilterDropdownをformとして扱うように修正 ([#151](https://github.com/kufu/eslint-plugin-smarthr/issues/151)) ([9d86ef8](https://github.com/kufu/eslint-plugin-smarthr/commit/9d86ef885b607582324df24e5743d42533108db3))
23
+
5
24
  ### [0.5.17](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.16...v0.5.17) (2024-11-04)
6
25
 
7
26
 
package/README.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-sectioning-content-in-form)
15
15
  - [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
16
16
  - [a11y-replace-unreadable-symbol](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-replace-unreadable-symbol)
17
+ - [a11y-required-layout-as-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-required-layout-as-attribute)
17
18
  - [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
18
19
  - [best-practice-for-button-element](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-button-element)
19
20
  - [best-practice-for-data-test-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-data-test-attribute)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -14,6 +14,7 @@ const FORM_EXPECTED_NAMES = {
14
14
  '((F|^f)orm)$': '(Form)$',
15
15
  '(FormDialog)$': '(FormDialog)$',
16
16
  'RemoteTrigger(.*)FormDialog$': 'RemoteTrigger(.*)FormDialog$',
17
+ 'FilterDropdown$': '(FilterDropdown)$',
17
18
  }
18
19
  const EXPECTED_NAMES = {
19
20
  ...FORM_CONTROL_EXPECTED_NAMES,
@@ -11,6 +11,7 @@ const EXPECTED_NAMES = {
11
11
  'Section$': 'Section$',
12
12
  'ModelessDialog$': 'ModelessDialog$',
13
13
  'Center$': 'Center$',
14
+ 'Cluster$': '(Cluster)$',
14
15
  'Reel$': 'Reel$',
15
16
  'Sidebar$': 'Sidebar$',
16
17
  'Stack$': 'Stack$',
@@ -41,6 +42,7 @@ const UNEXPECTED_NAMES = {
41
42
  unexpectedMessageTemplate,
42
43
  ],
43
44
  'Center$': '(Center)$',
45
+ 'Cluster$': '(Cluster)$',
44
46
  'Reel$': '(Reel)$',
45
47
  'Sidebar$': '(Sidebar)$',
46
48
  'Stack$': '(Stack)$',
@@ -166,6 +166,10 @@ module.exports = {
166
166
  context.report({
167
167
  node,
168
168
  message: `${isSection ? elementName : `${asAttr.name.name}="${asAttr.value.value}"`}とその内部に存在するHeadingをsmarthr-ui/Fieldsetに置き換えてください
169
+ - もしくはform要素を利用していない場合、フォームを構成する入力要素郡すべてを一つのform要素で囲んでください
170
+ - required属性、pattern属性など一部属性はform要素で囲まないと動作しません
171
+ - 送信用ボタンのonClickをform要素のonSubmitに移動し、送信用ボタンのtype属性に "submit" を指定することでより適切にマークアップ出来ます
172
+ - その際、onSubmitの動作中で "e.preventDefault()" と "e.stopPropagation()" を指定する必要がある場合があります。
169
173
  - form内の見出しとなる要素をlegend, labelのみに統一することでスクリーンリーダーのジャンプ機能などの利便性が向上します
170
174
  - smarthr-ui/Fieldset が利用できない場合、fieldset要素とlegend要素を使ったマークアップに修正してください
171
175
  - その際、fieldset要素の直下にlegend要素が存在するようにしてください。他要素がfieldsetとlegendの間に存在すると、正しく紐づけが行われない場合があります`,
@@ -181,6 +185,10 @@ module.exports = {
181
185
  context.report({
182
186
  node: sectioningContent.node,
183
187
  message: `${sectioningContent.elementName}とその内部に存在するHeadingをsmarthr-ui/Fieldsetに置き換えてください
188
+ - もしくはform要素を利用していない場合、フォームを構成する入力要素郡すべてを一つのform要素で囲んでください
189
+ - required属性、pattern属性など一部属性はform要素で囲まないと動作しません
190
+ - 送信用ボタンのonClickをform要素のonSubmitに移動し、送信用ボタンに `type="submit"` を指定することでより適切にマークアップ出来ます
191
+ - その際、onSubmitの動作中で "e.preventDefault()" と "e.stopPropagation()" を指定する必要がある場合があります。
184
192
  - form内の見出しとなる要素をlegend, labelのみに統一することでスクリーンリーダーのジャンプ機能などの利便性が向上します
185
193
  - smarthr-ui/Fieldset が利用できない場合、fieldset要素とlegend要素を使ったマークアップに修正してください
186
194
  - その際、fieldset要素の直下にlegend要素が存在するようにしてください。他要素がfieldsetとlegendの間に存在すると、正しく紐づけが行われない場合があります`,
@@ -0,0 +1,54 @@
1
+ # smarthr/a11y-required-layout-as-attribute
2
+
3
+ - smarthr-ui/Layoutに属するコンポーネントはデフォルトではdiv要素を出力します
4
+ - そのため他の一部コンポーネントとの組み合わせによってはinvalidなマークアップになる場合が起こり得ます
5
+ - 例: FormControlのtitle属性内でClusterを使うと `label > div` の構造になるためinvalid
6
+ - 対象コンポーネントの使用方法をチェックし、適切なマークアップになるよう、as・forwardedAs属性の利用を促します
7
+
8
+ ## rules
9
+
10
+ ```js
11
+ {
12
+ rules: {
13
+ 'smarthr/a11y-required-layout-as-attribute': [
14
+ 'error', // 'warn', 'off'
15
+ ]
16
+ },
17
+ }
18
+ ```
19
+
20
+ ## ❌ Incorrect
21
+
22
+ ```jsx
23
+ <Heading>
24
+ <Cluster>any</Cluster>
25
+ </Heading>
26
+
27
+ <HogeFormControl title={
28
+ <FugaCluster>any</FugaCluster>
29
+ } />
30
+
31
+ <StyledFieldset title={
32
+ <Cluster>any</Cluster>
33
+ }>
34
+ // any
35
+ </StyledFieldset>
36
+ ```
37
+
38
+ ## ✅ Correct
39
+
40
+ ```jsx
41
+ <Heading>
42
+ <Cluster as="span">any</Cluster>
43
+ </Heading>
44
+
45
+ <HogeFormControl title={
46
+ <FugaCluster forwardedAs="span">any</FugaCluster>
47
+ } />
48
+
49
+ <StyledFieldset title={
50
+ <Cluster as="strong">any</Cluster>
51
+ }>
52
+ // any
53
+ </StyledFieldset>
54
+ ```
@@ -0,0 +1,90 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
+
3
+ const LAYOUT_EXPECTED_NAMES = {
4
+ 'Center$': '(Center)$',
5
+ 'Cluster$': '(Cluster)$',
6
+ 'Reel$': '(Reel)$',
7
+ 'Sidebar$': '(Sidebar)$',
8
+ 'Stack$': '(Stack)$',
9
+ 'Base$': '(Base)$',
10
+ 'BaseColumn$': '(BaseColumn)$',
11
+ }
12
+ const EXPECTED_NAMES = {
13
+ ...LAYOUT_EXPECTED_NAMES,
14
+ 'PageHeading$': '(PageHeading)$',
15
+ 'Heading$': '(Heading)$',
16
+ '^h1$': '(PageHeading)$',
17
+ '^h(|2|3|4|5|6)$': '(Heading)$',
18
+ }
19
+
20
+ const UNEXPECTED_NAMES = {
21
+ ...LAYOUT_EXPECTED_NAMES,
22
+ '(Heading|^h(1|2|3|4|5|6))$': '(Heading)$',
23
+ }
24
+
25
+ const layoutRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/
26
+ const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
27
+ const asRegex = /^(as|forwardedAs)$/
28
+ const formControlRegex = /(FormControl|Fieldset)$/
29
+
30
+ const findAsAttr = (a) => a.name?.name.match(asRegex)
31
+
32
+ const searchBubbleUp = (node) => {
33
+ switch (node.type) {
34
+ case 'Program':
35
+ // rootまで検索した場合は確定でエラーにする
36
+ return null
37
+ case 'JSXElement': {
38
+ const name = node.openingElement.name.name || ''
39
+
40
+ if (headingRegex.test(name)) {
41
+ return name
42
+ }
43
+
44
+ break
45
+ }
46
+ case 'JSXAttribute': {
47
+ const name = node.name.name || ''
48
+
49
+ if (name === 'title' && formControlRegex.test(node.parent.name.name)) {
50
+ return `${node.parent.name.name}のtitle属性`
51
+ }
52
+ }
53
+ }
54
+
55
+ return searchBubbleUp(node.parent)
56
+ }
57
+
58
+ /**
59
+ * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
60
+ */
61
+ module.exports = {
62
+ meta: {
63
+ type: 'problem',
64
+ schema: [],
65
+ },
66
+ create(context) {
67
+ return {
68
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
69
+ JSXOpeningElement: (node) => {
70
+ const name = node.name.name || ''
71
+
72
+ if (layoutRegex.test(name) && !node.attributes.some(findAsAttr)) {
73
+ const parentName = searchBubbleUp(node.parent.parent)
74
+
75
+ if (parentName) {
76
+ context.report({
77
+ node,
78
+ message: `${name}は${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります
79
+ - smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します
80
+ - ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます
81
+ - span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`,
82
+ })
83
+ }
84
+ }
85
+ },
86
+ }
87
+ },
88
+ }
89
+
90
+ module.exports.schema = []
@@ -0,0 +1,35 @@
1
+ const rule = require('../rules/a11y-required-layout-as-attribute')
2
+ const RuleTester = require('eslint').RuleTester
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 12,
7
+ ecmaFeatures: {
8
+ experimentalObjectRestSpread: true,
9
+ jsx: true,
10
+ },
11
+ sourceType: 'module',
12
+ },
13
+ })
14
+
15
+ const generateErrorText = (parentName, name) => `${name}は${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります
16
+ - smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します
17
+ - ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます
18
+ - span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`
19
+
20
+ ruleTester.run('a11y-anchor-has-href-attribute', rule, {
21
+ valid: [
22
+ { code: `<h1><Cluster as="span">ほげ</Cluster></h1>` },
23
+ { code: `<Heading><Cluster as="strong" /></Heading>` },
24
+ { code: `<StyledHeading><AnyCluster forwardedAs="span" /></StyledHeading>` },
25
+ { code: `<FormControl title={<Cluster as="span" />} />` },
26
+ { code: `<StyledFieldset title={<AnyCluster forwardedAs="strong" />} />` },
27
+ ],
28
+ invalid: [
29
+ { code: `<h1><Cluster>ほげ</Cluster></h1>`, errors: [{ message: generateErrorText('h1', 'Cluster') }] },
30
+ { code: `<Heading><Cluster /></Heading>`, errors: [{ message: generateErrorText('Heading', 'Cluster') }] },
31
+ { code: `<StyledHeading><AnyCluster /></StyledHeading>`, errors: [{ message: generateErrorText('StyledHeading', 'AnyCluster') }] },
32
+ { code: `<FormControl title={<Cluster />} />`, errors: [{ message: generateErrorText('FormControlのtitle属性', 'Cluster') }] },
33
+ { code: `<StyledFieldset title={<AnyCluster />} />`, errors: [{ message: generateErrorText('StyledFieldsetのtitle属性', 'AnyCluster') }] },
34
+ ]
35
+ })