eslint-plugin-smarthr 0.3.3 → 0.3.4

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.4](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.3...v0.3.4) (2023-07-18)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-heading-in-sectioning-content を smarthr-ui/PageHeading に対応させる ([#66](https://github.com/kufu/eslint-plugin-smarthr/issues/66)) ([5abc13c](https://github.com/kufu/eslint-plugin-smarthr/commit/5abc13c73f57242fbe8b6016bc6e598fd0403f1c))
11
+
5
12
  ### [0.3.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.2...v0.3.3) (2023-07-10)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -39,14 +39,19 @@
39
39
  </section>
40
40
  ```
41
41
 
42
- ## ✅ Correct
43
-
44
42
  ```jsx
45
- <div>
46
- <Heading>hoge</Heading> // コンポーネント内にHeadingが一つのみの場合はOK
47
- </div>
43
+ <>
44
+ <PageHeading> // 同じファイル内に複数のPageHeadingが存在するとNG
45
+ hoge
46
+ </PageHeading>
47
+ <PageHeading>
48
+ fuga
49
+ </PageHeading>
50
+ </>
48
51
  ```
49
52
 
53
+ ## ✅ Correct
54
+
50
55
  ```jsx
51
56
  <Section>
52
57
  <Heading>hoge</Heading>
@@ -57,14 +62,17 @@
57
62
  ```
58
63
 
59
64
  ```jsx
60
- <Section>
61
- <Heading>
62
- hoge
63
- </Heading>
64
- </Section>
65
- <StyledSection>
66
- <Heading>
67
- fuga
68
- </Heading>
69
- </StyledSection>
65
+ <>
66
+ <PageHeading>Page Name.</PageHeading>
67
+ <Section>
68
+ <Heading>
69
+ hoge
70
+ </Heading>
71
+ </Section>
72
+ <StyledSection>
73
+ <Heading>
74
+ fuga
75
+ </Heading>
76
+ </StyledSection>
77
+ </>
70
78
  ```
@@ -1,23 +1,31 @@
1
1
  const { generateTagFormatter } = require('../../libs/format_styled_components')
2
2
 
3
3
  const EXPECTED_NAMES = {
4
+ 'PageHeading$': 'PageHeading$',
4
5
  'Heading$': 'Heading$',
5
- '^h(1|2|3|4|5|6)$': 'Heading$',
6
+ '^h1$': 'PageHeading$',
7
+ '^h(|2|3|4|5|6)$': 'Heading$',
6
8
  'Article$': 'Article$',
7
9
  'Aside$': 'Aside$',
8
10
  'Nav$': 'Nav$',
9
11
  'Section$': 'Section$',
12
+ 'ModelessDialog$': 'ModelessDialog$',
10
13
  }
11
14
 
12
15
  const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
13
16
  const declaratorHeadingRegex = /Heading$/
14
17
  const sectioningRegex = /((A(rticle|side))|Nav|Section|^SectioningFragment)$/
15
18
  const bareTagRegex = /^(article|aside|nav|section)$/
16
- const messagePrefix = 'Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。'
17
- const messageSuffix = 'Sectioning Content(Article, Aside, Nav, Section)でHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。現在のマークアップの構造を変更したくない場合はSectioningFragmentコンポーネントを利用してください。'
19
+ const modelessDialogRegex = /ModelessDialog$/
18
20
 
19
- const commonMessage = `${messagePrefix}${messageSuffix}`
20
- const rootMessage = `${messagePrefix}コンポーネント全体に対するHeadingではない場合、${messageSuffix}コンポーネント全体に対するHeadingの場合、他のHeadingのアウトラインが明確に指定されればエラーにならなくなります。`
21
+ const noHeadingTagNames = ['span', 'legend']
22
+
23
+ const headingMessage = `smarthr-ui/Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。
24
+ - smarthr-uiのArticle, Aside, Nav, SectionのいずれかでHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。`
25
+ const rootHeadingMessage = `${headingMessage}
26
+ - Headingをh1にしたい場合(機能名、ページ名などこのページ内でもっとも重要な見出しの場合)、smarthr-ui/PageHeadingを利用してください。その場合はSectionなどでアウトラインを示す必要はありません。`
27
+ const pageHeadingMessage = 'smarthr-ui/PageHeading が同一ファイル内に複数存在しています。PageHeadingはh1タグを出力するため最も重要な見出しにのみ利用してください。'
28
+ const pageHeadingInSectionMessage = 'smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。'
21
29
 
22
30
  const reportMessageBareToSHR = (tagName, visibleExample) => {
23
31
  const matcher = tagName.match(bareTagRegex)
@@ -38,8 +46,12 @@ const searchBubbleUp = (node) => {
38
46
  return node
39
47
  }
40
48
 
41
- // Headingコンポーネントの拡張なので対象外
42
- if (node.type === 'VariableDeclarator' && node.id.name.match(declaratorHeadingRegex)) {
49
+ if (
50
+ // Headingコンポーネントの拡張なので対象外
51
+ node.type === 'VariableDeclarator' && node.parent.parent?.type === 'Program' && node.id.name.match(declaratorHeadingRegex) ||
52
+ // ModelessDialogのheaderにHeadingを設定している場合も対象外
53
+ node.type === 'JSXAttribute' && node.name.name === 'header' && node.parent.name.name.match(modelessDialogRegex)
54
+ ) {
43
55
  return null
44
56
  }
45
57
 
@@ -52,6 +64,7 @@ module.exports = {
52
64
  schema: [],
53
65
  },
54
66
  create(context) {
67
+ let h1s = []
55
68
  let sections = []
56
69
  let { VariableDeclarator, ...formatter } = generateTagFormatter({ context, EXPECTED_NAMES })
57
70
 
@@ -86,31 +99,42 @@ module.exports = {
86
99
  node,
87
100
  message,
88
101
  })
89
- } else if (elementName.match(headingRegex)) {
102
+ // Headingに明示的にtag属性が設定されており、それらが span or legend の場合はHeading扱いしない
103
+ } else if (
104
+ elementName.match(headingRegex) &&
105
+ !noHeadingTagNames.includes(node.attributes.find((a) => a.name?.name == 'tag')?.value.value)
106
+ ) {
90
107
  const result = searchBubbleUp(node.parent)
91
108
 
92
109
  if (result) {
93
- const saved = sections.find((s) => s[0] === result)
94
-
95
- // HINT: 最初の1つ目は通知しない()
96
- if (!saved) {
97
- sections.push([result, node])
98
- } else {
99
- // HINT: 同じファイルで同じSectioningContent or トップノードを持つ場合
100
- const [section, unreport] = saved
101
- const targets = unreport ? [unreport, node] : [node]
102
-
103
- saved[1] = undefined
110
+ if (elementName.match(/PageHeading$/)) {
111
+ h1s.push(node)
104
112
 
105
- targets.forEach((n) => {
113
+ if (h1s.length > 1) {
106
114
  context.report({
107
- node: n,
108
- message:
109
- section.type === 'Program'
110
- ? rootMessage
111
- : commonMessage,
115
+ node,
116
+ message: pageHeadingMessage,
112
117
  })
118
+ } else if (result.type !== 'Program') {
119
+ context.report({
120
+ node,
121
+ message: pageHeadingInSectionMessage,
122
+ })
123
+ }
124
+ } else if (result.type === 'Program') {
125
+ context.report({
126
+ node,
127
+ message: rootHeadingMessage,
113
128
  })
129
+ } else {
130
+ if (sections.find((s) => s === result)) {
131
+ context.report({
132
+ node,
133
+ message: headingMessage,
134
+ })
135
+ } else {
136
+ sections.push(result)
137
+ }
114
138
  }
115
139
  }
116
140
  }
@@ -12,13 +12,17 @@ const ruleTester = new RuleTester({
12
12
  },
13
13
  });
14
14
 
15
- const rootMessage = 'Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。コンポーネント全体に対するHeadingではない場合、Sectioning Content(Article, Aside, Nav, Section)でHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。現在のマークアップの構造を変更したくない場合はSectioningFragmentコンポーネントを利用してください。コンポーネント全体に対するHeadingの場合、他のHeadingのアウトラインが明確に指定されればエラーにならなくなります。'
16
- const message = 'Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。Sectioning Content(Article, Aside, Nav, Section)でHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。現在のマークアップの構造を変更したくない場合はSectioningFragmentコンポーネントを利用してください。'
15
+ const lowerMessage = `smarthr-ui/Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。
16
+ - smarthr-uiのArticle, Aside, Nav, SectionのいずれかでHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。`
17
+ const message = `${lowerMessage}
18
+ - Headingをh1にしたい場合(機能名、ページ名などこのページ内でもっとも重要な見出しの場合)、smarthr-ui/PageHeadingを利用してください。その場合はSectionなどでアウトラインを示す必要はありません。`
19
+ const pageMessage = 'smarthr-ui/PageHeading が同一ファイル内に複数存在しています。PageHeadingはh1タグを出力するため最も重要な見出しにのみ利用してください。'
20
+ const pageInSectionMessage = 'smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。'
17
21
 
18
22
  ruleTester.run('a11y-heading-in-sectioning-content', rule, {
19
23
  valid: [
20
24
  { code: `import styled from 'styled-components'` },
21
- { code: 'const HogeHeading = styled.h1``' },
25
+ { code: 'const HogePageHeading = styled.h1``' },
22
26
  { code: 'const HogeHeading = styled.h2``' },
23
27
  { code: 'const HogeHeading = styled.h3``' },
24
28
  { code: 'const HogeHeading = styled.h4``' },
@@ -30,18 +34,14 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
30
34
  { code: 'const FugaAside = styled(HogeAside)``' },
31
35
  { code: 'const FugaNav = styled(HogeNav)``' },
32
36
  { code: 'const FugaSection = styled(HogeSection)``' },
33
- { code: '<Heading>hoge</Heading>' },
34
- { code: '<HogeHeading>hoge</HogeHeading>' },
35
- { code: '<><Heading>hoge</Heading><Section><Heading>hoge</Heading></Section></>' },
36
- { code: '<><Heading>hoge</Heading><Aside><Heading>hoge</Heading></Aside></>' },
37
- { code: '<><Heading>hoge</Heading><Article><Heading>hoge</Heading></Article></>' },
38
- { code: '<><Heading>hoge</Heading><Nav><Heading>hoge</Heading></Nav></>' },
39
- { code: '<><Heading>hoge</Heading><SectioningFragment><Heading>hoge</Heading></SectioningFragment></>' },
40
- { code: 'const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>;const FugaHeading = () => <AbcHeading anyArg={abc}>hoge</AbcHeading>' },
37
+ { code: '<PageHeading>hoge</PageHeading>' },
38
+ { code: '<Section><Heading>hoge</Heading></Section>' },
39
+ { code: '<><Section><Heading>hoge</Heading></Section><Section><Heading>fuga</Heading></Section></>' },
40
+ { code: 'const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
41
41
  ],
42
42
  invalid: [
43
43
  { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
44
- { code: 'const Hoge = styled.h1``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
44
+ { code: 'const Hoge = styled.h1``', errors: [ { message: `Hogeを正規表現 "/PageHeading$/" がmatchする名称に変更してください` } ] },
45
45
  { code: 'const Hoge = styled.h2``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
46
46
  { code: 'const Hoge = styled.h3``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
47
47
  { code: 'const Hoge = styled.h4``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
@@ -57,12 +57,11 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
57
57
  { code: 'const StyledAside = styled.aside``', errors: [ { message: `"aside"を利用せず、smarthr-ui/Asideを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.aside" -> "styled(Aside)")` } ] },
58
58
  { code: 'const StyledNav = styled.nav``', errors: [ { message: `"nav"を利用せず、smarthr-ui/Navを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.nav" -> "styled(Nav)")` } ] },
59
59
  { code: 'const StyledSection = styled.section``', errors: [ { message: `"section"を利用せず、smarthr-ui/Sectionを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.section" -> "styled(Section)")` } ] },
60
- { code: '<><Heading>hoge</Heading><Heading>hoge</Heading></>', errors: [ { message: rootMessage }, { message: rootMessage } ] },
61
- { code: '<><Heading>hoge</Heading><Section><Heading>hoge</Heading><Heading>hoge</Heading></Section></>', errors: [ { message }, { message } ] },
62
- { code: '<article>hoge</article>', errors: [ { message: `"article"を利用せず、smarthr-ui/Articleを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
63
- { code: '<aside>hoge</aside>', errors: [ { message: `"aside"を利用せず、smarthr-ui/Asideを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
64
- { code: '<nav>hoge</nav>', errors: [ { message: `"nav"を利用せず、smarthr-ui/Navを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
65
- { code: '<section>hoge</section>', errors: [ { message: `"section"を利用せず、smarthr-ui/Sectionを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
66
- { code: 'const Hoge = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>;const Fuga = () => <AbcHeading anyArg={abc}>hoge</AbcHeading>', errors: [ { message: rootMessage }, { message: rootMessage } ] },
60
+ { code: '<><PageHeading>hoge</PageHeading><PageHeading>fuga</PageHeading></>', errors: [ { message: pageMessage } ] },
61
+ { code: '<Heading>hoge</Heading>', errors: [ { message } ] },
62
+ { code: '<><Heading>hoge</Heading><Heading>fuga</Heading></>', errors: [ { message }, { message } ] },
63
+ { code: 'const Hoge = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>', errors: [ { message } ] },
64
+ { code: '<Section><Heading>hoge</Heading><Heading>fuga</Heading></Section>', errors: [ { message: lowerMessage } ] },
65
+ { code: '<Section><PageHeading>hoge</PageHeading></Section>', errors: [ { message: pageInSectionMessage } ] },
67
66
  ],
68
67
  });