-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: a11y-numbered-text-within-olを追加する (#105)
* feat: a11y-numbered-text-within-olを追加する * chore: テキストノードと属性の組み合わせでも連番判定を行えるように修正
- Loading branch information
Showing
4 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# smarthr/a11y-numbered-text-within-ol | ||
|
||
- "1. hoge", "2. fuga" ... のように連番のテキストをもつコンポーネントはol要素でマークアップすることを促すルールです | ||
- ol要素でマークアップすることで連番テキストをもつ要素同士の関係、順番に意味があることを適切に示すことが出来ます | ||
|
||
## rules | ||
|
||
```js | ||
{ | ||
rules: { | ||
'smarthr/a11y-numbered-text-within-ol': 'error', // 'warn', 'off', | ||
}, | ||
} | ||
``` | ||
|
||
## ❌ Incorrect | ||
|
||
```jsx | ||
// ol要素で囲まれていないためNG | ||
<Any>1. hoge</Any> | ||
<Any>2. fuga</Any> | ||
|
||
// 属性でも同様にチェックする | ||
<Any title="1. hoge" /> | ||
<Any title="2. fuga" /> | ||
|
||
// ol要素内で連番を設定しているとNG | ||
<OrderedList> | ||
<li>1. hoge</li> | ||
<li>2. fuga</li> | ||
</OrderedList> | ||
|
||
// 同一のol要素で囲まれていないためNG | ||
<OrderedList> | ||
<li>hoge</li> | ||
</OrderedList> | ||
<OrderedList> | ||
<li>fuga</li> | ||
</OrderedList>> | ||
|
||
``` | ||
|
||
## ✅ Correct | ||
|
||
```jsx | ||
<ol> | ||
<li>hoge</li> | ||
<li>fuga</li> | ||
</ol> | ||
|
||
<OrderedList> | ||
<Any title="hoge" /> | ||
<Any title="fuga" /> | ||
</OrderedList> | ||
|
||
// デフォルトの連番からフォーマット、スタイルを変更したい場合 | ||
// counter-reset + counter-increment で表現する | ||
// 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters) | ||
<OrderedList> | ||
<li> | ||
<NumberedHeading>hoge</NumberedHeading> | ||
<Any /> | ||
</li> | ||
<li> | ||
<NumberedHeading>fuga</NumberedHeading> | ||
<Any /> | ||
</li> | ||
</OrderedList> | ||
|
||
... | ||
|
||
const OrderedList = styled.ol` | ||
list-style: none; // デフォルトのstyleを消す | ||
counter-reset: hoge; // カウンターの名称。わかりやすいものなら何でもOK | ||
` | ||
const NumberedHeading = styled(Heading)` | ||
&::before { | ||
counter-increment: hoge; // hogeカウンターを+1する | ||
content: "No " counter(hoge) ": "; // 表示される連番のフォーマット | ||
} | ||
` | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
const { generateTagFormatter } = require('../../libs/format_styled_components') | ||
|
||
const EXPECTED_NAMES = { | ||
'(Ordered(.*)List|^ol)$': '(Ordered(.*)List)$', | ||
'(S|s)elect$': '(Select)$', | ||
} | ||
const UNEXPECTED_NAMES = EXPECTED_NAMES | ||
|
||
const NUMBERED_TEXT_REGEX = /^[\s\n]*(([1-9])([^1-9]{2})[^\s\n]*)/ | ||
const ORDERED_LIST_REGEX = /(Ordered(.*)List|^ol)$/ | ||
const SELECT_REGEX = /(S|s)elect$/ | ||
|
||
const searchOrderedList = (node) => { | ||
if (node.type === 'JSXElement' && node.openingElement.name?.name) { | ||
const name = node.openingElement.name.name | ||
|
||
if (name.match(ORDERED_LIST_REGEX)) { | ||
return node.openingElement | ||
} else if (name.match(SELECT_REGEX)) { | ||
// HINT: select要素の場合、optionのラベルに連番がついている場合がありえるのでignoreする | ||
// 通常と処理を分けるためnullではなく0を返す | ||
return 0 | ||
} | ||
} | ||
|
||
if (node.type === 'Program') { | ||
return null | ||
} | ||
|
||
return searchOrderedList(node.parent) | ||
} | ||
|
||
const checkNumberedTextInOl = (result, node, context) => { | ||
if (result) { | ||
context.report({ | ||
node, | ||
message: `${result.name.name} 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。 | ||
- ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください | ||
- 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)`, | ||
}) | ||
} | ||
} | ||
|
||
const renderTag = (node) => `\`${node.name.name}="${node.value.value}"\`` | ||
const renderNode = (node, matcher) => node.type === 'JSXText' ? `\`${matcher[1]}\`` : renderTag(node) | ||
|
||
const SCHEMA = [] | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
schema: SCHEMA, | ||
}, | ||
create(context) { | ||
let firstNumber = 0 | ||
let firstNumberedNode = null | ||
let firstNumberedMatcher = null | ||
|
||
function checker(node, matcher) { | ||
if (matcher) { | ||
const result = searchOrderedList(node) | ||
|
||
if (result !== 0) { | ||
checkNumberedTextInOl(result, node, context) | ||
|
||
const nowNumber = matcher[2] * 1 | ||
|
||
if (firstNumberedNode && nowNumber !== firstNumber) { | ||
if (nowNumber === firstNumber + 1) { | ||
const resultFirst = searchOrderedList(firstNumberedNode) | ||
|
||
if (!resultFirst) { | ||
if (!result) { | ||
context.report({ | ||
node: firstNumberedNode, | ||
message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。 | ||
- ${renderNode(firstNumberedNode, firstNumberedMatcher)} と ${renderNode(node, matcher)} が同じol要素内に存在するように修正してください | ||
- 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります`, | ||
}) | ||
} else { | ||
context.report({ | ||
node: firstNumberedNode, | ||
message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。 | ||
- ${renderNode(firstNumberedNode, firstNumberedMatcher)} が ${renderNode(node, matcher)} を囲んでいるol要素内(<${result.name.name}>)に存在するように修正してください | ||
- 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<${result.name.name}>)に存在する必要があります`, | ||
}) | ||
} | ||
} else { | ||
if (!result) { | ||
context.report({ | ||
node, | ||
message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。 | ||
- ${renderNode(node, matcher)} が ${renderNode(firstNumberedNode, firstNumberedMatcher)} を囲んでいるol要素内(<${resultFirst.name.name}>)に存在するように修正してください | ||
- 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<${resultFirst.name.name}>)に存在する必要があります`, | ||
}) | ||
|
||
firstNumberedNode = null | ||
} else if (resultFirst !== result) { | ||
context.report({ | ||
node, | ||
message: `連番を含むテキストが同一のol要素でマークアップされていません。同一のol要素でマークアップすることでリスト内の要素関連性を正しく表せるためマークアップの修正を行ってください。 | ||
- ${renderNode(firstNumberedNode, firstNumberedMatcher)} と ${renderNode(node, matcher)} が同じol要素内に存在するように修正してください | ||
- 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります`, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
firstNumber = nowNumber | ||
firstNumberedNode = node | ||
firstNumberedMatcher = matcher | ||
} else if (!firstNumberedNode || nowNumber === firstNumber) { | ||
firstNumber = nowNumber | ||
firstNumberedNode = node | ||
firstNumberedMatcher = matcher | ||
} | ||
} | ||
} | ||
} | ||
|
||
return { | ||
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }), | ||
JSXAttribute: (node) => { | ||
if (node.value?.value) { | ||
checker(node, node.value.value.match(NUMBERED_TEXT_REGEX)) | ||
} | ||
}, | ||
JSXText: (node) => { | ||
checker(node, node.value.match(NUMBERED_TEXT_REGEX)) | ||
}, | ||
} | ||
}, | ||
} | ||
module.exports.schema = SCHEMA |
Oops, something went wrong.