eslint-plugin-primer-react 4.0.0-rc.b9f7dc6 → 4.0.0

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.
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: npm
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 99
8
+ allow:
9
+ - dependency-name: "eslint-plugin-github"
10
+ - dependency-name: "eslint-plugin-jsx-a11y"
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@
6
6
 
7
7
  - [#51](https://github.com/primer/eslint-plugin-primer-react/pull/51) [`a65aa32`](https://github.com/primer/eslint-plugin-primer-react/commit/a65aa32c612c7fe952ec47bb3d926cf8adae9fab) Thanks [@broccolinisoup](https://github.com/broccolinisoup)! - Add `a11y-tooltip-interactive-trigger`
8
8
 
9
+ * [#66](https://github.com/primer/eslint-plugin-primer-react/pull/66) [`d1df609`](https://github.com/primer/eslint-plugin-primer-react/commit/d1df609b260505ee9dea1740bc96a3187355d727) Thanks [@TylerJDev](https://github.com/TylerJDev)! - Add `a11y-explicit-heading` rule
10
+
11
+ ### Minor Changes
12
+
13
+ - [#67](https://github.com/primer/eslint-plugin-primer-react/pull/67) [`4dfdb47`](https://github.com/primer/eslint-plugin-primer-react/commit/4dfdb47b40e7f187573b8203830541b86cc7a953) Thanks [@TylerJDev](https://github.com/TylerJDev)! - \* Updates component mapping, adds `components.js`
14
+ - Bumps `eslint-plugin-github` and `eslint-plugin-jsx-a11y`
15
+ - Fixes bug in `a11y-explicit-heading` when using spread props, or variable for `as`
16
+
17
+ ### Patch Changes
18
+
19
+ - [#72](https://github.com/primer/eslint-plugin-primer-react/pull/72) [`522b9cc`](https://github.com/primer/eslint-plugin-primer-react/commit/522b9ccbcfb26d18f2ea8b2514a6a7975480aaa7) Thanks [@TylerJDev](https://github.com/TylerJDev)! - Removes `Link`, `Spinner` and `TabNav.Link` from component mapping
20
+
21
+ * [#73](https://github.com/primer/eslint-plugin-primer-react/pull/73) [`974d9e8`](https://github.com/primer/eslint-plugin-primer-react/commit/974d9e85c7460a05eb0345086b340b650700d24d) Thanks [@TylerJDev](https://github.com/TylerJDev)! - \* Fixes `nonInteractiveLink` rule for links that pass values through JSX rather than a string
22
+ - Adds optional chaining to `getJSXOpeningElementAttribute` to avoid error when no `name` is present
23
+
9
24
  ## 3.0.0
10
25
 
11
26
  ### Major Changes
package/README.md CHANGED
@@ -33,3 +33,4 @@ ESLint rules for Primer React
33
33
  - [no-deprecated-colors](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-colors.md)
34
34
  - [no-system-props](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-system-props.md)
35
35
  - [a11y-tooltip-interactive-trigger](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-tooltip-interactive-trigger.md)
36
+ - [a11y-explicit-heading](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-explicit-heading.md)
@@ -0,0 +1,37 @@
1
+ ## Require explicit heading level on `<Heading>` component
2
+
3
+ The `Heading` component does not require you to use `as` to specify the heading level, as it will default to an `h2` if one isn't specified. This may lead to inaccessible usage if the default is out of order in the existing heading hierarchy.
4
+
5
+ ## Rule Details
6
+
7
+ This rule enforces using `as` on the `<Heading>` component to specify a heading level (`h1`-`h6`). In addition, it enforces `as` usage to only be used for headings.
8
+
9
+ 👎 Examples of **incorrect** code for this rule
10
+
11
+ ```jsx
12
+ import {Heading} from '@primer/react'
13
+
14
+ <Heading>Heading without explicit heading level</Heading>
15
+ ```
16
+
17
+ `as` must only be for headings (`h1`-`h6`)
18
+
19
+ ```jsx
20
+ import {Heading} from '@primer/react'
21
+
22
+ <Heading as="span">Heading component used as "span"</Heading>
23
+ ```
24
+
25
+ 👍 Examples of **correct** code for this rule:
26
+
27
+ ```jsx
28
+ import {Heading} from '@primer/react';
29
+
30
+ <Heading as="h2">Heading level 2</Heading>
31
+ ```
32
+
33
+ ## Options
34
+
35
+ - `skipImportCheck` (default: `false`)
36
+
37
+ By default, the `a11y-explicit-heading` rule will only check for `<Heading>` components imported directly from `@primer/react`. You can disable this behavior by setting `skipImportCheck` to `true`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "4.0.0-rc.b9f7dc6",
3
+ "version": "4.0.0",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -24,7 +24,7 @@
24
24
  "@changesets/changelog-github": "^0.4.0",
25
25
  "@changesets/cli": "^2.16.0",
26
26
  "@github/prettier-config": "0.0.4",
27
- "@primer/primitives": "^5.1.0",
27
+ "@primer/primitives": "^7.11.14",
28
28
  "eslint": "^8.0.1",
29
29
  "jest": "^27.0.6"
30
30
  },
@@ -35,8 +35,8 @@
35
35
  "prettier": "@github/prettier-config",
36
36
  "dependencies": {
37
37
  "@styled-system/props": "^5.1.5",
38
- "eslint-plugin-github": "^4.3.7",
39
- "eslint-plugin-jsx-a11y": "^6.6.1",
38
+ "eslint-plugin-github": "^4.9.2",
39
+ "eslint-plugin-jsx-a11y": "^6.7.1",
40
40
  "eslint-traverse": "^1.0.0",
41
41
  "lodash": "^4.17.21",
42
42
  "styled-system": "^5.1.5"
@@ -0,0 +1,23 @@
1
+ const { flattenComponents } = require('../utils/flatten-components');
2
+
3
+ const components = flattenComponents({
4
+ Button: 'button',
5
+ IconButton: 'button',
6
+ ToggleSwitch: 'button',
7
+ Radio: 'input',
8
+ Checkbox: 'input',
9
+ Text: 'span',
10
+ TextInput: {
11
+ Action: 'button',
12
+ self: 'input',
13
+ },
14
+ Select: {
15
+ Option: 'option',
16
+ self: 'select',
17
+ },
18
+ TabNav: {
19
+ self: 'nav',
20
+ },
21
+ })
22
+
23
+ module.exports = components;
@@ -1,3 +1,5 @@
1
+ const components = require('./components');
2
+
1
3
  module.exports = {
2
4
  parserOptions: {
3
5
  sourceType: 'module',
@@ -11,20 +13,15 @@ module.exports = {
11
13
  'primer-react/direct-slot-children': 'error',
12
14
  'primer-react/no-deprecated-colors': 'warn',
13
15
  'primer-react/no-system-props': 'warn',
14
- 'primer-react/a11y-tooltip-interactive-trigger': 'error'
16
+ 'primer-react/a11y-tooltip-interactive-trigger': 'error',
17
+ 'primer-react/a11y-explicit-heading': 'error'
15
18
  },
16
19
  settings: {
17
20
  github: {
18
- components: {
19
- Link: {props: {as: {undefined: 'a', a: 'a', button: 'button'}}},
20
- Button: {default: 'button'}
21
- }
21
+ components: components
22
22
  },
23
23
  'jsx-a11y': {
24
- components: {
25
- Button: 'button',
26
- IconButton: 'button'
27
- }
24
+ components: components
28
25
  }
29
26
  }
30
27
  }
package/src/index.js CHANGED
@@ -3,7 +3,8 @@ module.exports = {
3
3
  'direct-slot-children': require('./rules/direct-slot-children'),
4
4
  'no-deprecated-colors': require('./rules/no-deprecated-colors'),
5
5
  'no-system-props': require('./rules/no-system-props'),
6
- 'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger')
6
+ 'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger'),
7
+ 'a11y-explicit-heading': require('./rules/a11y-explicit-heading')
7
8
  },
8
9
  configs: {
9
10
  recommended: require('./configs/recommended')
@@ -0,0 +1,63 @@
1
+ const rule = require('../a11y-explicit-heading')
2
+ const {RuleTester} = require('eslint')
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 'latest',
7
+ sourceType: 'module',
8
+ ecmaFeatures: {
9
+ jsx: true
10
+ }
11
+ }
12
+ })
13
+
14
+ ruleTester.run('a11y-explicit-heading', rule, {
15
+ valid: [
16
+ `import {Heading} from '@primer/react';
17
+ <Heading as="h1">Heading level 1</Heading>
18
+ `,
19
+ `import {Heading} from '@primer/react';
20
+ <Heading as="h2">Heading level 2</Heading>
21
+ `,
22
+ `import {Heading} from '@primer/react';
23
+ <Heading as="H3">Heading level 3</Heading>
24
+ `,
25
+ `import {Heading} from '@primer/react';
26
+ const args = {};
27
+ <Heading {...args}>A heading with spread props</Heading>
28
+ `,
29
+ `import {Heading} from '@primer/react';
30
+ const as = 'h3';
31
+ <Heading as={as}>Heading as passed prop</Heading>
32
+ `,
33
+ `import {Heading} from '@primer/react';
34
+ const args = {};
35
+ <Heading as="h2" {...args}>Heading level 2</Heading>
36
+ `,
37
+ `
38
+ import {Heading} from '@primer/react';
39
+ <Heading
40
+ {...someProps}
41
+ as="h3"
42
+ >
43
+ Passed spread props
44
+ </Heading>
45
+ `
46
+
47
+ ],
48
+ invalid: [
49
+ {
50
+ code:
51
+ `import {Heading} from '@primer/react';
52
+ <Heading>Heading without "as"</Heading>`,
53
+ errors: [{ messageId: 'nonExplicitHeadingLevel' }]
54
+ },
55
+ {
56
+ code:
57
+ `import {Heading} from '@primer/react';
58
+ <Heading as="span">Heading component used as "span"</Heading>
59
+ `,
60
+ errors: [{ messageId: 'invalidAsValue' }]
61
+ },
62
+ ]
63
+ })
@@ -51,7 +51,22 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
51
51
  `import {Tooltip, Link} from '@primer/react';
52
52
  <Tooltip aria-label="Supplementary text" direction="e">
53
53
  <Link href="https://github.com">Link</Link>
54
- </Tooltip>`
54
+ </Tooltip>`,
55
+ `
56
+ import {Tooltip, Link} from '@primer/react';
57
+ <Tooltip aria-label={avatar.avatarName} direction="e">
58
+ <Link href={avatar.avatarLink} underline={true}>
59
+ User avatar
60
+ </Link>
61
+ </Tooltip>` ,
62
+ `
63
+ import {Tooltip, Link} from '@primer/react';
64
+ <Tooltip aria-label="product" direction="e">
65
+ <Link href={productLink}>
66
+ Product
67
+ </Link>
68
+ </Tooltip>
69
+ `
55
70
  ],
56
71
  invalid: [
57
72
  {
@@ -96,7 +111,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
96
111
  </Tooltip>`,
97
112
  errors: [
98
113
  {
99
- messageId: 'anchorTagWithoutHref'
114
+ messageId: 'nonInteractiveLink'
100
115
  }
101
116
  ]
102
117
  },
@@ -108,7 +123,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
108
123
  </Tooltip>`,
109
124
  errors: [
110
125
  {
111
- messageId: 'anchorTagWithoutHref'
126
+ messageId: 'nonInteractiveLink'
112
127
  }
113
128
  ]
114
129
  },
@@ -120,7 +135,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
120
135
  </Tooltip>`,
121
136
  errors: [
122
137
  {
123
- messageId: 'hiddenInput'
138
+ messageId: 'nonInteractiveInput'
124
139
  }
125
140
  ]
126
141
  },
