eslint-plugin-smarthr 0.3.15 → 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
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.17](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.16...v0.3.17) (2023-12-30)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * a11y-delegate-element-has-role-presentationのインタラクティブなコンポーネントであることの判定で、小文字始まりのコンポーネントの場合判定ミスされるバグを修正 ([#94](https://github.com/kufu/eslint-plugin-smarthr/issues/94)) ([50a8296](https://github.com/kufu/eslint-plugin-smarthr/commit/50a8296b6d22c5a6475469b1ed2ea1a46234f6fd))
11
+
12
+ ### [0.3.16](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.15...v0.3.16) (2023-12-30)
13
+
14
+
15
+ ### Features
16
+
17
+ * a11y-delegate-element-has-role-presantation を追加 ([#92](https://github.com/kufu/eslint-plugin-smarthr/issues/92)) ([c211ffb](https://github.com/kufu/eslint-plugin-smarthr/commit/c211ffb7e698d010ca639d0bdda9ea82cb31033a))
18
+
5
19
  ### [0.3.15](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.14...v0.3.15) (2023-11-29)
6
20
 
7
21
 
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
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
+ - [a11y-delegate-element-has-role-presentation](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-delegate-element-has-role-presentation)
5
6
  - [a11y-heading-in-sectioning-content](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-heading-in-sectioning-content)
6
7
  - [a11y-image-has-alt-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-image-has-alt-attribute)
7
8
  - [a11y-input-has-name-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-has-name-attribute)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -0,0 +1,55 @@
1
+ # smarthr/a11y-delegate-element-has-role-presantation
2
+
3
+ - 'role="presantation"'を適切に設定することを促すルールです
4
+ - インタラクティブな要素に対して'role="presantation"'が設定されている場合、エラーになります
5
+ - インタラクティブな要素とは form, inputなどの入力要素、button, a などのクリッカブルな要素を指します
6
+ - インタラクティブな要素から発生するイベントを親要素でキャッチする場合、親要素に 'role="presantation"' を設定することを促します
7
+ - インタラクティブではない要素でイベントをキャッチしており、かつ'role="presantation"'を設定しているにも関わらず、子要素にインタラクティブな要素がない場合はエラーになります
8
+ - additionalInteractiveComponentRegexオプションに独自コンポーネントの名称を正規表現で設定することで、インタラクティブな要素として判定することが可能です
9
+
10
+ ## rules
11
+
12
+ ```js
13
+ {
14
+ rules: {
15
+ 'smarthr/a11y-delegate-element-has-role-presantation': [
16
+ 'error', // 'warn', 'off'
17
+ // { additionalInteractiveComponentRegex: ['^InteractiveComponent%'] }
18
+ ]
19
+ },
20
+ }
21
+ ```
22
+
23
+ ## ❌ Incorrect
24
+
25
+ ```jsx
26
+ // インタラクティブな要素に対して role="presantation" は設定できない
27
+ <Button role="presantation">text.</Button>
28
+ <input type="text" role="presantation" />
29
+
30
+ // インタラクティブな要素で発生するイベントを非インタラクティブな要素でキャッチする場合
31
+ // role="presantation" を設定する必要がある
32
+ <div onClick={hoge}>
33
+ <Button>text.</Button>
34
+ </div>
35
+
36
+ // 非インタラクティブな要素でイベントをキャッチする場合、
37
+ // 子要素にインタラクティブな要素がない場合はエラー
38
+ <div onClick={hoge} role="presentation">
39
+ <Text>hoge.</Text>
40
+ </div>
41
+ ```
42
+
43
+ ## ✅ Correct
44
+
45
+ ```jsx
46
+ // インタラクティブな要素で発生するイベントを非インタラクティブな要素でキャッチする場合
47
+ // role="presantation" を設定する
48
+ <div onClick={hoge} role="presentation">
49
+ <Button>text.</Button>
50
+ </div>
51
+
52
+ <div onClick={hoge} role="presentation">
53
+ <AnyForm />
54
+ </div>
55
+ ```
@@ -0,0 +1,135 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components');
2
+
3
+ const EXPECTED_NAMES = {
4
+ '(i|I)nput$': 'Input$',
5
+ '(t|T)extarea$': 'Textarea$',
6
+ '(s|S)elect$': 'Select$',
7
+ 'InputFile$': 'InputFile$',
8
+ 'RadioButtonPanel$': 'RadioButtonPanel$',
9
+ 'Check(b|B)ox$': 'CheckBox$',
10
+ 'Combo(b|B)ox$': 'ComboBox$',
11
+ 'DatePicker$': 'DatePicker$',
12
+ 'DropZone$': 'DropZone$',
13
+ 'FieldSet$': 'FieldSet$',
14
+ '(b|B)utton$': 'Button$',
15
+ 'Anchor$': 'Anchor$',
16
+ 'Link$': 'Link$',
17
+ 'TabItem$': 'TabItem$',
18
+ '^a$': '(Anchor|Link)$',
19
+
20
+ '(f|F)orm$': 'Form$',
21
+ 'ActionDialogWithTrigger$': 'ActionDialogWithTrigger$',
22
+ 'RemoteDialogTrigger$': 'RemoteDialogTrigger$',
23
+ 'RemoteTrigger(.+)Dialog$': 'RemoteTrigger(.+)Dialog$',
24
+ 'Pagination$': 'Pagination$',
25
+ 'SideNav$': 'SideNav$',
26
+ 'AccordionPanel$': 'AccordionPanel$',
27
+ }
28
+
29
+ const UNEXPECTED_NAMES = {
30
+ '(B|^b)utton$': '(Button)$',
31
+ '(Anchor|^a)$': '(Anchor)$',
32
+ '(Link|^a)$': '(Link)$',
33
+ }
34
+
35
+ const INTERACTIVE_COMPONENT_NAMES = Object.keys(EXPECTED_NAMES)
36
+ const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/
37
+
38
+ const messageNonInteractiveEventHandler = (nodeName, interactiveComponentRegex, onAttrs) => {
39
+ const onAttrsText = onAttrs.join(', ')
40
+
41
+ return `${nodeName} に${onAttrsText}を設定するとブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
42
+ - 方法1: ${nodeName}がinput、buttonやaなどのインタラクティブな要素の場合、コンポーネント名の末尾をインタラクティブなコンポーネントであることがわかる名称に変更してください
43
+ - "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに差し替える、もしくは名称を変更してください
44
+ - 方法2: インタラクティブな親要素、もしくは子要素が存在する場合、直接${onAttrsText}を設定することを検討してください
45
+ - 方法3: インタラクティブな親要素、もしくは子要素が存在しない場合、インタラクティブな要素を必ず持つようにマークアップを修正後、${onAttrsText}の設定要素を検討してください
46
+ - 方法4: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください`
47
+ }
48
+ const messageRolePresentationNotHasInteractive = (nodeName, interactiveComponentRegex, onAttrs) => `${nodeName}に 'role="presentation"' が設定されているにも関わらず、子要素にinput、buttonやaなどのインタラクティブな要素が見つからないため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
49
+ - 方法1: 子要素にインタラクティブな要素が存在するにも関わらずこのエラーが表示されている場合、子要素の名称を変更してください
50
+ - "${interactiveComponentRegex}" の正規表現にmatchするよう、インタラクティブな子要素全てを差し替える、もしくは名称を変更してください
51
+ - 方法2: ${nodeName}自体がインタラクティブな要素の場合、'role="presentation"'を削除した上で名称を変更してください
52
+ - "${interactiveComponentRegex}" の正規表現にmatchするよう、${nodeName}の名称を変更してください
53
+ - 方法3: 子要素にインタラクティブな要素が存在し、${onAttrs.join(', ')}全属性をそれらの要素に移動させられる場合、'role="presentation"'を消した上で実施してください`
54
+ const messageInteractiveHasRolePresentation = (nodeName, interactiveComponentRegex) => `${nodeName}はinput、buttonやaなどのインタラクティブな要素にもかかわらず 'role="presentation"' が設定されているため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
55
+ - 方法1: 'role="presentation"' を削除してください
56
+ - 方法2: ${nodeName}の名称を "${interactiveComponentRegex}" とマッチしない名称に変更してください`
57
+
58
+ const SCHEMA = [
59
+ {
60
+ type: 'object',
61
+ properties: {
62
+ additionalInteractiveComponentRegex: { type: 'array', items: { type: 'string' } },
63
+ },
64
+ additionalProperties: false,
65
+ }
66
+ ]
67
+
68
+ module.exports = {
69
+ meta: {
70
+ type: 'problem',
71
+ schema: SCHEMA,
72
+ },
73
+ create(context) {
74
+ const options = context.options[0]
75
+ const interactiveComponentRegex = new RegExp(`(${INTERACTIVE_COMPONENT_NAMES.join('|')}${options?.additionalInteractiveComponentRegex ? `|${options.additionalInteractiveComponentRegex.join('|')}` : ''})`)
76
+
77
+ return {
78
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
79
+ JSXOpeningElement: (node) => {
80
+ const nodeName = node.name.name || '';
81
+
82
+ let onAttrs = []
83
+ let isRolePresentation = false
84
+
85
+ node.attributes.forEach((a) => {
86
+ const aName = a.name?.name || ''
87
+
88
+ if (aName.match(INTERACTIVE_ON_REGEX)) {
89
+ onAttrs.push(aName)
90
+ } else if (aName === 'role' && a.value?.value === 'presentation') {
91
+ isRolePresentation = true
92
+ }
93
+ })
94
+
95
+ if (!nodeName.match(interactiveComponentRegex)) {
96
+ if (onAttrs.length > 0) {
97
+ if (!isRolePresentation) {
98
+ context.report({
99
+ node,
100
+ message: messageNonInteractiveEventHandler(nodeName, interactiveComponentRegex, onAttrs),
101
+ });
102
+ } else {
103
+ const isHasInteractive = (c) => {
104
+ if (c.type === 'JSXElement') {
105
+ if ((c.openingElement.name.name || '').match(interactiveComponentRegex)) {
106
+ return true
107
+ }
108
+
109
+ if (c.children.length > 0) {
110
+ return c.children.find(isHasInteractive)
111
+ }
112
+ }
113
+
114
+ return false
115
+ }
116
+
117
+ if (!node.parent.children.find(isHasInteractive)) {
118
+ context.report({
119
+ node,
120
+ message: messageRolePresentationNotHasInteractive(nodeName, interactiveComponentRegex, onAttrs)
121
+ })
122
+ }
123
+ }
124
+ }
125
+ } else if (isRolePresentation) {
126
+ context.report({
127
+ node,
128
+ message: messageInteractiveHasRolePresentation(nodeName, interactiveComponentRegex)
129
+ })
130
+ }
131
+ },
132
+ };
133
+ },
134
+ };
135
+ module.exports.schema = SCHEMA;
@@ -1,7 +1,7 @@
1
1
  # smarthr/a11y-input-has-name-attribute
2
2
 
3
- - input, textarea, select name 属性を設定することを強制するルールです。
4
- - input name を設定することでブラウザの補完機能が有効になる可能性が高まります。
3
+ - input, textarea, select など入力要素に name 属性を設定することを強制するルールです。
4
+ - 入力要素は name を設定することでブラウザの補完機能が有効になる可能性が高まります。
5
5
  - 補完機能はブラウザによって異なるため、補完される可能性が上がるよう、name には半角英数の小文字・大文字と一部記号(`_ , [, ]`)のみ利用可能です。
6
6
  - input[type="radio"] は name を適切に設定することでラジオグループが確立され、キーボード操作しやすくなる等のメリットがあります。
7
7
  - checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
@@ -6,6 +6,7 @@ const EXPECTED_NAMES = {
6
6
  '(s|S)elect$': 'Select$',
7
7
  'InputFile$': 'InputFile$',
8
8
  'RadioButton$': 'RadioButton$',
9
+ 'RadioButtonPanel$': 'RadioButtonPanel$',
9
10
  'Check(b|B)ox$': 'CheckBox$',
10
11
  'Combo(b|B)ox$': 'ComboBox$',
11
12
  'DatePicker$': 'DatePicker$',
@@ -0,0 +1,59 @@
1
+ const rule = require('../rules/a11y-delegate-element-has-role-presentation');
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 defaultInteractiveRegex = '/((i|I)nput$|(t|T)extarea$|(s|S)elect$|InputFile$|RadioButtonPanel$|Check(b|B)ox$|Combo(b|B)ox$|DatePicker$|DropZone$|FieldSet$|(b|B)utton$|Anchor$|Link$|TabItem$|^a$|(f|F)orm$|ActionDialogWithTrigger$|RemoteDialogTrigger$|RemoteTrigger(.+)Dialog$|Pagination$|SideNav$|AccordionPanel$)/'
16
+ const messageNonInteractiveEventHandler = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => {
17
+ const onAttrsText = onAttrs.join(', ')
18
+
19
+ return `${nodeName} に${onAttrsText}を設定するとブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
20
+ - 方法1: ${nodeName}がinput、buttonやaなどのインタラクティブな要素の場合、コンポーネント名の末尾をインタラクティブなコンポーネントであることがわかる名称に変更してください
21
+ - "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに差し替える、もしくは名称を変更してください
22
+ - 方法2: インタラクティブな親要素、もしくは子要素が存在する場合、直接${onAttrsText}を設定することを検討してください
23
+ - 方法3: インタラクティブな親要素、もしくは子要素が存在しない場合、インタラクティブな要素を必ず持つようにマークアップを修正後、${onAttrsText}の設定要素を検討してください
24
+ - 方法4: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください`
25
+ }
26
+ const messageRolePresentationNotHasInteractive = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => `${nodeName}に 'role="presentation"' が設定されているにも関わらず、子要素にinput、buttonやaなどのインタラクティブな要素が見つからないため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
27
+ - 方法1: 子要素にインタラクティブな要素が存在するにも関わらずこのエラーが表示されている場合、子要素の名称を変更してください
28
+ - "${interactiveComponentRegex}" の正規表現にmatchするよう、インタラクティブな子要素全てを差し替える、もしくは名称を変更してください
29
+ - 方法2: ${nodeName}自体がインタラクティブな要素の場合、'role="presentation"'を削除した上で名称を変更してください
30
+ - "${interactiveComponentRegex}" の正規表現にmatchするよう、${nodeName}の名称を変更してください
31
+ - 方法3: 子要素にインタラクティブな要素が存在し、${onAttrs.join(', ')}全属性をそれらの要素に移動させられる場合、'role="presentation"'を消した上で実施してください`
32
+ const messageInteractiveHasRolePresentation = (nodeName, interactiveComponentRegex = defaultInteractiveRegex) => `${nodeName}はinput、buttonやaなどのインタラクティブな要素にもかかわらず 'role="presentation"' が設定されているため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
33
+ - 方法1: 'role="presentation"' を削除してください
34
+ - 方法2: ${nodeName}の名称を "${interactiveComponentRegex}" とマッチしない名称に変更してください`
35
+
36
+ ruleTester.run('smarthr/a11y-delegate-element-has-role-presentation', rule, {
37
+ valid: [
38
+ { code: '<Input />' },
39
+ { code: '<HogeForm>any</HogeForm>' },
40
+ { code: '<FugaButton>any</FugaButton>' },
41
+ { code: '<Link />' },
42
+ { code: '<div onClick={any} role="presentation"><Link /></div>' },
43
+ { code: '<div onClick={any} role="presentation"><button /></div>' },
44
+ { code: '<Wrapper onClick={any} role="presentation"><Link /></Wrapper>' },
45
+ { code: '<Wrapper onClick={any} role="presentation"><Hoge /></Wrapper>', options: [{ additionalInteractiveComponentRegex: ['^Hoge$'] }] },
46
+ { code: '<Wrapper onClick={any} role="presentation"><any><Link /></any></Wrapper>' },
47
+ ],
48
+ invalid: [
49
+ { code: '<Input role="presentation" />', errors: [ { message: messageInteractiveHasRolePresentation('Input') } ] },
50
+ { code: '<HogeForm role="presentation">any</HogeForm>', errors: [ { message: messageInteractiveHasRolePresentation('HogeForm') } ] },
51
+ { code: '<FugaButton role="presentation">any</FugaButton>', errors: [ { message: messageInteractiveHasRolePresentation('FugaButton') } ] },
52
+ { code: '<Link role="presentation" />', errors: [ { message: messageInteractiveHasRolePresentation('Link') } ] },
53
+ { code: '<div onClick={any} onSubmit={any2} role="presentation"><Hoge /></div>', errors: [ { message: messageRolePresentationNotHasInteractive('div', ['onClick', 'onSubmit']) } ] },
54
+ { code: '<div onClick={any}><Link /></div>', errors: [ { message: messageNonInteractiveEventHandler('div', ['onClick']) } ] },
55
+ { code: '<Wrapper onClick={any}><Link /></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick']) } ] },
56
+ { 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$|DatePicker$|DropZone$|FieldSet$|(b|B)utton$|Anchor$|Link$|TabItem$|^a$|(f|F)orm$|ActionDialogWithTrigger$|RemoteDialogTrigger$|RemoteTrigger(.+)Dialog$|Pagination$|SideNav$|AccordionPanel$|^Hoge$)/') } ] },
57
+ { code: '<Wrapper onClick={any} onChange={anyany}><any><Link /></any></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick', 'onChange']) } ] },
58
+ ],
59
+ });