Skip to content

Commit

Permalink
feat(avatar): update avatar style to v2, alignment vue mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
slatejack committed Oct 18, 2024
1 parent bdca4ab commit c784636
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 174 deletions.
144 changes: 69 additions & 75 deletions src/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,83 @@
import React, { useContext, useMemo, Ref, useState } from 'react';
import type { FC } from 'react';
import React, { useContext } from 'react';
import cls from 'classnames';
import type { TdAvatarProps } from './type';
import Badge from '../badge/index';
import { StyledProps } from '../common';
import { ConfigContext } from '../config-provider';
import useSizeHook from './hooks/useSizeHooks';
import AvatarGroup from './AvatarGroup';
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
import Image from 'tdesign-mobile-react/image';
import Badge from 'tdesign-mobile-react/badge';
import { AvatarGroupContext } from './AvatarGroupContext';
import { isValidSize } from '../_common/js/avatar/utils';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';
import { avatarDefaultProps } from './defaultProps';
import type { TdAvatarProps } from './type';
import type { StyledProps } from '../common';

export interface AvatarProps extends TdAvatarProps, StyledProps {
children?: React.ReactNode;
}
export interface AvatarProps extends TdAvatarProps, StyledProps {}

const Avatar = forwardRefWithStatics(
(props: AvatarProps, ref: Ref<HTMLDivElement>) => {
const {
size = '',
shape = 'circle',
icon,
children,
hideOnLoadFailed = false,
image = '',
badgeProps,
alt = '',
onError,
className,
...restProps
} = props;
const { size: avatarGroupSize } = useContext(AvatarGroupContext) || {};
const sizeCls = useSizeHook(size || avatarGroupSize);
const [sizeValue] = useState(size || avatarGroupSize);
const { classPrefix } = useContext(ConfigContext);
const baseAvatarCls = `${classPrefix}-avatar`;
const Avatar: FC<AvatarProps> = (props) => {
const {
size = '',
shape = 'circle',
icon,
children,
hideOnLoadFailed = false,
image = '',
badgeProps,
alt = '',
imageProps,
onError,
} = useDefaultProps(props, avatarDefaultProps);
const avatarGroupProps = useContext(AvatarGroupContext) || {};
const rootClassName = usePrefixClass('avatar');

const isIconOnly = icon && !children;
const { size: avatarGroupSize, shape: avatarGroupShape } = avatarGroupProps;
const hasAvatarGroupProps = Object.keys(avatarGroupProps).length > 0;
const shapeValue = shape || avatarGroupShape || 'circle';
const sizeValue = size || avatarGroupSize;
const isCustomSize = !isValidSize(sizeValue);

const avatarCls = cls(
baseAvatarCls,
{
[sizeCls]: true,
[`${baseAvatarCls}--${shape}`]: shape,
},
className,
);
const avatarClasses = cls(
rootClassName,
`${rootClassName}-${isCustomSize ? 'medium' : sizeValue}`,
`${rootClassName}-${shapeValue}`,
{
[`${rootClassName}--border ${rootClassName}--border-${isCustomSize ? 'medium' : sizeValue}`]: hasAvatarGroupProps,
},
);

// size 没有命中原有 size 规则且 size 仍有值, 推断为 size 值
const customSize = useMemo(() => {
if (sizeCls === '' && sizeValue) {
return {
width: sizeValue,
height: sizeValue,
};
const customSize = isCustomSize
? {
height: sizeValue,
width: sizeValue,
'font-size': `${(Number.parseInt(sizeValue, 10) / 8) * 3 + 2}px`,
}
return {};
}, [sizeCls, sizeValue]);
: {};

const iconCls = `${baseAvatarCls}__icon`;
const badgeCls = `${baseAvatarCls}__badge`;
const innerCls = `${baseAvatarCls}__inner`;
const handleImgLoaderError = (context: any) => {
onError?.(context);
};

const renderIcon = <div className={iconCls}>{icon}</div>;
const renderImage = <img style={customSize} alt={alt} src={image} onError={onError}></img>;
const renderContent = <>{children}</>;
const renderBadge = <Badge {...badgeProps}></Badge>;
const renderAvatar = () => {
if (image && !hideOnLoadFailed) {
return <Image src={image} alt={alt} {...imageProps} onError={handleImgLoaderError} />;
}
if (icon) {
return <div className={`${rootClassName}__icon`}>{icon}</div>;
}
return parseTNode(children);
};

const isShowImage = image && !hideOnLoadFailed;
const isShowBadge = !!badgeProps;

return (
<div ref={ref} className={avatarCls} style={customSize} {...restProps}>
<div className={innerCls}>
{isShowImage && renderImage}
{!isShowImage && isIconOnly && renderIcon}
{!isShowImage && !isIconOnly && renderContent}
</div>
{isShowBadge && <div className={badgeCls}>{renderBadge}</div>}
return (
<div className={`${rootClassName}__wrapper`}>
<div className={`${rootClassName}__badge`}>
<Badge {...badgeProps}>
<div className={avatarClasses} style={customSize}>
{renderAvatar()}
</div>
</Badge>
</div>
);
},
{
Group: AvatarGroup,
},
);

Avatar.displayName = 'Avatar';
</div>
);
};

export default Avatar;
95 changes: 42 additions & 53 deletions src/avatar/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React, { forwardRef, Ref, useContext, useEffect, useMemo, useState } from 'react';
import type { MouseEvent } from 'react';
import React from 'react';
import cls from 'classnames';
import { ShapeEnum, TdAvatarGroupProps } from './type';
import { StyledProps } from '../common';
import { ConfigContext } from '../config-provider';
import Avatar from './Avatar';
import { AvatarGroupContextProvider } from './AvatarGroupContext';
import parseTNode from '../_util/parseTNode';
import { isValidSize } from '../_common/js/avatar/utils';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';
import { avatarGroupDefaultProps } from './defaultProps';
import type { TdAvatarGroupProps } from './type';
import type { StyledProps } from '../common';

export interface AvatarGroupProps extends TdAvatarGroupProps, StyledProps {
children?: React.ReactNode;
Expand All @@ -14,67 +19,51 @@ function getValidChildren(children: React.ReactNode) {
return React.Children.toArray(children).filter((child) => React.isValidElement(child)) as React.ReactElement[];
}

const AvatarGroup = forwardRef((props: AvatarGroupProps, ref: Ref<HTMLDivElement>) => {
const { cascading, children, max, collapseAvatar, size, className, ...restProps } = props;
const { classPrefix } = useContext(ConfigContext);
const [isShowEllipsisContent, setIsShowEllipsisContent] = useState(false);
const [lastOneShape, setLastOneShape] = useState<ShapeEnum>('circle');

const baseAvatarGroupCls = `${classPrefix}-avatar-group`;

const avatarGroupCls = cls(
baseAvatarGroupCls,
{
[`${classPrefix}-avatar--offset-right`]: cascading === 'right-up',
[`${classPrefix}-avatar--offset-left`]: cascading === 'left-up',
},
className,
const AvatarGroup = (props: AvatarGroupProps) => {
const { cascading, children, size, shape, max, collapseAvatar, onCollapsedItemClick } = useDefaultProps(
props,
avatarGroupDefaultProps,
);
const rootClassName = usePrefixClass('avatar-group');

const validChildren = getValidChildren(children);
const childrenCount = validChildren.length;
const childrenWithinMax = max ? validChildren.slice(0, max) : validChildren;
const direction = cascading ? cascading.split('-')[0] : 'right';
const isCustomSize = !isValidSize(size);

const renderCollapseAvatar = useMemo(() => {
const popupNum = `+${childrenCount - max}`;
return collapseAvatar || popupNum;
}, [collapseAvatar, max, childrenCount]);

const ellipsisSize = useMemo(
() => childrenWithinMax[childrenWithinMax.length - 1]?.props.size || props.size,
[childrenWithinMax, props.size],
const avatarGroupClasses = cls(
rootClassName,
`${rootClassName}-offset-${direction}`,
`${rootClassName}-offset-${direction}-${isCustomSize ? 'medium' : size}`,
);

useEffect(() => {
if (max && childrenCount > max) {
setIsShowEllipsisContent(true);
} else {
setIsShowEllipsisContent(false);
}
}, [max, childrenCount]);
const handleCollapsedItemClick = (e: MouseEvent<HTMLSpanElement>) => {
onCollapsedItemClick?.({ e });
};

useEffect(() => {
if (
childrenWithinMax.length > 0 &&
childrenWithinMax?.[childrenWithinMax.length - 1]?.props?.shape !== lastOneShape
) {
setLastOneShape(childrenWithinMax[childrenWithinMax.length - 1].props.shape);
const renderAvatar = () => {
const validChildren = getValidChildren(children);
if (validChildren.length <= max) {
return validChildren;
}
}, [childrenWithinMax, lastOneShape]);
const showAvatarList = validChildren.slice(0, max);
const renderCollapseAvatar = () => parseTNode(collapseAvatar);
showAvatarList.push(
<div className={`${rootClassName}__collapse--default`} onClick={handleCollapsedItemClick}>
<Avatar size={showAvatarList[0].props.size || size} shape={shape}>
{renderCollapseAvatar() || `+${validChildren.length - max}`}
</Avatar>
</div>,
);
return showAvatarList;
};

return (
<div className={avatarGroupCls} ref={ref} {...restProps}>
<AvatarGroupContextProvider size={size}>
{childrenWithinMax}
{isShowEllipsisContent ? (
<Avatar shape={lastOneShape} size={ellipsisSize}>
{renderCollapseAvatar}
</Avatar>
) : null}
<div className={avatarGroupClasses}>
<AvatarGroupContextProvider size={size} shape={shape}>
{renderAvatar()}
</AvatarGroupContextProvider>
</div>
);
});
};

AvatarGroup.displayName = 'AvatarGroup';

Expand Down
9 changes: 5 additions & 4 deletions src/avatar/AvatarGroupContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import type { AvatarGroupProps } from './AvatarGroup';

export const AvatarGroupContext = React.createContext(null);

export function AvatarGroupContextProvider(props: Pick<AvatarGroupProps, 'size' | 'children'>) {
const memoSize = useMemo(
export function AvatarGroupContextProvider(props: Pick<AvatarGroupProps, 'size' | 'children' | 'shape'>) {
const memoInfo = useMemo(
() => ({
size: props.size,
shape: props.shape,
}),
[props.size],
[props.size, props.shape],
);
return <AvatarGroupContext.Provider value={memoSize}>{props.children}</AvatarGroupContext.Provider>;
return <AvatarGroupContext.Provider value={memoInfo}>{props.children}</AvatarGroupContext.Provider>;
}
17 changes: 17 additions & 0 deletions src/avatar/avatar-group.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
:: BASE_DOC ::

## API


### AvatarGroup Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
cascading | String | 'right-up' | multiple images cascading。options: left-up/right-up。Typescript:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/avatar/type.ts) | N
collapseAvatar | TNode | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
max | Number | - | \- | N
shape | String | - | shape。options: circle/round。Typescript:`ShapeEnum`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
size | String | - | size | N
onCollapsedItemClick | Function | | Typescript:`(context: { e: MouseEvent }) => void`<br/> | N
17 changes: 17 additions & 0 deletions src/avatar/avatar-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
:: BASE_DOC ::

## API


### AvatarGroup Props

名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
className | String | - | 类名 | N
style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
cascading | String | 'right-up' | 图片之间的层叠关系,可选值:左侧图片在上和右侧图片在上。可选项:left-up/right-up。TS 类型:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/avatar/type.ts) | N
collapseAvatar | TNode | - | 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5``...`, `更多`。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
max | Number | - | 能够同时显示的最多头像数量 | N
shape | String | - | 形状。优先级低于 Avatar.shape。可选项:circle/round。TS 类型:`ShapeEnum`[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
size | String | - | 尺寸,示例值:small/medium/large/24px/38px 等。优先级低于 Avatar.size | N
onCollapsedItemClick | Function | | TS 类型:`(context: { e: MouseEvent }) => void`<br/>点击头像折叠元素触发 | N
34 changes: 34 additions & 0 deletions src/avatar/avatar.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
:: BASE_DOC ::

## API

### Avatar Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
alt | String | - | show it when url is not valid | N
badgeProps | Object | - | Typescript:`BadgeProps`[Badge API Documents](./badge?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/avatar/type.ts) | N
children | TNode | - | children, same as `content`。Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
hideOnLoadFailed | Boolean | false | hide image when loading image failed | N
icon | TElement | - | use icon to fill。Typescript:`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
image | String | - | images url | N
imageProps | Object | - | Typescript:`ImageProps`[Image API Documents](./image?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/avatar/type.ts) | N
shape | String | - | shape。options: circle/round。Typescript:`ShapeEnum`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
size | String | - | size | N
onError | Function | | Typescript:`(context: { e: Event }) => void`<br/>trigger on image load failed | N


### AvatarGroup Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
cascading | String | 'right-up' | multiple images cascading。options: left-up/right-up。Typescript:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/avatar/type.ts) | N
collapseAvatar | TNode | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
max | Number | - | \- | N
shape | String | - | shape。options: circle/round。Typescript:`ShapeEnum`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
size | String | - | size | N
onCollapsedItemClick | Function | | Typescript:`(context: { e: MouseEvent }) => void`<br/> | N
Loading

0 comments on commit c784636

Please sign in to comment.