Skip to content

Commit 21409ae

Browse files
committed
feat: add titleId prop to enhance a11y
Closes #360
1 parent b56407e commit 21409ae

File tree

7 files changed

+80
-23
lines changed

7 files changed

+80
-23
lines changed

packages/babel-plugin-svg-dynamic-title/src/index.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,46 @@ const plugin = ({ types: t }) => ({
1818
children,
1919
)
2020
}
21+
22+
function createTitleIdAttribute() {
23+
return t.jsxAttribute(
24+
t.jsxIdentifier('id'),
25+
t.jsxExpressionContainer(t.identifier('titleId')),
26+
)
27+
}
28+
29+
function enhanceAttributes(attributes) {
30+
const existingId = attributes.find(
31+
attribute => attribute.name.name === 'id',
32+
)
33+
if (!existingId) {
34+
return [...attributes, createTitleIdAttribute()]
35+
}
36+
existingId.value = t.jsxExpressionContainer(
37+
t.logicalExpression('||', t.identifier('titleId'), existingId.value),
38+
)
39+
return attributes
40+
}
41+
2142
function getTitleElement(existingTitle) {
2243
const titleExpression = t.identifier('title')
44+
if (existingTitle) {
45+
existingTitle.openingElement.attributes = enhanceAttributes(
46+
existingTitle.openingElement.attributes,
47+
)
48+
}
2349
let titleElement = t.conditionalExpression(
2450
titleExpression,
2551
createTitle(
2652
[t.jsxExpressionContainer(titleExpression)],
27-
existingTitle ? existingTitle.openingElement.attributes : [],
53+
existingTitle
54+
? existingTitle.openingElement.attributes
55+
: [
56+
t.jsxAttribute(
57+
t.jsxIdentifier('id'),
58+
t.jsxExpressionContainer(t.identifier('titleId')),
59+
),
60+
],
2861
),
2962
t.nullLiteral(),
3063
)

packages/babel-plugin-svg-dynamic-title/src/index.test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,44 @@ const testPlugin = (code, options) => {
1313
describe('plugin', () => {
1414
it('should add title attribute if not present', () => {
1515
expect(testPlugin('<svg></svg>')).toMatchInlineSnapshot(
16-
`"<svg>{title ? <title>{title}</title> : null}</svg>;"`,
16+
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
1717
)
1818
})
1919

2020
it('should add title element and fallback to existing title', () => {
2121
// testing when the existing title contains a simple string
2222
expect(testPlugin(`<svg><title>Hello</title></svg>`)).toMatchInlineSnapshot(
23-
`"<svg>{title === undefined ? <title>Hello</title> : title ? <title>{title}</title> : null}</svg>;"`,
23+
`"<svg>{title === undefined ? <title id={titleId}>Hello</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
2424
)
2525
// testing when the existing title contains an JSXExpression
2626
expect(
2727
testPlugin(`<svg><title>{"Hello"}</title></svg>`),
2828
).toMatchInlineSnapshot(
29-
`"<svg>{title === undefined ? <title>{\\"Hello\\"}</title> : title ? <title>{title}</title> : null}</svg>;"`,
29+
`"<svg>{title === undefined ? <title id={titleId}>{\\"Hello\\"}</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
3030
)
3131
})
3232
it('should preserve any existing title attributes', () => {
3333
// testing when the existing title contains a simple string
3434
expect(
35-
testPlugin(`<svg><title attr='a'>Hello</title></svg>`),
35+
testPlugin(`<svg><title id='a'>Hello</title></svg>`),
3636
).toMatchInlineSnapshot(
37-
`"<svg>{title === undefined ? <title attr='a'>Hello</title> : title ? <title attr='a'>{title}</title> : null}</svg>;"`,
37+
`"<svg>{title === undefined ? <title id={titleId || 'a'}>Hello</title> : title ? <title id={titleId || 'a'}>{title}</title> : null}</svg>;"`,
3838
)
3939
})
4040
it('should support empty title', () => {
4141
expect(testPlugin('<svg><title></title></svg>')).toMatchInlineSnapshot(
42-
`"<svg>{title ? <title>{title}</title> : null}</svg>;"`,
42+
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
4343
)
4444
})
4545
it('should support self closing title', () => {
4646
expect(testPlugin('<svg><title /></svg>')).toMatchInlineSnapshot(
47-
`"<svg>{title ? <title>{title}</title> : null}</svg>;"`,
47+
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
4848
)
4949
})
5050

5151
it('should work if an attribute is already present', () => {
5252
expect(testPlugin('<svg><foo /></svg>')).toMatchInlineSnapshot(
53-
`"<svg>{title ? <title>{title}</title> : null}<foo /></svg>;"`,
53+
`"<svg>{title ? <title id={titleId}>{title}</title> : null}<foo /></svg>;"`,
5454
)
5555
})
5656
})

packages/babel-plugin-transform-svg-component/src/util.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ export const getProps = ({ types: t }, opts) => {
2121
true,
2222
),
2323
)
24+
25+
props.push(
26+
t.objectProperty(
27+
t.identifier('titleId'),
28+
t.identifier('titleId'),
29+
false,
30+
true,
31+
),
32+
)
2433
}
2534

