forked from delvtech/elf-council-frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsvgr-template.js
165 lines (151 loc) · 5.23 KB
/
svgr-template.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
const t = require("@babel/types");
/**
* This template is used in the @svgr/webpack loader options to ensure a unique
* id for every SVG, even when the same SVG is used multiple times on a single
* page. This is needed for 2 reasons:
* 1. 2 different SVGs exported from a design tool with the same id, on a
* `<pattern>` tag for example, will collide when used inline, and both
* SVGs will use the `<pattern>` in the first SVG.
* 2. If the first instance of an SVG in the DOM is hidden from either
* `display: none` or a `display: none` parent, tags in any other instance
* of that same SVG with references to other tags by id (e.g.,
* `fill="url(#elfi-icon-gradient)"`) will reference the hidden SVG tags
* which won't show.
*
* It also adds a conditional `<title>` tag and `aria-labeledby` attribute that
* only show if a title is passed in the props. This is needed because the title
* attribute on `<svg>` tags is invalid, but there may be times when an SVG
* needs a descriptor for screen readers, scrapers, and/or mouse over.
*
* https://react-svgr.com/docs/custom-templates/
*
* TODO: Handle this in a custom SVGO or Babel plugin
*/
module.exports = (
{ imports, interfaces, componentName, props, jsx, exports },
{ tpl },
) => {
addAccessibleTitle(componentName, jsx);
convertSVGIds(componentName, jsx);
return tpl`
${imports};
import { useMemo } from "react";
${interfaces};
const counter = (() => {
let counts = {};
return (name) => {
counts[name] = ++counts[name] || 1;
return counts[name];
};
})();
const ${componentName} = (${props}) => {
const key = useMemo(() => counter(${componentName}), []);
const getId = (baseId) => \`\${baseId}__\${key}\`;
const getRef = (baseId) => \`#\${getId(baseId)}\`;
const getUrl = (baseId) => \`url(\${getRef(baseId)})\`;
return ${jsx};
};
${exports};
`;
};
/**
* Adds a conditional `<title>` tag and `aria-labeledby` attribute that only
* show if a title is passed in the props.
*
* @param {string} componentName Used to prefix the id of the `<title>` tag.
* @param {JSXElement} ASTNode The node to add them to.
*/
function addAccessibleTitle(componentName, { openingElement, children }) {
const nameSpacedId = `${componentName}__title`;
if (openingElement) {
// AST of `aria-labeledby={props.title ? "foo_title" : undefined}`
openingElement.attributes.push(
t.jSXAttribute(
t.jsxIdentifier("aria-labelledby"),
t.jsxExpressionContainer(
t.conditionalExpression(
t.memberExpression(t.identifier("props"), t.identifier("title")),
t.stringLiteral(nameSpacedId),
t.identifier("undefined"),
),
),
),
);
}
// AST of `{props.title && <title id="foo_title" lang="en">{props.title}</title>}`
children.unshift(
t.jSXExpressionContainer(
t.logicalExpression(
"&&",
t.memberExpression(t.identifier("props"), t.identifier("title")),
t.jSXElement(
t.jSXOpeningElement(t.jSXIdentifier("title"), [
t.jSXAttribute(
t.jSXIdentifier("id"),
t.stringLiteral(nameSpacedId),
),
t.jSXAttribute(t.jSXIdentifier("lang"), t.stringLiteral("en")),
]),
t.jSXClosingElement(t.jSXIdentifier("title")),
[
t.jSXExpressionContainer(
t.memberExpression(t.identifier("props"), t.identifier("title")),
),
],
),
),
),
);
}
/**
* Modifies all id attributes and attributes that reference ids in an SVG AST
* node to call a function with a new prefixed id.
*
* @param {string} componentName Used to prefix the id.
* @param {JSXElement} ASTNode The node to modify.
*/
function convertSVGIds(componentName, { openingElement, children }) {
if (openingElement?.attributes?.length) {
for (const attribute of openingElement.attributes) {
const { name, value } = attribute;
// skip if the attribute has no value to change or if the value is a
// base64 string
if (!value.value || /^data:/.test(value.value)) {
continue;
}
// remove `#` before ids and `url()` around it to just get the id
// example: both `url(#foo)` and `#foo` would become `foo`
const baseId = value.value.replace(/(^(url\()?#|\)$)/g, "");
const nameSpacedId = `${componentName}__${baseId}`;
let fnName = null;
// has a url reference to an id
// example: `fill="url(#foo)"`
if (/^url\(#/.test(value.value)) {
fnName = "getUrl";
}
// has an href attribute to an id
// example: `href="#foo"`
else if (name.name === "href" || name.name === "xlinkHref") {
fnName = "getRef";
}
// has an id or aria-labelledby attribute
else if (name.name === "id" || name.name === "aria-labelledby") {
fnName = "getId";
}
if (fnName) {
// AST of `{foo('baz')}`
attribute.value = t.jSXExpressionContainer(
t.callExpression(t.identifier(fnName), [
t.stringLiteral(nameSpacedId),
]),
);
}
}
}
// run recursively for each child
if (children?.length) {
for (const child of children) {
convertSVGIds(componentName, child);
}
}
}