eslint-plugin-smarthr 0.3.18 → 0.3.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,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.3.19](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.18...v0.3.19) (2024-01-01)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * a11y-delegate-element-has-role-presentation のインタラクティブな要素の判定を調整する ([#96](https://github.com/kufu/eslint-plugin-smarthr/issues/96)) ([63c9bda](https://github.com/kufu/eslint-plugin-smarthr/commit/63c9bda3b5d52baa3c1c2fd183ebee06d3a429d2))
11
+
5
12
  ### [0.3.18](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.17...v0.3.18) (2023-12-30)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -38,6 +38,9 @@ const UNEXPECTED_NAMES = {
38
38
 
39
39
  const INTERACTIVE_COMPONENT_NAMES = Object.keys(EXPECTED_NAMES)
40
40
  const INTERACTIVE_ON_REGEX = /^on(Change|Input|Focus|Blur|(Double)?Click|Key(Down|Up|Press)|Mouse(Enter|Over|Down|Up|Leave)|Select|Submit)$/
41
+ const MEANED_ROLE_REGEX = /^(combobox|group|slider|toolbar)$/
42
+
43
+ const INTERACTIVE_NODE_TYPE = ['JSXElement', 'JSXExpressionContainer', 'ConditionalExpression']
41
44
 
42
45
  const messageNonInteractiveEventHandler = (nodeName, interactiveComponentRegex, onAttrs) => {
43
46
  const onAttrsText = onAttrs.join(', ')
@@ -47,7 +50,9 @@ const messageNonInteractiveEventHandler = (nodeName, interactiveComponentRegex,
47
50
  - "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに差し替える、もしくは名称を変更してください
48
51
  - 方法2: インタラクティブな親要素、もしくは子要素が存在する場合、直接${onAttrsText}を設定することを検討してください
49
52
  - 方法3: インタラクティブな親要素、もしくは子要素が存在しない場合、インタラクティブな要素を必ず持つようにマークアップを修正後、${onAttrsText}の設定要素を検討してください
50
- - 方法4: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください`
53
+ - 方法4: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください
54
+ - 'role="presentation"' を設定した要素はマークアップとしての意味がなくなるため、div・span などマークアップとしての意味を持たない要素に設定してください
55
+ - 'role="presentation"' を設定する適切な要素が存在しない場合、div、またはspanでイベントが発生する要素を囲んだ上でrole属性を設定してください`
51
56
  }
52
57
  const messageRolePresentationNotHasInteractive = (nodeName, interactiveComponentRegex, onAttrs) => `${nodeName}に 'role="presentation"' が設定されているにも関わらず、子要素にinput、buttonやaなどのインタラクティブな要素が見つからないため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
53
58
  - 方法1: 子要素にインタラクティブな要素が存在するにも関わらずこのエラーが表示されている場合、子要素の名称を変更してください
@@ -77,6 +82,31 @@ module.exports = {
77
82
  create(context) {
78
83
  const options = context.options[0]
79
84
  const interactiveComponentRegex = new RegExp(`(${INTERACTIVE_COMPONENT_NAMES.join('|')}${options?.additionalInteractiveComponentRegex ? `|${options.additionalInteractiveComponentRegex.join('|')}` : ''})`)
85
+ const isHasInteractive = (c) => {
86
+ switch (c.type) {
87
+ case 'JSXElement': {
88
+ if ((c.openingElement.name.name || '').match(interactiveComponentRegex)) {
89
+ return true
90
+ }
91
+
92
+ if (c.children.length > 0) {
93
+ return !!c.children.find(isHasInteractive)
94
+ }
95
+ }
96
+ case 'JSXExpressionContainer':
97
+ case 'ConditionalExpression': {
98
+ let expression = c
99
+
100
+ if (c.expression) {
101
+ expression = c.expression
102
+ }
103
+
104
+ return !![expression.right, expression.consequent, expression.alternate].find((ec) => INTERACTIVE_NODE_TYPE.includes(ec?.type) && isHasInteractive(ec))
105
+ }
106
+ }
107
+
108
+ return false
109
+ }
80
110
 
81
111
  return {
82
112
  ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
@@ -84,6 +114,7 @@ module.exports = {
84
114
  const nodeName = node.name.name || '';
85
115
 
86
116
  let onAttrs = []
117
+ let isMeanedRole = false
87
118
  let isRolePresentation = false
88
119
 
89
120
  node.attributes.forEach((a) => {
@@ -91,39 +122,34 @@ module.exports = {
91
122
 
92
123
  if (aName.match(INTERACTIVE_ON_REGEX)) {
93
124
  onAttrs.push(aName)
94
- } else if (aName === 'role' && a.value?.value === 'presentation') {
95
- isRolePresentation = true
125
+ } else if (aName === 'role') {
126
+ const v = a.value?.value || ''
127
+
128
+ if (v === 'presentation') {
129
+ isMeanedRole = isRolePresentation = true
130
+ } else if (v.match(MEANED_ROLE_REGEX)) {
131
+ isMeanedRole = true
132
+ }
96
133
  }
97
134
  })
98
135
 
99
136
  if (!nodeName.match(interactiveComponentRegex)) {
100
137
  if (onAttrs.length > 0) {
101
138
  if (!isRolePresentation) {
102
- context.report({
103
- node,
104
- message: messageNonInteractiveEventHandler(nodeName, interactiveComponentRegex, onAttrs),
105
- });
106
- } else {
107
- const isHasInteractive = (c) => {
108
- if (c.type === 'JSXElement') {
109
- if ((c.openingElement.name.name || '').match(interactiveComponentRegex)) {
110
- return true
111
- }
112
-
113
- if (c.children.length > 0) {
114
- return c.children.find(isHasInteractive)
115
- }
116
- }
117
-
118
- return false
119
- }
120
-
121
- if (!node.parent.children.find(isHasInteractive)) {
139
+ // HINT: role="presentation"以外で意味があるroleが設定されている場合はエラーにしない
140
+ // 基本的にsmarthr-uiでroleの設定などは巻き取る && そもそもroleを設定するよりタグを適切にマークアップすることが優先されるため
141
+ // エラーなどには表示しない
142
+ if (!isMeanedRole) {
122
143
  context.report({
123
144
  node,
124
- message: messageRolePresentationNotHasInteractive(nodeName, interactiveComponentRegex, onAttrs)
125
- })
145
+ message: messageNonInteractiveEventHandler(nodeName, interactiveComponentRegex, onAttrs),
146
+ });
126
147
  }
148
+ } else if (!node.parent.children.find(isHasInteractive)) {
149
+ context.report({
150
+ node,
151
+ message: messageRolePresentationNotHasInteractive(nodeName, interactiveComponentRegex, onAttrs)
152
+ })
127
153
  }
128
154
  }
129
155
  } else if (isRolePresentation) {
@@ -12,7 +12,7 @@ const ruleTester = new RuleTester({
12
12
  },
13
13
  });
14
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$)/'
15
+ const defaultInteractiveRegex = '/((i|I)nput$|(t|T)extarea$|(s|S)elect$|InputFile$|RadioButtonPanel$|Check(b|B)ox$|Combo(b|B)ox$|DatePicker$|DropZone$|Switch$|SegmentedControl$|RightFixedNote$|FieldSet$|(b|B)utton$|Anchor$|Link$|TabItem$|^a$|(f|F)orm$|ActionDialogWithTrigger$|RemoteDialogTrigger$|RemoteTrigger(.+)Dialog$|FormDialog$|Pagination$|SideNav$|AccordionPanel$)/'
16
16
  const messageNonInteractiveEventHandler = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => {
17
17
  const onAttrsText = onAttrs.join(', ')
18
18
 
@@ -21,7 +21,9 @@ const messageNonInteractiveEventHandler = (nodeName, onAttrs, interactiveCompone
21
21
  - "${interactiveComponentRegex}" の正規表現にmatchするコンポーネントに差し替える、もしくは名称を変更してください
22
22
  - 方法2: インタラクティブな親要素、もしくは子要素が存在する場合、直接${onAttrsText}を設定することを検討してください
23
23
  - 方法3: インタラクティブな親要素、もしくは子要素が存在しない場合、インタラクティブな要素を必ず持つようにマークアップを修正後、${onAttrsText}の設定要素を検討してください
24
- - 方法4: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください`
24
+ - 方法4: インタラクティブな子要素から発生したイベントをキャッチすることが目的で${onAttrsText}を設定している場合、'role="presentation"' を設定してください
25
+ - 'role="presentation"' を設定した要素はマークアップとしての意味がなくなるため、div・span などマークアップとしての意味を持たない要素に設定してください
26
+ - 'role="presentation"' を設定する適切な要素が存在しない場合、div、またはspanでイベントが発生する要素を囲んだ上でrole属性を設定してください`
25
27
  }
26
28
  const messageRolePresentationNotHasInteractive = (nodeName, onAttrs, interactiveComponentRegex = defaultInteractiveRegex) => `${nodeName}に 'role="presentation"' が設定されているにも関わらず、子要素にinput、buttonやaなどのインタラクティブな要素が見つからないため、ブラウザが正しく解釈が行えず、ユーザーが利用することが出来ない場合があるため、以下のいずれかの対応をおこなってください。
27
29
  - 方法1: 子要素にインタラクティブな要素が存在するにも関わらずこのエラーが表示されている場合、子要素の名称を変更してください
@@ -44,6 +46,12 @@ ruleTester.run('smarthr/a11y-delegate-element-has-role-presentation', rule, {
44
46
  { code: '<Wrapper onClick={any} role="presentation"><Link /></Wrapper>' },
45
47
  { code: '<Wrapper onClick={any} role="presentation"><Hoge /></Wrapper>', options: [{ additionalInteractiveComponentRegex: ['^Hoge$'] }] },
46
48
  { code: '<Wrapper onClick={any} role="presentation"><any><Link /></any></Wrapper>' },
49
+ { code: '<Wrapper onClick={any} role="presentation">{any && <AnyButton />}</Wrapper>' },
50
+ { code: '<Wrapper onClick={any} role="presentation">{any || <AnyButton />}</Wrapper>' },
51
+ { code: '<Wrapper onClick={any} role="presentation">{any1 && (any2 || any3) && <AnyButton />}</Wrapper>' },
52
+ { code: '<Wrapper onClick={any} role="presentation">{any ? <AnyButton /> : null}</Wrapper>' },
53
+ { code: '<Wrapper onClick={any} role="presentation">{any1 ? (any2 ? <HogeLink /> : null) : null}</Wrapper>' },
54
+ { code: '<Wrapper onClick={any} role="presentation">{any ? null : (hoge ? <AnyLink /> : null)}</Wrapper>' },
47
55
  ],
48
56
  invalid: [
49
57
  { code: '<Input role="presentation" />', errors: [ { message: messageInteractiveHasRolePresentation('Input') } ] },
@@ -53,7 +61,8 @@ ruleTester.run('smarthr/a11y-delegate-element-has-role-presentation', rule, {
53
61
  { code: '<div onClick={any} onSubmit={any2} role="presentation"><Hoge /></div>', errors: [ { message: messageRolePresentationNotHasInteractive('div', ['onClick', 'onSubmit']) } ] },
54
62
  { code: '<div onClick={any}><Link /></div>', errors: [ { message: messageNonInteractiveEventHandler('div', ['onClick']) } ] },
55
63
  { 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$)/') } ] },
64
+ { 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$|Switch$|SegmentedControl$|RightFixedNote$|FieldSet$|(b|B)utton$|Anchor$|Link$|TabItem$|^a$|(f|F)orm$|ActionDialogWithTrigger$|RemoteDialogTrigger$|RemoteTrigger(.+)Dialog$|FormDialog$|Pagination$|SideNav$|AccordionPanel$|^Hoge$)/') } ] },
57
65
  { code: '<Wrapper onClick={any} onChange={anyany}><any><Link /></any></Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick', 'onChange']) } ] },
66
+ { code: '<Wrapper onClick={any}>{any ? null : (hoge ? <AnyLink /> : null)}</Wrapper>', errors: [ { message: messageNonInteractiveEventHandler('Wrapper', ['onClick']) } ] },
58
67
  ],
59
68
  });