2635
if (opts.expandProps) {

packages/babel-preset/src/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ const plugin = (api, opts) => {
4646
]
4747
}
4848

49+
if (opts.titleProp) {
50+
toAddAttributes = [
51+
...toAddAttributes,
52+
{
53+
name: 'aria-labelledby',
54+
value: 'titleId',
55+
literal: true,
56+
},
57+
]
58+
}
59+
4960
if (opts.expandProps) {
5061
toAddAttributes = [
5162
...toAddAttributes,

packages/babel-preset/src/index.test.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ describe('preset', () => {
6666
"import React from \\"react\\";
6767
6868
function SvgComponent({
69-
title
69+
title,
70+
titleId
7071
}) {
71-
return <svg>{title ? <title>{title}</title> : null}</svg>;
72+
return <svg aria-labelledby={titleId}>{title ? <title id={titleId}>{title}</title> : null}</svg>;
7273
}
7374
7475
export default SvgComponent;"
@@ -87,9 +88,10 @@ describe('preset', () => {
8788
"import React from \\"react\\";
8889
8990
function SvgComponent({
90-
title
91+
title,
92+
titleId
9193
}) {
92-
return <svg>{title === undefined ? <title>Hello</title> : title ? <title>{title}</title> : null}</svg>;
94+
return <svg aria-labelledby={titleId}>{title === undefined ? <title id={titleId}>Hello</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;
9395
}
9496
9597
export default SvgComponent;"
@@ -106,9 +108,10 @@ describe('preset', () => {
106108
"import React from \\"react\\";
107109
108110
function SvgComponent({
109-
title
111+
title,
112+
titleId
110113
}) {
111-
return <svg>{title === undefined ? <title>{\\"Hello\\"}</title> : title ? <title>{title}</title> : null}</svg>;
114+
return <svg aria-labelledby={titleId}>{title === undefined ? <title id={titleId}>{\\"Hello\\"}</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;
112115
}
113116
114117
export default SvgComponent;"

packages/cli/src/__snapshots__/index.test.js.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,10 @@ export default SvgFile
381381
exports[`cli should support various args: --title-prop 1`] = `
382382
"import React from 'react'
383383
384-
function SvgFile({ title, ...props }) {
384+
function SvgFile({ title, titleId, ...props }) {
385385
return (
386-
<svg width={48} height={1} {...props}>
387-
{title ? <title>{title}</title> : null}
386+
<svg width={48} height={1} aria-labelledby={titleId} {...props}>
387+
{title ? <title id={titleId}>{title}</title> : null}
388388
<path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
389389
</svg>
390390
)

packages/core/src/__snapshots__/convert.test.js.snap

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,10 +360,10 @@ export default noop
360360
exports[`convert config should support options 16 1`] = `
361361
"import React from 'react'
362362
363-
function SvgComponent({ title, ...props }) {
363+
function SvgComponent({ title, titleId, ...props }) {
364364
return (
365-
<svg width={88} height={88} {...props}>
366-
{title ? <title>{title}</title> : null}
365+
<svg width={88} height={88} aria-labelledby={titleId} {...props}>
366+
{title ? <title id={titleId}>{title}</title> : null}
367367
<g
368368
stroke=\\"#063855\\"
369369
strokeWidth={2}
@@ -408,17 +408,18 @@ export default MemoSvgComponent
408408
exports[`convert config titleProp: without title added 1`] = `
409409
"import React from 'react'
410410
411-
function SvgComponent({ title, ...props }) {
411+
function SvgComponent({ title, titleId, ...props }) {
412412
return (
413413
<svg
414414
width={0}
415415
height={0}
416416
style={{
417417
position: 'absolute',
418418
}}
419+
aria-labelledby={titleId}
419420
{...props}
420421
>
421-
{title ? <title>{title}</title> : null}
422+
{title ? <title id={titleId}>{title}</title> : null}
422423
<path d=\\"M0 0h24v24H0z\\" fill=\\"none\\" />
423424
</svg>
424425
)

0 commit comments

Comments
 (0)