@sj-distributor/react-iconfont-cli 2.4.2 → 2.6.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.
- package/README.md +62 -0
- package/libs/generateComponent.js +12 -2
- package/libs/replace.d.ts +1 -0
- package/libs/replace.js +5 -1
- package/package.json +1 -1
- package/templates/Icon.d.ts.template +6 -2
- package/templates/Icon.js.template +64 -2
- package/templates/Icon.tsx.template +79 -5
- package/templates/SingleIcon.js.template +9 -1
- package/templates/SingleIcon.tsx.template +9 -1
package/README.md
CHANGED
|
@@ -155,6 +155,68 @@ export const App = () => {
|
|
|
155
155
|
</div>
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### 点击交互
|
|
159
|
+
|
|
160
|
+
不要直接把 SVG 图标作为交互控件。SVG 本身不是原生按钮,直接使用时还需要自行实现焦点、键盘操作、控件角色和可访问名称。
|
|
161
|
+
|
|
162
|
+
汇总 `IconFont` 组件检测到 `onClick` 后,会自动渲染一个原生 `button`,并将内部 SVG 标记为装饰元素;没有 `onClick` 时仍然只渲染 SVG:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
const handleIconClick = () => {
|
|
166
|
+
console.log("icon clicked");
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
<IconFont
|
|
170
|
+
name="alipay"
|
|
171
|
+
aria-label="打开支付宝"
|
|
172
|
+
onClick={handleIconClick}
|
|
173
|
+
buttonProps={{
|
|
174
|
+
className: "icon-button",
|
|
175
|
+
disabled: false,
|
|
176
|
+
}}
|
|
177
|
+
/>;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`buttonProps` 用于设置外层按钮的属性;`className`、`style` 等普通图标属性仍应用到内部 SVG。按钮仅默认设置 `type="button"`,不附带布局、尺寸、颜色或重置样式,调用方可以使用普通 CSS、CSS Modules 或 Tailwind CSS 自行控制。原生按钮已经支持 Enter/Space 键盘操作,因此不需要额外添加 `onKeyDown`。
|
|
181
|
+
|
|
182
|
+
交互模式下,顶层的 `aria-*`、`role` 和 `tabIndex` 会自动应用到外层按钮,不会留在已隐藏的 SVG 上。除 `aria-label` 和 `onClick` 外,也可以通过 `buttonProps` 设置按钮属性;同名属性以 `buttonProps` 为准。
|
|
183
|
+
|
|
184
|
+
调用方自定义按钮样式时,应保留清晰的键盘焦点状态,并根据适用的 WCAG 等级确保足够的点击区域和间距。
|
|
185
|
+
|
|
186
|
+
按需引入的单图标组件始终只渲染 SVG。如需交互,请由调用方显式包裹按钮:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<button type="button" onClick={handleIconClick} aria-label="打开支付宝">
|
|
190
|
+
<IconAlipay aria-hidden="true" />
|
|
191
|
+
</button>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 无障碍支持(WCAG)
|
|
195
|
+
|
|
196
|
+
无障碍属性添加在最终生成的单图标 SVG 上。重新运行生成命令后,现有调用无需修改即可获得默认的可访问名称。
|
|
197
|
+
|
|
198
|
+
将生成类似下面的 SVG:
|
|
199
|
+
|
|
200
|
+
```html
|
|
201
|
+
<svg role="img" aria-label="alipay" focusable="false">...</svg>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
默认的 `aria-label` 是本包提供的兜底值:它根据去除前缀后的图标名生成,并将连字符、下划线和驼峰命名转换为空格。例如 `arrow-left` 会生成 `aria-label="arrow left"`。
|
|
205
|
+
|
|
206
|
+
本包无法判断图标在具体页面中的含义和语言。调用方应根据实际语境覆盖默认标签:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
<IconFont name="alipay" aria-label="支付宝" />
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
如果图标只是装饰,或者所在按钮、链接已经有可访问名称,可以显式隐藏图标,避免读屏软件重复朗读:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<button type="button" aria-label="关闭">
|
|
216
|
+
<IconFont name="close" aria-hidden="true" />
|
|
217
|
+
</button>
|
|
218
|
+
```
|
|
219
|
+
|
|
158
220
|
## 更新图标
|
|
159
221
|
|
|
160
222
|
当 iconfont 项目图标有变更时,更新 `iconfont.json` 中的 `symbol_url` 后重新生成:
|
|
@@ -36,9 +36,11 @@ var generateComponent = function (data, config) {
|
|
|
36
36
|
names.push(iconIdAfterTrim);
|
|
37
37
|
cases += "".concat((0, whitespace_1.whitespace)(4), "case '").concat(iconIdAfterTrim, "':\n");
|
|
38
38
|
imports.push(componentName);
|
|
39
|
-
cases += "".concat((0, whitespace_1.whitespace)(6), "
|
|
39
|
+
cases += "".concat((0, whitespace_1.whitespace)(6), "icon = <").concat(componentName, " {...iconProps} />;\n");
|
|
40
|
+
cases += "".concat((0, whitespace_1.whitespace)(6), "break;\n");
|
|
40
41
|
singleFile = (0, getTemplate_1.getTemplate)("SingleIcon" + jsxExtension);
|
|
41
42
|
singleFile = (0, replace_1.replaceSize)(singleFile, config.default_icon_size);
|
|
43
|
+
singleFile = (0, replace_1.replaceAriaLabel)(singleFile, getAccessibleLabel(iconIdAfterTrim));
|
|
42
44
|
if (jsxExtension === ".tsx") {
|
|
43
45
|
singleFile = (0, replace_1.replaceReactName)(singleFile, config.can_import_react);
|
|
44
46
|
}
|
|
@@ -74,7 +76,7 @@ var generateComponent = function (data, config) {
|
|
|
74
76
|
exports.generateComponent = generateComponent;
|
|
75
77
|
var generateCase = function (data, baseIdent) {
|
|
76
78
|
var e_1, _a;
|
|
77
|
-
var template = "\n".concat((0, whitespace_1.whitespace)(baseIdent), "<svg viewBox=\"").concat(data.$.viewBox, "\" width={size} height={size} style={style} {...rest}>\n");
|
|
79
|
+
var template = "\n".concat((0, whitespace_1.whitespace)(baseIdent), "<svg viewBox=\"").concat(data.$.viewBox, "\" width={size} height={size} style={style} role={role} aria-label={accessibilityLabel} focusable=\"false\" {...rest}>\n");
|
|
78
80
|
// Determine the primary fill color (first path's fill)
|
|
79
81
|
// to distinguish primary paths from detail paths at runtime.
|
|
80
82
|
var primaryFill = getPrimaryFill(data);
|
|
@@ -115,6 +117,14 @@ var generateCase = function (data, baseIdent) {
|
|
|
115
117
|
template += "".concat((0, whitespace_1.whitespace)(baseIdent), "</svg>\n");
|
|
116
118
|
return template;
|
|
117
119
|
};
|
|
120
|
+
var getAccessibleLabel = function (iconName) {
|
|
121
|
+
var label = iconName
|
|
122
|
+
.replace(/([a-z\d])([A-Z])/g, "$1 $2")
|
|
123
|
+
.replace(/[-_.=+#@!~*]+/g, " ")
|
|
124
|
+
.replace(/\s+/g, " ")
|
|
125
|
+
.trim();
|
|
126
|
+
return label || iconName;
|
|
127
|
+
};
|
|
118
128
|
var getPrimaryFill = function (data) {
|
|
119
129
|
var e_2, _a;
|
|
120
130
|
var _b;
|
package/libs/replace.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const replaceSize: (content: string, size: number) => string;
|
|
2
|
+
export declare const replaceAriaLabel: (content: string, label: string) => string;
|
|
2
3
|
export declare const replaceCases: (content: string, cases: string) => string;
|
|
3
4
|
export declare const replaceNames: (content: string, names: string[]) => string;
|
|
4
5
|
export declare const replaceNamesArray: (content: string, names: string[]) => string;
|
package/libs/replace.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.replaceExports = exports.replaceSizeUnit = exports.replaceImports = exports.replaceSingleIconContent = exports.replaceReactName = exports.replaceComponentName = exports.replaceNamesArray = exports.replaceNames = exports.replaceCases = exports.replaceSize = void 0;
|
|
3
|
+
exports.replaceExports = exports.replaceSizeUnit = exports.replaceImports = exports.replaceSingleIconContent = exports.replaceReactName = exports.replaceComponentName = exports.replaceNamesArray = exports.replaceNames = exports.replaceCases = exports.replaceAriaLabel = exports.replaceSize = void 0;
|
|
4
4
|
var replaceSize = function (content, size) {
|
|
5
5
|
return content.replace(/#size#/g, String(size));
|
|
6
6
|
};
|
|
7
7
|
exports.replaceSize = replaceSize;
|
|
8
|
+
var replaceAriaLabel = function (content, label) {
|
|
9
|
+
return content.replace(/#ariaLabel#/g, JSON.stringify(label));
|
|
10
|
+
};
|
|
11
|
+
exports.replaceAriaLabel = replaceAriaLabel;
|
|
8
12
|
var replaceCases = function (content, cases) {
|
|
9
13
|
return content.replace(/#cases#/g, cases);
|
|
10
14
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ButtonHTMLAttributes, FunctionComponent, MouseEventHandler, SVGAttributes } from 'react';
|
|
4
4
|
#exports#
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label' | 'children' | 'dangerouslySetInnerHTML' | 'onClick'>;
|
|
7
|
+
|
|
8
|
+
interface Props extends Omit<SVGAttributes<SVGElement>, 'color' | 'onClick'> {
|
|
7
9
|
name: '#names#';
|
|
8
10
|
size?: number;
|
|
9
11
|
color?: string | string[];
|
|
12
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
13
|
+
buttonProps?: ButtonProps;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
declare const IconFont: FunctionComponent<Props>;
|
|
@@ -4,12 +4,74 @@ import React from 'react';
|
|
|
4
4
|
#imports#
|
|
5
5
|
#exports#
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const splitInteractiveProps = (props) => {
|
|
8
|
+
const buttonA11yProps = {};
|
|
9
|
+
const iconProps = {};
|
|
10
|
+
|
|
11
|
+
Object.keys(props).forEach((propName) => {
|
|
12
|
+
if (
|
|
13
|
+
propName.indexOf('aria-') === 0 ||
|
|
14
|
+
propName === 'role' ||
|
|
15
|
+
propName === 'tabIndex'
|
|
16
|
+
) {
|
|
17
|
+
buttonA11yProps[propName] = props[propName];
|
|
18
|
+
} else {
|
|
19
|
+
iconProps[propName] = props[propName];
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return { buttonA11yProps, iconProps };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getAccessibleLabel = (iconName) => {
|
|
27
|
+
const label = iconName
|
|
28
|
+
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
29
|
+
.replace(/[-_.=+#@!~*]+/g, ' ')
|
|
30
|
+
.replace(/\s+/g, ' ')
|
|
31
|
+
.trim();
|
|
32
|
+
|
|
33
|
+
return label || iconName;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const IconFont = ({
|
|
37
|
+
name,
|
|
38
|
+
onClick,
|
|
39
|
+
buttonProps = {},
|
|
40
|
+
'aria-label': ariaLabel,
|
|
41
|
+
...rest
|
|
42
|
+
}) => {
|
|
43
|
+
const interactiveProps = splitInteractiveProps(rest);
|
|
44
|
+
const iconProps = onClick
|
|
45
|
+
? { ...interactiveProps.iconProps, 'aria-hidden': true }
|
|
46
|
+
: { ...rest, 'aria-label': ariaLabel };
|
|
47
|
+
let icon = null;
|
|
48
|
+
|
|
8
49
|
switch (name) {
|
|
9
50
|
#cases#
|
|
10
51
|
}
|
|
11
52
|
|
|
12
|
-
|
|
53
|
+
if (!icon || !onClick) {
|
|
54
|
+
return icon;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { type = 'button', ...restButtonProps } = buttonProps;
|
|
58
|
+
delete restButtonProps.dangerouslySetInnerHTML;
|
|
59
|
+
const accessibilityLabel =
|
|
60
|
+
typeof ariaLabel === 'string' && ariaLabel.trim()
|
|
61
|
+
? ariaLabel.trim()
|
|
62
|
+
: getAccessibleLabel(name);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
{...interactiveProps.buttonA11yProps}
|
|
67
|
+
{...restButtonProps}
|
|
68
|
+
type={type}
|
|
69
|
+
aria-label={accessibilityLabel}
|
|
70
|
+
onClick={onClick}
|
|
71
|
+
>
|
|
72
|
+
{icon}
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
13
75
|
};
|
|
14
76
|
|
|
15
77
|
export default IconFont;
|
|
@@ -1,24 +1,98 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
|
|
4
|
-
import #reactName#{
|
|
4
|
+
import #reactName#{ ButtonHTMLAttributes, FunctionComponent, MouseEventHandler, ReactElement, SVGAttributes } from 'react';
|
|
5
5
|
#imports#
|
|
6
6
|
#exports#
|
|
7
7
|
|
|
8
8
|
export type IconNames = '#names#';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label' | 'children' | 'dangerouslySetInnerHTML' | 'onClick'>;
|
|
11
|
+
|
|
12
|
+
type IconProps = Omit<SVGAttributes<SVGElement>, 'color' | 'onClick'> & {
|
|
12
13
|
size?: number;
|
|
13
14
|
color?: string | string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface Props extends IconProps {
|
|
18
|
+
name: IconNames;
|
|
19
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
20
|
+
buttonProps?: ButtonProps;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
const
|
|
23
|
+
const splitInteractiveProps = (props: IconProps) => {
|
|
24
|
+
const source = props as Record<string, unknown>;
|
|
25
|
+
const buttonA11yProps: Record<string, unknown> = {};
|
|
26
|
+
const iconProps: Record<string, unknown> = {};
|
|
27
|
+
|
|
28
|
+
Object.keys(source).forEach((propName) => {
|
|
29
|
+
if (
|
|
30
|
+
propName.indexOf('aria-') === 0 ||
|
|
31
|
+
propName === 'role' ||
|
|
32
|
+
propName === 'tabIndex'
|
|
33
|
+
) {
|
|
34
|
+
buttonA11yProps[propName] = source[propName];
|
|
35
|
+
} else {
|
|
36
|
+
iconProps[propName] = source[propName];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
buttonA11yProps: buttonA11yProps as ButtonHTMLAttributes<HTMLButtonElement>,
|
|
42
|
+
iconProps: iconProps as IconProps,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getAccessibleLabel = (iconName: string): string => {
|
|
47
|
+
const label = iconName
|
|
48
|
+
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
49
|
+
.replace(/[-_.=+#@!~*]+/g, ' ')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim();
|
|
52
|
+
|
|
53
|
+
return label || iconName;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const IconFont: FunctionComponent<Props> = ({
|
|
57
|
+
name,
|
|
58
|
+
onClick,
|
|
59
|
+
buttonProps = {},
|
|
60
|
+
'aria-label': ariaLabel,
|
|
61
|
+
...rest
|
|
62
|
+
}) => {
|
|
63
|
+
const interactiveProps = splitInteractiveProps(rest);
|
|
64
|
+
const iconProps = onClick
|
|
65
|
+
? { ...interactiveProps.iconProps, 'aria-hidden': true }
|
|
66
|
+
: { ...rest, 'aria-label': ariaLabel };
|
|
67
|
+
let icon: ReactElement | null = null;
|
|
68
|
+
|
|
17
69
|
switch (name) {
|
|
18
70
|
#cases#
|
|
19
71
|
}
|
|
20
72
|
|
|
21
|
-
|
|
73
|
+
if (!icon || !onClick) {
|
|
74
|
+
return icon;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { type = 'button', ...restButtonProps } = buttonProps;
|
|
78
|
+
const safeButtonProps = restButtonProps as ButtonHTMLAttributes<HTMLButtonElement>;
|
|
79
|
+
delete safeButtonProps.dangerouslySetInnerHTML;
|
|
80
|
+
const accessibilityLabel =
|
|
81
|
+
typeof ariaLabel === 'string' && ariaLabel.trim()
|
|
82
|
+
? ariaLabel.trim()
|
|
83
|
+
: getAccessibleLabel(name);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<button
|
|
87
|
+
{...interactiveProps.buttonA11yProps}
|
|
88
|
+
{...safeButtonProps}
|
|
89
|
+
type={type}
|
|
90
|
+
aria-label={accessibilityLabel}
|
|
91
|
+
onClick={onClick}
|
|
92
|
+
>
|
|
93
|
+
{icon}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
22
96
|
};
|
|
23
97
|
|
|
24
98
|
export default IconFont;
|
|
@@ -7,8 +7,16 @@ const DEFAULT_STYLE = {
|
|
|
7
7
|
display: 'block',
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
const #componentName# = ({
|
|
10
|
+
const #componentName# = ({
|
|
11
|
+
size = #size#,
|
|
12
|
+
color,
|
|
13
|
+
style: _style,
|
|
14
|
+
role = 'img',
|
|
15
|
+
'aria-label': ariaLabel,
|
|
16
|
+
...rest
|
|
17
|
+
}) => {
|
|
11
18
|
const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE;
|
|
19
|
+
const accessibilityLabel = typeof ariaLabel === 'string' && ariaLabel.trim() ? ariaLabel.trim() : #ariaLabel#;
|
|
12
20
|
|
|
13
21
|
return (#iconContent# );
|
|
14
22
|
};
|
|
@@ -13,8 +13,16 @@ const DEFAULT_STYLE: CSSProperties = {
|
|
|
13
13
|
display: 'block',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const #componentName#: FunctionComponent<Props> = ({
|
|
16
|
+
const #componentName#: FunctionComponent<Props> = ({
|
|
17
|
+
size = #size#,
|
|
18
|
+
color,
|
|
19
|
+
style: _style,
|
|
20
|
+
role = 'img',
|
|
21
|
+
'aria-label': ariaLabel,
|
|
22
|
+
...rest
|
|
23
|
+
}) => {
|
|
17
24
|
const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE;
|
|
25
|
+
const accessibilityLabel = ariaLabel && ariaLabel.trim() ? ariaLabel.trim() : #ariaLabel#;
|
|
18
26
|
|
|
19
27
|
return (#iconContent# );
|
|
20
28
|
};
|