@@ -132,7 +147,43 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
132
147
  </Tooltip>`,
133
148
  errors: [
134
149
  {
135
- messageId: 'hiddenInput'
150
+ messageId: 'nonInteractiveInput'
151
+ }
152
+ ]
153
+ },
154
+ {
155
+ code: `
156
+ import {Tooltip, Button} from '@primer/react';
157
+ <Tooltip aria-label="Supplementary text" direction="e">
158
+ <Button disabled>Save</Button>
159
+ </Tooltip>`,
160
+ errors: [
161
+ {
162
+ messageId: 'nonInteractiveTrigger'
163
+ }
164
+ ]
165
+ },
166
+ {
167
+ code: `
168
+ import {Tooltip, Button} from '@primer/react';
169
+ <Tooltip aria-label="Supplementary text" direction="e">
170
+ <IconButton disabled>Save</IconButton>
171
+ </Tooltip>`,
172
+ errors: [
173
+ {
174
+ messageId: 'nonInteractiveTrigger'
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ code: `
180
+ import {Tooltip, Button} from '@primer/react';
181
+ <Tooltip aria-label="Supplementary text" direction="e">
182
+ <input disabled>Save</input>
183
+ </Tooltip>`,
184
+ errors: [
185
+ {
186
+ messageId: 'nonInteractiveInput'
136
187
  }
137
188
  ]
138
189
  },
@@ -159,7 +210,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
159
210
  </Tooltip>`,
