Skip to content

Commit 018eea1

Browse files
committed
feat: 新增avatar组件
2 parents b6085a6 + 0c5d996 commit 018eea1

18 files changed

Lines changed: 1002 additions & 0 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
"react-transition-group": "~4.4.1",
177177
"tdesign-icons-react": "~0.0.5",
178178
"tslib": "~2.2.0",
179+
"use-resize-observer": "^8.0.0",
179180
"uuid": "~8.3.2",
180181
"validator": "~13.7.0"
181182
}

site/site.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,13 @@ export default {
245245
name: 'Data',
246246
type: 'component', // 组件文档
247247
children: [
248+
{
249+
title: 'Avatar 头像',
250+
name: 'avatar',
251+
docType: 'data',
252+
path: '/react/components/avatar',
253+
component: () => import('tdesign-react/avatar/README.md'),
254+
},
248255
{
249256
title: 'Badge 徽标数',
250257
name: 'badge',

src/avatar/Avatar.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { forwardRef, useRef, useState, useEffect, useContext } from 'react';
2+
import useResizeObserver from 'use-resize-observer';
3+
import classNames from 'classnames';
4+
import useConfig from '../_util/useConfig';
5+
import useCommonClassName from '../_util/useCommonClassName';
6+
import composeRefs from '../_util/composeRefs';
7+
import { TdAvatarProps } from './type';
8+
import { StyledProps } from '../common';
9+
import AvatarContext from './AvatarContext';
10+
11+
export interface AvatarProps extends TdAvatarProps, StyledProps {
12+
children?: React.ReactNode;
13+
}
14+
const Avatar = forwardRef<HTMLElement, AvatarProps>((props, ref) => {
15+
const {
16+
alt,
17+
hideOnLoadFailed = false,
18+
icon,
19+
image,
20+
shape = 'circle',
21+
size: avatarSize = 'default',
22+
onError,
23+
children,
24+
style,
25+
className,
26+
...avatarProps
27+
} = props;
28+
const groupSize = useContext(AvatarContext);
29+
const { classPrefix } = useConfig();
30+
const [scale, setScale] = useState(1);
31+
const [isImgExist, setIsImgExist] = useState(true);
32+
const avatarRef = useRef<HTMLElement>(null);
33+
const avatarChildrenRef = useRef<HTMLElement>(null);
34+
const size = avatarSize === 'default' ? groupSize : avatarSize;
35+
const gap = 4;
36+
const handleScale = () => {
37+
if (!avatarChildrenRef.current || !avatarRef.current) {
38+
return;
39+
}
40+
const childrenWidth = avatarChildrenRef.current.offsetWidth;
41+
const avatarWidth = avatarRef.current.offsetWidth;
42+
43+
if (childrenWidth !== 0 && avatarWidth !== 0) {
44+
if (gap * 2 < avatarWidth) {
45+
setScale(avatarWidth - gap * 2 < childrenWidth ? (avatarWidth - gap * 2) / childrenWidth : 1);
46+
}
47+
}
48+
};
49+
const { ref: observerRef } = useResizeObserver<HTMLDivElement>({
50+
onResize: handleScale,
51+
});
52+
53+
const handleImgLoadError = () => {
54+
onError && onError();
55+
!hideOnLoadFailed && setIsImgExist(false);
56+
};
57+
58+
useEffect(() => {
59+
setIsImgExist(true);
60+
setScale(1);
61+
}, [props.image]);
62+
63+
useEffect(() => {
64+
handleScale();
65+
}, []);
66+
67+
const { SIZE } = useCommonClassName();
68+
const numSizeStyle: React.CSSProperties =
69+
size && !SIZE[size]
70+
? {
71+
width: size,
72+
height: size,
73+
fontSize: `${Number.parseInt(size, 10) / 2}px`,
74+
}
75+
: {};
76+
const imageStyle: React.CSSProperties =
77+
size && !SIZE[size]
78+
? {
79+
width: size,
80+
height: size,
81+
}
82+
: {};
83+
84+
const preClass = `${classPrefix}-avatar`;
85+
86+
const avatarClass = classNames(preClass, className, {
87+
[SIZE[size]]: !!SIZE[size],
88+
[`${preClass}--${shape}`]: !!shape,
89+
[`${preClass}-icon`]: !!icon,
90+
});
91+
92+
let content;
93+
if (image && isImgExist) {
94+
content = <img src={image} alt={alt} style={imageStyle} onError={handleImgLoadError} />;
95+
} else if (icon) {
96+
content = icon;
97+
} else {
98+
const childrenStyle: React.CSSProperties = {
99+
transform: `scale(${scale})`,
100+
};
101+
content = (
102+
<span ref={composeRefs(ref, avatarChildrenRef, observerRef) as any} style={childrenStyle}>
103+
{children}
104+
</span>
105+
);
106+
}
107+
return (
108+
<div
109+
ref={composeRefs(ref, avatarRef) as any}
110+
className={avatarClass}
111+
style={{ ...numSizeStyle, ...style }}
112+
{...avatarProps}
113+
>
114+
{content}
115+
</div>
116+
);
117+
});
118+
119+
Avatar.displayName = 'Avatar';
120+
121+
export default Avatar;

src/avatar/AvatarContext.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from 'react';
2+
3+
const AvatarContext = React.createContext('default');
4+
5+
export interface AvatarContextProps {
6+
size?: string;
7+
}
8+
9+
export const AvatarContextProvider: React.FC<AvatarContextProps> = ({ children, size }) => (
10+
<AvatarContext.Consumer>
11+
{(inputSize) => <AvatarContext.Provider value={size || inputSize}>{children}</AvatarContext.Provider>}
12+
</AvatarContext.Consumer>
13+
);
14+
15+
export default AvatarContext;

src/avatar/Group.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import Avatar from './Avatar';
4+
import Popup from '../popup/Popup';
5+
import useConfig from '../_util/useConfig';
6+
import { AvatarContextProvider } from './AvatarContext';
7+
import { TdAvatarGroupProps } from './type';
8+
import { StyledProps } from '../common';
9+
10+
export interface AvatarGroupProps extends TdAvatarGroupProps, StyledProps {
11+
children?: React.ReactNode;
12+
}
13+
const Group: React.FC<AvatarGroupProps> = (props) => {
14+
const { classPrefix } = useConfig();
15+
const preClass = `${classPrefix}-avatar`;
16+
const {
17+
className,
18+
cascading = 'right-up',
19+
collapseAvatar,
20+
max,
21+
placement,
22+
popupProps,
23+
size = 'medium',
24+
children,
25+
...avatarGroupProps
26+
} = props;
27+
28+
const childrenList = React.Children.toArray(children);
29+
let allChildrenList;
30+
if (childrenList.length > 0) {
31+
allChildrenList = childrenList.map((child: JSX.Element, index: number) =>
32+
React.cloneElement(child, { key: `avatar-group-item-${index}`, ...child.props }),
33+
);
34+
}
35+
const groupClass = classNames(`${preClass}-group`, className, {
36+
[`${preClass}--offset-right`]: cascading === 'right-up',
37+
[`${preClass}--offset-left`]: cascading === 'left-up',
38+
});
39+
const childrenCount = childrenList.length;
40+
if (max && childrenCount > max) {
41+
const showList = allChildrenList.slice(0, max);
42+
const hiddenList = allChildrenList.slice(max, childrenCount);
43+
const popupNum = `+${childrenCount - max}`;
44+
const popupMergeProps = { ...popupProps, placement };
45+
const popupNodes = popupProps ? (
46+
<Popup {...popupMergeProps}>
47+
{collapseAvatar ? <Avatar size={size}>{collapseAvatar}</Avatar> : <Avatar size={size}>{popupNum}</Avatar>}
48+
</Popup>
49+
) : (
50+
<Popup key="avatar-popup-key" placement={placement} content={hiddenList} trigger="hover" showArrow>
51+
{collapseAvatar ? <Avatar size={size}>{collapseAvatar}</Avatar> : <Avatar size={size}>{popupNum}</Avatar>}
52+
</Popup>
53+
);
54+
showList.push(popupNodes);
55+
return (
56+
<AvatarContextProvider size={size}>
57+
<div className={groupClass}>{showList}</div>
58+
</AvatarContextProvider>
59+
);
60+
}
61+
return (
62+
<AvatarContextProvider size={size}>
63+
<div className={groupClass} {...avatarGroupProps}>
64+
{allChildrenList}
65+
</div>
66+
</AvatarContextProvider>
67+
);
68+
};
69+
export default Group;

0 commit comments

Comments
 (0)