Skip to content

Commit 9092cae

Browse files
authored
feat: add tag component (#50)
* feat: add tag component * chore: clean code * docs: add demo * fix: compiler error
1 parent bc32ada commit 9092cae

File tree

16 files changed

+702
-1
lines changed

16 files changed

+702
-1
lines changed

packages/sqi-web/src/config-provider/type.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import type { PopupProps } from '../popup';
99
import type { RadioGroupProps, RadioProps } from '../radio';
1010
import type { SpaceProps } from '../space';
1111
import type { SwitchProps } from '../switch';
12-
import type { TooltipProps } from '../tooltip/type';
12+
import type { TagProps } from '../tag';
13+
import type { TooltipProps } from '../tooltip';
1314
import type { TriggerProps } from '../trigger';
1415

1516
export type ConfigSize = 'sm' | 'md' | 'lg';
@@ -51,6 +52,7 @@ export type ComponentConfig = {
5152
Row?: RowProps;
5253
Space?: SpaceProps;
5354
Switch?: SwitchProps;
55+
Tag?: TagProps;
5456
Tooltip?: TooltipProps;
5557
Trigger?: TriggerProps;
5658
};

packages/sqi-web/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export * from './popup';
99
export * from './radio';
1010
export * from './space';
1111
export * from './switch';
12+
export * from './tag';
1213
export * from './tooltip';
1314
export * from './trigger';

packages/sqi-web/src/tag/Tag.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client';
2+
import React, { cloneElement, forwardRef, isValidElement, useContext, useState } from 'react';
3+
import type { TagProps } from './type';
4+
import { ConfigContext } from '../config-provider/context';
5+
import { useMergeProps } from '@sqi-ui/hooks';
6+
import clsx from 'clsx';
7+
import { CloseIcon } from '@sqi-ui/icons';
8+
9+
const defaultProps: TagProps = {};
10+
11+
const Tag = forwardRef<HTMLSpanElement, TagProps>((baseProps, ref) => {
12+
const { prefixCls, size: ctxSize = 'md', componentConfig } = useContext(ConfigContext);
13+
const {
14+
children,
15+
className,
16+
size = ctxSize,
17+
title,
18+
bordered,
19+
icon,
20+
closable,
21+
onClose,
22+
color,
23+
style,
24+
onClick,
25+
...restProps
26+
} = useMergeProps(baseProps, defaultProps, componentConfig?.Tag);
27+
28+
const [innerVisible, setInnerVisible] = useState(true);
29+
30+
const classes = clsx(
31+
`${prefixCls}-tag`,
32+
{
33+
[`${prefixCls}-tag-size-${size}`]: size,
34+
[`${prefixCls}-tag-has-color`]: color,
35+
[`${prefixCls}-tag-bordered`]: bordered,
36+
},
37+
className,
38+
);
39+
40+
// ======================== Close Icon =============================
41+
42+
const onInnerClose: React.MouseEventHandler<HTMLElement> = (e) => {
43+
e.stopPropagation();
44+
onClose?.(e);
45+
46+
if (e.defaultPrevented) return;
47+
48+
setInnerVisible(false);
49+
};
50+
51+
const renderCloseIcon = () => {
52+
if (!closable) return null;
53+
54+
const closeCls = `${prefixCls}-tag-close-icon`;
55+
56+
if (isValidElement(closable)) {
57+
return cloneElement(closable as any, {
58+
className: closeCls,
59+
onClick: (e: React.MouseEvent<HTMLElement>) => {
60+
(closable as any).props.onClick?.(e);
61+
if (!e.defaultPrevented) onInnerClose(e);
62+
},
63+
});
64+
}
65+
return <CloseIcon onClick={onInnerClose} className={closeCls} />;
66+
};
67+
68+
if (innerVisible === false) return null;
69+
70+
return (
71+
<span
72+
{...restProps}
73+
ref={ref}
74+
title={title}
75+
className={classes}
76+
style={{ ...style, backgroundColor: color }}
77+
onClick={onClick}
78+
role="button"
79+
tabIndex={0}
80+
aria-label="close"
81+
>
82+
{icon}
83+
{children}
84+
{renderCloseIcon()}
85+
</span>
86+
);
87+
});
88+
89+
Tag.displayName = 'Tag';
90+
91+
export default Tag;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Tag > should apply custom className 1`] = `
4+
<span
5+
class="sqi-tag sqi-tag-size-md custom-class"
6+
>
7+
Tag
8+
</span>
9+
`;
10+
11+
exports[`Tag > should apply custom style 1`] = `
12+
<span
13+
class="sqi-tag sqi-tag-size-md"
14+
style="color: red;"
15+
>
16+
Tag
17+
</span>
18+
`;
19+
20+
exports[`Tag > should get size from context 1`] = `
21+
<span
22+
class="sqi-tag sqi-tag-size-lg"
23+
>
24+
Tag
25+
</span>
26+
`;
27+
28+
exports[`Tag > should handle click events 1`] = `
29+
<span
30+
class="sqi-tag sqi-tag-size-md"
31+
>
32+
Tag
33+
</span>
34+
`;
35+
36+
exports[`Tag > should not be closable by default 1`] = `
37+
<span
38+
class="sqi-tag sqi-tag-size-md"
39+
>
40+
Tag
41+
</span>
42+
`;
43+
44+
exports[`Tag > should not hide if close event is prevented 1`] = `
45+
<span
46+
class="sqi-tag sqi-tag-size-md"
47+
>
48+
Tag
49+
<span
50+
aria-label="close"
51+
class="sqi-icon sqi-tag-close-icon"
52+
role="img"
53+
>
54+
<svg
55+
aria-hidden="true"
56+
fill="none"
57+
focusable="false"
58+
height="1em"
59+
viewBox="0 0 24 24"
60+
width="1em"
61+
xmlns="http://www.w3.org/2000/svg"
62+
>
63+
<path
64+
d="M7.04996 5.63599L11.9997 10.5857L16.9494 5.63599L18.3637 7.0502L13.4139 11.9999L18.3637 16.9497L16.9494 18.3639L11.9997 13.4142L7.04996 18.3639L5.63574 16.9497L10.5855 11.9999L5.63574 7.0502L7.04996 5.63599Z"
65+
fill="currentColor"
66+
/>
67+
</svg>
68+
</span>
69+
</span>
70+
`;
71+
72+
exports[`Tag > should render close icon when closable is true 1`] = `
73+
<span
74+
class="sqi-tag sqi-tag-size-md"
75+
>
76+
Tag
77+
<span
78+
aria-label="close"
79+
class="sqi-icon sqi-tag-close-icon"
80+
role="img"
81+
>
82+
<svg
83+
aria-hidden="true"
84+
fill="none"
85+
focusable="false"
86+
height="1em"
87+
viewBox="0 0 24 24"
88+
width="1em"
89+
xmlns="http://www.w3.org/2000/svg"
90+
>
91+
<path
92+
d="M7.04996 5.63599L11.9997 10.5857L16.9494 5.63599L18.3637 7.0502L13.4139 11.9999L18.3637 16.9497L16.9494 18.3639L11.9997 13.4142L7.04996 18.3639L5.63574 16.9497L10.5855 11.9999L5.63574 7.0502L7.04996 5.63599Z"
93+
fill="currentColor"
94+
/>
95+
</svg>
96+
</span>
97+
</span>
98+
`;
99+
100+
exports[`Tag > should render correctly 1`] = `
101+
<span
102+
class="sqi-tag sqi-tag-size-md"
103+
>
104+
Tag Text
105+
</span>
106+
`;
107+
108+
exports[`Tag > should render custom close icon 1`] = `
109+
<span
110+
class="sqi-tag sqi-tag-size-md"
111+
>
112+
Tag
113+
<span
114+
class="sqi-tag-close-icon"
115+
data-testid="custom-close"
116+
>
117+
x
118+
</span>
119+
</span>
120+
`;
121+
122+
exports[`Tag > should render with border 1`] = `
123+
<span
124+
class="sqi-tag sqi-tag-size-md sqi-tag-bordered"
125+
>
126+
Tag
127+
</span>
128+
`;
129+
130+
exports[`Tag > should render with color 1`] = `
131+
<span
132+
class="sqi-tag sqi-tag-size-md sqi-tag-has-color"
133+
style="background-color: red;"
134+
>
135+
Tag
136+
</span>
137+
`;
138+
139+
exports[`Tag > should render with different sizes 1`] = `
140+
<span
141+
class="sqi-tag sqi-tag-size-md"
142+
>
143+
Tag
144+
</span>
145+
`;
146+
147+
exports[`Tag > should render with different sizes 2`] = `
148+
<span
149+
class="sqi-tag sqi-tag-size-lg"
150+
>
151+
Tag
152+
</span>
153+
`;
154+
155+
exports[`Tag > should render with icon 1`] = `
156+
<span
157+
class="sqi-tag sqi-tag-size-md"
158+
>
159+
<span
160+
data-testid="tag-icon"
161+
>
162+
icon
163+
</span>
164+
Tag
165+
</span>
166+
`;
167+
168+
exports[`Tag > should render with title 1`] = `
169+
<span
170+
class="sqi-tag sqi-tag-size-md"
171+
title="Tag Title"
172+
>
173+
Tag
174+
</span>
175+
`;

0 commit comments

Comments
 (0)