160
211
  errors: [
161
212
  {
162
- messageId: 'anchorTagWithoutHref'
213
+ messageId: 'nonInteractiveLink'
163
214
  }
164
215
  ]
165
216
  }
@@ -0,0 +1,66 @@
1
+ const {isPrimerComponent} = require('../utils/is-primer-component')
2
+ const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
3
+ const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
4
+
5
+ const validHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
6
+
7
+ const isHeadingComponent = elem => getJSXOpeningElementName(elem) === 'Heading'
8
+ const isUsingAsProp = elem => {
9
+ const componentAs = getJSXOpeningElementAttribute(elem, 'as')
10
+
11
+ if (!componentAs) return
12
+
13
+ return componentAs.value
14
+ }
15
+
16
+ const isValidAsUsage = value => validHeadings.includes(value.toLowerCase())
17
+ const isInvalid = elem => {
18
+ if (elem.attributes.length === 1 && elem.attributes[0].type === 'JSXSpreadAttribute') return;
19
+
20
+ const elemAs = isUsingAsProp(elem)
21
+
22
+ if (!elemAs) return 'nonExplicitHeadingLevel'
23
+ if (elemAs.value && !isValidAsUsage(elemAs.value)) return 'invalidAsValue'
24
+
25
+ return false
26
+ }
27
+
28
+ module.exports = {
29
+ meta: {
30
+ docs: {
31
+ description: 'Heading component must have explicit heading level, and specific `as` usage.',
32
+ url: require('../url')(module),
33
+ },
34
+ schema: [
35
+ {
36
+ properties: {
37
+ skipImportCheck: {
38
+ type: 'boolean'
39
+ }
40
+ }
41
+ }
42
+ ],
43
+ messages: {
44
+ nonExplicitHeadingLevel: 'Heading must have an explicit heading level applied through the `as` prop.',
45
+ invalidAsValue: 'Usage of `as` must only be used for heading elements (h1-h6).'
46
+ }
47
+ },
48
+ create: function(context) {
49
+ return {
50
+ JSXOpeningElement(jsxNode) {
51
+ const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
52
+
53
+ if ((skipImportCheck || isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) && isHeadingComponent(jsxNode)) {
54
+ const error = isInvalid(jsxNode)
55
+
56
+ if (error) {
57
+ context.report({
58
+ node: jsxNode,
59
+ messageId: error
60
+ })
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
@@ -1,41 +1,55 @@
1
+ const {getPropValue, propName} = require('jsx-ast-utils')
1
2
  const {isPrimerComponent} = require('../utils/is-primer-component')
2
3
  const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
3
4
  const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
4
5
 
5
6
  const isInteractive = child => {
6
7
  const childName = getJSXOpeningElementName(child.openingElement)
7
- return ['button', 'summary', 'select', 'textarea', 'a', 'input', 'link', 'iconbutton', 'textinput'].includes(
8
- childName.toLowerCase()
8
+ return (
9
+ ['button', 'summary', 'select', 'textarea', 'a', 'input', 'link', 'iconbutton', 'textinput'].includes(
10
+ childName.toLowerCase()
11
+ ) && !hasDisabledAttr(child)
9
12
  )
10
13
  }
11
14
 
15
+ const hasDisabledAttr = child => {
16
+ const hasDisabledAttr = getJSXOpeningElementAttribute(child.openingElement, 'disabled')
17
+ return hasDisabledAttr
18
+ }
19
+
12
20
  const isAnchorTag = el => {
13
- return (
14
- getJSXOpeningElementName(el.openingElement) === 'a' ||
15
- getJSXOpeningElementName(el.openingElement).toLowerCase() === 'link'
16
- )
21
+ const openingEl = getJSXOpeningElementName(el.openingElement)
22
+ return openingEl === 'a' || openingEl.toLowerCase() === 'link'
23
+ }
24
+
25
+ const isJSXValue = (attributes) => {
26
+ const node = attributes.find(attribute => propName(attribute) === 'href');
27
+ const isJSXExpression = node.value.type === 'JSXExpressionContainer' && node
28
+ && typeof getPropValue(node) === 'string';
29
+
30
+ return isJSXExpression
17
31
  }
18
32
 
19
33
  const isInteractiveAnchor = child => {
20
34
  const hasHref = getJSXOpeningElementAttribute(child.openingElement, 'href')
21
35
  if (!hasHref) return false
22
36
  const href = getJSXOpeningElementAttribute(child.openingElement, 'href').value.value
23
- const isAnchorInteractive = typeof href === 'string' && href !== ''
37
+ const hasJSXValue = isJSXValue(child.openingElement.attributes);
38
+ const isAnchorInteractive = (typeof href === 'string' && href !== '' || hasJSXValue)
39
+
24
40
  return isAnchorInteractive
25
41
  }
26
42
 
27
43
  const isInputTag = el => {
28
- return (
29
- getJSXOpeningElementName(el.openingElement) === 'input' ||
30
- getJSXOpeningElementName(el.openingElement).toLowerCase() === 'textinput'
31
- )
44
+ const openingEl = getJSXOpeningElementName(el.openingElement)
45
+ return openingEl === 'input' || openingEl.toLowerCase() === 'textinput'
32
46
  }
33
47
 
34
48
  const isInteractiveInput = child => {
35
49
  const hasHiddenType =
36
50
  getJSXOpeningElementAttribute(child.openingElement, 'type') &&
37
51
  getJSXOpeningElementAttribute(child.openingElement, 'type').value.value === 'hidden'
38
- return !hasHiddenType
52
+ return !hasHiddenType && !hasDisabledAttr(child)
39
53
  }
40
54
 
41
55
  const isOtherThanAnchorOrInput = el => {
@@ -57,12 +71,12 @@ const getAllChildren = node => {
57
71
 
58
72
  const checks = [
59
73
  {
60
- id: 'anchorTagWithoutHref',
74
+ id: 'nonInteractiveLink',
61
75
  filter: jsxElement => isAnchorTag(jsxElement),
62
76
  check: isInteractiveAnchor
63
77
  },
64
78
  {
65
- id: 'hiddenInput',
79
+ id: 'nonInteractiveInput',
66
80
  filter: jsxElement => isInputTag(jsxElement),
67
81
  check: isInteractiveInput
68
82
  },
@@ -76,16 +90,11 @@ const checks = [
76
90
  const checkTriggerElement = jsxNode => {
77
91
  const elements = [...getAllChildren(jsxNode)]
78
92
  const hasInteractiveElement = elements.find(element => {
79
- if (
80
- getJSXOpeningElementName(element.openingElement) === 'a' ||
81
- getJSXOpeningElementName(element.openingElement) === 'Link'
82
- ) {
93
+ const openingEl = getJSXOpeningElementName(element.openingElement)
94
+ if (openingEl === 'a' || openingEl === 'Link') {
83
95
  return isInteractiveAnchor(element)
84
96
  }
85
- if (
86
- getJSXOpeningElementName(element.openingElement) === 'input' ||
87
- getJSXOpeningElementName(element.openingElement) === 'TextInput'
88
- ) {
97
+ if (openingEl === 'input' || openingEl === 'TextInput') {
89
98
  return isInteractiveInput(element)
90
99
  } else {
91
100
  return isInteractive(element)
@@ -110,10 +119,10 @@ const checkTriggerElement = jsxNode => {
110
119
  }
111
120
  // check the specificity of the errors. If there are multiple errors, only return the most specific one.
112
121
  if (errors.size > 1) {
113
- if (errors.has('anchorTagWithoutHref')) {
122
+ if (errors.has('nonInteractiveLink')) {
114
123
  errors.delete('nonInteractiveTrigger')
115
124
  }
116
- if (errors.has('hiddenInput')) {
125
+ if (errors.has('nonInteractiveInput')) {
117
126
  errors.delete('nonInteractiveTrigger')
118
127
  }
119
128
  }
@@ -135,11 +144,11 @@ module.exports = {
135
144
  ],
136
145
  messages: {
137
146
  nonInteractiveTrigger:
138
- 'The `Tooltip` component expects a single React element that contains interactive content. Consider using a `<button>` or equivalent interactive element instead.',
139
- anchorTagWithoutHref:
147
+ 'Tooltips should only be applied to interactive elements that are not disabled. Consider using a `<button>` or equivalent interactive element instead.',
148
+ nonInteractiveLink:
140
149
  'Anchor tags without an href attribute are not interactive, therefore they cannot be used as a trigger for a tooltip. Please add an href attribute or use an alternative interactive element instead',
141
- hiddenInput:
142
- 'Hidden inputs are not interactive and cannot be used as a trigger for a tooltip. Please use an alternate input type or use a different interactive element instead',
150
+ nonInteractiveInput:
151
+ 'Hidden or disabled inputs are not interactive and cannot be used as a trigger for a tooltip. Please use an alternate input type or use a different interactive element instead',
143
152
  singleChild: 'The `Tooltip` component expects a single React element as a child.'
144
153
  }
145
154
  },
package/src/url.js ADDED
@@ -0,0 +1,10 @@
1
+ const {homepage, version} = require('../package.json')
2
+ const path = require('path')
3
+
4
+ module.exports = ({id}) => {
5
+ const url = new URL(homepage)
6
+ const rule = path.basename(id, '.js')
7
+ url.hash = ''
8
+ url.pathname += `/blob/v${version}/docs/rules/${rule}.md`
9
+ return url.toString()
10
+ }
@@ -0,0 +1,62 @@
1
+ const {flattenComponents} = require('../flatten-components')
2
+
3
+ const mockComponents = function(passedObj) {
4
+ return {
5
+ Button: 'button',
6
+ Link: 'a',
7
+ Spinner: 'svg',
8
+ Radio: 'input',
9
+ TextInput: {
10
+ Action: 'button',
11
+ self: 'input'
12
+ },
13
+ ...passedObj
14
+ }
15
+ }
16
+
17
+ describe('getElementType', function() {
18
+ it('flattens passed object 1-level deep', function() {
19
+ const result = flattenComponents(mockComponents())
20
+
21
+ const expectedResult = {
22
+ Button: 'button',
23
+ Link: 'a',
24
+ Spinner: 'svg',
25
+ Radio: 'input',
26
+ TextInput: 'input',
27
+ 'TextInput.Action': 'button'
28
+ }
29
+
30
+ expect(result).toEqual(expectedResult)
31
+ })
32
+
33
+ it('ignores objects nested deeper than 1-level', function() {
34
+ const result = flattenComponents(
35
+ mockComponents({
36
+ Select: {
37
+ Items: {
38
+ self: 'div'
39
+ },
40
+ Option: 'option',
41
+ self: 'select'
42
+ }
43
+ })
44
+ )
45
+
46
+ const expectedResult = {
47
+ Button: 'button',
48
+ Link: 'a',
49
+ Spinner: 'svg',
50
+ Radio: 'input',
51
+ TextInput: 'input',
52
+ 'TextInput.Action': 'button',
53
+ 'Select.Items': {
54
+ self: 'div'
55
+ },
56
+ Select: 'select',
57
+ 'Select.Option': 'option'
58
+ }
59
+
60
+ expect(result).toEqual(expectedResult)
61
+ })
62
+ })
@@ -0,0 +1,17 @@
1
+ function flattenComponents(componentObj) {
2
+ let result = {};
3
+
4
+ Object.keys(componentObj).forEach((key) => {
5
+ if (typeof componentObj[key] === 'object') {
6
+ const test = Object.keys(componentObj[key]).forEach((item) => {
7
+ result = { ...result, [`${key}${item !== 'self' ? `.${item}` : ''}`]: componentObj[key][item] };
8
+ });
9
+ } else {
10
+ result = {...result, [key]: componentObj[key]}
11
+ }
12
+ });
13
+
14
+ return result;
15
+ }
16
+
17
+ exports.flattenComponents = flattenComponents
@@ -1,7 +1,7 @@
1
1
  function getJSXOpeningElementAttribute(openingEl, name) {
2
2
  const attributes = openingEl.attributes
3
3
  const attribute = attributes.find(attribute => {
4
- return attribute.name.name === name
4
+ return attribute.name && attribute.name.name === name
5
5
  })
6
6
 
7
7
  return attribute