Skip to content

Commit f6c8d2b

Browse files
Add tabIndex prop to React Icon component (#849)
* add focusable prop to svg * add changeset and write tests * update snapshots and tests and add docs * tabIndex prop introduced * update tree shaking snapshot * changeset update & remove type * Minor copy edits Co-authored-by: Cole Bemis <[email protected]>
1 parent 0c758fe commit f6c8d2b

File tree

7 files changed

+46
-2
lines changed

7 files changed

+46
-2
lines changed

.changeset/sixty-nails-juggle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/octicons': minor
3+
---
4+
5+
Add `tabIndex` prop to React icon components

docs/content/packages/react.mdx

+18
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ export default () => (
6666
)
6767
```
6868

69+
### `tabIndex`
70+
71+
You can add the `tabindex` attribute to an SVG element via the `tabIndex` prop if the SVG element is intended to be interactive.
72+
`tabIndex` prop also controls the `focusable` attribute of the SVG element which is defined by SVG Tiny 1.2 and only implemented in
73+
Internet Explorer and Microsoft Edge.
74+
75+
If there is no `tabIndex` prop is present (default behavior), it will set the `focusable` attribute to `false`. This is helpful
76+
for preventing the decorative SVG from being announced by some specialized assistive technology browsing modes which can get delayed
77+
while trying to parse the SVG markup.
78+
79+
```js
80+
// Example usage
81+
import {PlusIcon} from '@primer/octicons-react'
82+
export default () => (
83+
<PlusIcon aria-label="Interactive Plus Icon" tabIndex={0} /> New Item
84+
)
85+
```
86+
6987
### Sizes
7088

7189
The `size` prop takes `small`, `medium`, and `large` values that can be used to

lib/octicons_react/__tests__/tree-shaking.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ test('tree shaking single export', async () => {
5050
})
5151

5252
const bundleSize = Buffer.byteLength(output[0].code.trim()) / 1000
53-
expect(`${bundleSize}kB`).toMatchInlineSnapshot(`"2.484kB"`)
53+
expect(`${bundleSize}kB`).toMatchInlineSnapshot(`"2.595kB"`)
5454
})

lib/octicons_react/src/__tests__/__snapshots__/octicon.js.snap

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ exports[`An icon component matches snapshot 1`] = `
55
aria-hidden="true"
66
class="octicon octicon-alert"
77
fill="currentColor"
8+
focusable="false"
89
height="16"
910
role="img"
1011
style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"

lib/octicons_react/src/__tests__/octicon.js

+17
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,23 @@ describe('An icon component', () => {
6969
expect(container.querySelector('svg')).toHaveAttribute('aria-label', 'icon')
7070
})
7171

72+
it('set the focusable prop to false if tabIndex prop is not present', () => {
73+
const {container} = render(<AlertIcon />)
74+
expect(container.querySelector('svg')).toHaveAttribute('focusable', 'false')
75+
})
76+
77+
it('sets focusable prop to true if tabIndex prop is present and greater than 0', () => {
78+
const {container} = render(<AlertIcon aria-label="icon" tabIndex={0} />)
79+
expect(container.querySelector('svg')).toHaveAttribute('tabindex', '0')
80+
expect(container.querySelector('svg')).toHaveAttribute('focusable', 'true')
81+
})
82+
83+
it('sets focusable prop to false if tabIndex prop is -1', () => {
84+
const {container} = render(<AlertIcon aria-label="icon" tabIndex={-1} />)
85+
expect(container.querySelector('svg')).toHaveAttribute('tabindex', '-1')
86+
expect(container.querySelector('svg')).toHaveAttribute('focusable', 'false')
87+
})
88+
7289
it('respects the className prop', () => {
7390
const {container} = render(<AlertIcon className="foo" />)
7491
expect(container.querySelector('svg')).toHaveAttribute('class', 'foo')

lib/octicons_react/src/createIconComponent.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function createIconComponent(name, defaultClassName, getSVGData) {
1010
const svgDataByHeight = getSVGData()
1111
const heights = Object.keys(svgDataByHeight)
1212

13-
function Icon({'aria-label': ariaLabel, className, fill = 'currentColor', size, verticalAlign}) {
13+
function Icon({'aria-label': ariaLabel, tabIndex, className, fill = 'currentColor', size, verticalAlign}) {
1414
const height = sizeMap[size] || size
1515
const naturalHeight = closestNaturalHeight(heights, height)
1616
const naturalWidth = svgDataByHeight[naturalHeight].width
@@ -20,6 +20,8 @@ export function createIconComponent(name, defaultClassName, getSVGData) {
2020
return (
2121
<svg
2222
aria-hidden={ariaLabel ? 'false' : 'true'}
23+
tabIndex={tabIndex}
24+
focusable={tabIndex >= 0 ? 'true' : 'false'}
2325
aria-label={ariaLabel}
2426
role="img"
2527
className={className}

lib/octicons_react/src/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type Size = 'small' | 'medium' | 'large'
66

77
export interface OcticonProps {
88
'aria-label'?: string
9+
tabIndex?: number
910
children?: React.ReactElement<any>
1011
className?: string
1112
fill?: string

0 commit comments

Comments
 (0)