eslint-plugin-smarthr
Advanced tools
Comparing version 0.3.6 to 0.3.7
@@ -5,2 +5,9 @@ # Changelog | ||
### [0.3.7](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.6...v0.3.7) (2023-08-24) | ||
### Features | ||
* a11y-clickable-element-has-text のチェック時、リンク内部に名称の末尾がTextがつくコンポーネントがある場合、チェックを通過するように修正 ([#69](https://github.com/kufu/eslint-plugin-smarthr/issues/69)) ([182b5d5](https://github.com/kufu/eslint-plugin-smarthr/commit/182b5d5e52c1faee26011572c48271e4c03512e1)) | ||
### [0.3.6](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.5...v0.3.6) (2023-08-20) | ||
@@ -7,0 +14,0 @@ |
{ | ||
"name": "eslint-plugin-smarthr", | ||
"version": "0.3.6", | ||
"version": "0.3.7", | ||
"author": "SmartHR", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -18,9 +18,24 @@ const { generateTagFormatter } = require('../../libs/format_styled_components') | ||
'Link$': 'Link$', | ||
'Text$': 'Text$', | ||
'Message$': 'Message$', | ||
'^a$': '(Anchor|Link)$', | ||
} | ||
const filterFalsyJSXText = (cs) => cs.filter((c) => ( | ||
!(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/)) | ||
)) | ||
const REGEX_NLSP = /^\s*\n+\s*$/ | ||
const REGEX_CLICKABLE_ELEMENT = /^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/ | ||
const REGEX_SMARTHR_LOGO = /SmartHRLogo$/ | ||
const REGEX_TEXT_COMPONENT = /(Text|Message)$/ | ||
const HIT_TYPES_RECURSICVE_SEARCH = ['JSXText', 'JSXExpressionContainer'] | ||
const HIT_TEXT_ATTRS = ['visuallyHiddenText', 'alt'] | ||
const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText) | ||
const checkFalsyJSXText = (c) => ( | ||
!(c.type === 'JSXText' && c.value.match(REGEX_NLSP)) | ||
) | ||
const message = `a, buttonなどのクリッカブルな要素内にはテキストを設定してください。 | ||
- 要素内にアイコン、画像のみを設置する場合はaltなどの代替テキスト用属性を指定してください | ||
- クリッカブルな要素内に設置しているコンポーネントがテキストを含んでいる場合、"XxxxText" のように末尾に "Text" もしくは "Message" という名称を設定してください` | ||
module.exports = { | ||
@@ -45,3 +60,3 @@ meta: { | ||
if (!node.name.name || !node.name.name.match(/^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/)) { | ||
if (!node.name.name || !node.name.name.match(REGEX_CLICKABLE_ELEMENT)) { | ||
return | ||
@@ -51,49 +66,47 @@ } | ||
const recursiveSearch = (c) => { | ||
if (['JSXText', 'JSXExpressionContainer'].includes(c.type)) { | ||
if (HIT_TYPES_RECURSICVE_SEARCH.includes(c.type)) { | ||
return true | ||
} | ||
if (c.type === 'JSXFragment') { | ||
if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) { | ||
return true | ||
switch (c.type) { | ||
case 'JSXFragment': { | ||
return c.children && filterFalsyJSXText(c.children).some(recursiveSearch) | ||
} | ||
case 'JSXElement': { | ||
// // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする | ||
if (c.openingElement.name.name.match(REGEX_SMARTHR_LOGO)) { | ||
return true | ||
} | ||
return false | ||
} | ||
const tagName = c.openingElement.name.name | ||
if (c.type === 'JSXElement') { | ||
// // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする | ||
if (c.openingElement.name.name.match(/SmartHRLogo$/)) { | ||
return true | ||
} | ||
if (tagName.match(REGEX_TEXT_COMPONENT) || componentsWithText.includes(tagName)) { | ||
return true | ||
} | ||
if (componentsWithText.includes(c.openingElement.name.name)) { | ||
return true | ||
} | ||
// HINT: role & aria-label を同時に設定されている場合は許可 | ||
let existRole = false | ||
let existAriaLabel = false | ||
const result = c.openingElement.attributes.reduce((prev, a) => { | ||
existRole = existRole || (a.name.name === 'role' && a.value.value === 'img') | ||
existAriaLabel = existAriaLabel || a.name.name === 'aria-label' | ||
// HINT: role & aria-label を同時に設定されている場合は許可 | ||
let existRole = false | ||
let existAriaLabel = false | ||
const result = c.openingElement.attributes.reduce((prev, a) => { | ||
existRole = existRole || (a.name.name === 'role' && a.value.value === 'img') | ||
existAriaLabel = existAriaLabel || a.name.name === 'aria-label' | ||
if ( | ||
prev || | ||
!HIT_TEXT_ATTRS.includes(a.name.name) | ||
) { | ||
return prev | ||
} | ||
if (prev) { | ||
return prev | ||
} | ||
return (!!a.value.value || a.value.type === 'JSXExpressionContainer') ? a : prev | ||
}, null) | ||
if (!['visuallyHiddenText', 'alt'].includes(a.name.name)) { | ||
return prev | ||
if ( | ||
result || | ||
(existRole && existAriaLabel) || | ||
(c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) | ||
) { | ||
return true | ||
} | ||
return (!!a.value.value || a.value.type === 'JSXExpressionContainer') ? a : prev | ||
}, null) | ||
if (result || (existRole && existAriaLabel)) { | ||
return true | ||
} | ||
if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) { | ||
return true | ||
} | ||
} | ||
@@ -104,8 +117,6 @@ | ||
const child = filterFalsyJSXText(parentNode.children).find(recursiveSearch) | ||
if (!child) { | ||
if (!filterFalsyJSXText(parentNode.children).find(recursiveSearch)) { | ||
context.report({ | ||
node, | ||
message: 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください', | ||
message, | ||
}); | ||
@@ -112,0 +123,0 @@ } |
@@ -40,2 +40,8 @@ # smarthr/a11y-clickable-element-has-text | ||
```jsx | ||
<XxxAnchor>> | ||
<XxxTextYyyy /> | ||
</XxxAnchor> | ||
``` | ||
## ✅ Correct | ||
@@ -70,2 +76,8 @@ | ||
```jsx | ||
<XxxAnchor>> | ||
<XxxText /> | ||
</XxxAnchor> | ||
``` | ||
```jsx | ||
/* | ||
@@ -76,3 +88,3 @@ rules: { | ||
{ | ||
componentsWithText: ['AnyComponent'], | ||
componentsWithText: ['Hoge'], | ||
}, | ||
@@ -84,4 +96,4 @@ ] | ||
<XxxButton> | ||
<AnyComponent /> | ||
<Hoge /> | ||
</XxxButton> | ||
``` |
@@ -10,2 +10,5 @@ const { generateTagFormatter } = require('../../libs/format_styled_components') | ||
const REGEX_IMG = /(img|image)$/i // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする | ||
const findAltAttr = (a) => a.name?.name === 'alt' | ||
const isWithinSvgJsxElement = (node) => { | ||
@@ -19,9 +22,12 @@ if ( | ||
if (!node.parent) { | ||
return false | ||
} | ||
return isWithinSvgJsxElement(node.parent) | ||
return node.parent ? isWithinSvgJsxElement(node.parent) : false | ||
} | ||
const MESSAGE_NOT_EXIST_ALT = `画像にはalt属性を指定してください。 | ||
- コンポーネントが画像ではない場合、img or image を末尾に持たない名称に変更してください。 | ||
- ボタンやリンクの先頭・末尾などに設置するアイコンとしての役割を持つ画像の場合、コンポーネント名の末尾を "Icon" に変更してください。 | ||
- SVG component の場合、altを属性として受け取れるようにした上で '<svg role="img" aria-label={alt}>' のように指定してください。` | ||
const MESSAGE_NULL_ALT = `画像の情報をテキストにした代替テキスト('alt')を設定してください。 | ||
- 装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。` | ||
module.exports = { | ||
@@ -36,21 +42,24 @@ meta: { | ||
JSXOpeningElement: (node) => { | ||
const matcher = (node.name.name || '').match(/(img|image)$/i) // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする | ||
if (matcher) { | ||
const alt = node.attributes.find((a) => a.name?.name === 'alt') | ||
if (node.name.name) { | ||
const matcher = node.name.name.match(REGEX_IMG) | ||
let message = '' | ||
if (matcher) { | ||
const alt = node.attributes.find(findAltAttr) | ||
if (!alt) { | ||
if (matcher.input !== 'image' || !isWithinSvgJsxElement(node.parent)) { | ||
message = '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。' | ||
let message = '' | ||
if (!alt) { | ||
if (matcher.input !== 'image' || !isWithinSvgJsxElement(node.parent)) { | ||
message = MESSAGE_NOT_EXIST_ALT | ||
} | ||
} else if (alt.value.value === '') { | ||
message = MESSAGE_NULL_ALT | ||
} | ||
} else if (alt.value.value === '') { | ||
message = '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。' | ||
} | ||
if (message) { | ||
context.report({ | ||
node, | ||
message, | ||
}); | ||
if (message) { | ||
context.report({ | ||
node, | ||
message, | ||
}); | ||
} | ||
} | ||
@@ -57,0 +66,0 @@ } |
@@ -15,3 +15,5 @@ const rule = require('../rules/a11y-clickable-element-has-text') | ||
const defaultErrorMessage = 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください' | ||
const defaultErrorMessage = `a, buttonなどのクリッカブルな要素内にはテキストを設定してください。 | ||
- 要素内にアイコン、画像のみを設置する場合はaltなどの代替テキスト用属性を指定してください | ||
- クリッカブルな要素内に設置しているコンポーネントがテキストを含んでいる場合、"XxxxText" のように末尾に "Text" もしくは "Message" という名称を設定してください` | ||
@@ -34,2 +36,4 @@ ruleTester.run('a11y-clickable-element-has-text', rule, { | ||
{ code: 'const HogeAnchor = styled(Anchor)(() => ``)' }, | ||
{ code: 'const FugaText = styled(HogeText)(() => ``)' }, | ||
{ code: 'const FugaMessage = styled(HogeMessage)(() => ``)' }, | ||
{ | ||
@@ -111,2 +115,11 @@ code: `<a>ほげ</a>`, | ||
{ | ||
code: `<a><Text /></a>`, | ||
}, | ||
{ | ||
code: `<a><HogeText /></a>`, | ||
}, | ||
{ | ||
code: `<a><FormattedMessage /></a>`, | ||
}, | ||
{ | ||
code: `<a><AnyComponent /></a>`, | ||
@@ -132,2 +145,4 @@ options: [{ | ||
{ code: 'const Piyo = styled(Anchor)(() => ``)', errors: [ { message: `Piyoを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] }, | ||
{ code: 'const Hoge = styled(Text)``', errors: [ { message: `Hogeを正規表現 "/Text$/" がmatchする名称に変更してください` } ] }, | ||
{ code: 'const Hoge = styled(HogeMessage)``', errors: [ { message: `Hogeを正規表現 "/Message$/" がmatchする名称に変更してください` } ] }, | ||
{ | ||
@@ -182,2 +197,6 @@ code: `<a><img src="hoge.jpg" /></a>`, | ||
{ | ||
code: `<a><TextWithHoge /></a>`, | ||
errors: [{ message: defaultErrorMessage }] | ||
}, | ||
{ | ||
code: `<a><AnyComponent /></a>`, | ||
@@ -184,0 +203,0 @@ options: [{ |
@@ -15,2 +15,9 @@ const rule = require('../rules/a11y-image-has-alt-attribute') | ||
const messageNotExistAlt = `画像にはalt属性を指定してください。 | ||
- コンポーネントが画像ではない場合、img or image を末尾に持たない名称に変更してください。 | ||
- ボタンやリンクの先頭・末尾などに設置するアイコンとしての役割を持つ画像の場合、コンポーネント名の末尾を "Icon" に変更してください。 | ||
- SVG component の場合、altを属性として受け取れるようにした上で '<svg role="img" aria-label={alt}>' のように指定してください。` | ||
const messageNullAlt = `画像の情報をテキストにした代替テキスト('alt')を設定してください。 | ||
- 装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。` | ||
ruleTester.run('a11y-image-has-alt-attribute', rule, { | ||
@@ -43,6 +50,6 @@ valid: [ | ||
{ code: 'const Hoge = styled(Image)``', errors: [ { message: `Hogeを正規表現 "/Image$/" がmatchする名称に変更してください` } ] }, | ||
{ code: '<img />', errors: [ { message: '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。' } ] }, | ||
{ code: '<HogeImage alt="" />', errors: [ { message: '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。' } ] }, | ||
{ code: '<hoge><image /></hoge>', errors: [ { message: '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。' } ] }, | ||
{ code: '<img />', errors: [ { message: messageNotExistAlt } ] }, | ||
{ code: '<HogeImage alt="" />', errors: [ { message: messageNullAlt } ] }, | ||
{ code: '<hoge><image /></hoge>', errors: [ { message: messageNotExistAlt } ] }, | ||
] | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
205216
3773