@sj-distributor/react-iconfont-cli 2.5.0 → 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 +36 -0
- package/libs/generateComponent.js +2 -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/README.md
CHANGED
|
@@ -155,6 +155,42 @@ 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
|
+
|
|
158
194
|
## 无障碍支持(WCAG)
|
|
159
195
|
|
|
160
196
|
无障碍属性添加在最终生成的单图标 SVG 上。重新运行生成命令后,现有调用无需修改即可获得默认的可访问名称。
|
|
@@ -36,7 +36,8 @@ 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);
|
|
42
43
|
singleFile = (0, replace_1.replaceAriaLabel)(singleFile, getAccessibleLabel(iconIdAfterTrim));
|
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;
|