Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(avatar): 组件对齐 mobile-vue #541

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export default {
{
title: 'Avatar 头像',
name: 'avatar',
component: () => import('tdesign-mobile-react/avatar/_example/index.jsx'),
component: () => import('tdesign-mobile-react/avatar/_example/index.tsx'),
},
{
title: 'Indexes 索引',
Expand Down
146 changes: 71 additions & 75 deletions src/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,85 @@
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 {
className,
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,
},
);
const containerClassName = cls(`${rootClassName}__wrapper`, className);

// 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={containerClassName}>
<div className={`${rootClassName}__badge`}>
<Badge {...badgeProps}>
<div className={avatarClasses} style={customSize}>
{renderAvatar()}
</div>
</Badge>
</div>
);
},
{
Group: AvatarGroup,
},
);

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

export default Avatar;
99 changes: 46 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,55 @@ 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
key="avatar-group-extension"
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>;
}
54 changes: 0 additions & 54 deletions src/avatar/_example/action.jsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/avatar/_example/action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Avatar, AvatarGroup } from 'tdesign-mobile-react';
import { UserAddIcon } from 'tdesign-icons-react';

export default function ActionAvatar() {
const imageList = [
'https://tdesign.gtimg.com/mobile/demos/avatar1.png',
'https://tdesign.gtimg.com/mobile/demos/avatar2.png',
'https://tdesign.gtimg.com/mobile/demos/avatar3.png',
'https://tdesign.gtimg.com/mobile/demos/avatar4.png',
'https://tdesign.gtimg.com/mobile/demos/avatar5.png',
'https://tdesign.gtimg.com/mobile/demos/avatar1.png',
'https://tdesign.gtimg.com/mobile/demos/avatar2.png',
'https://tdesign.gtimg.com/mobile/demos/avatar3.png',
'https://tdesign.gtimg.com/mobile/demos/avatar4.png',
'https://tdesign.gtimg.com/mobile/demos/avatar5.png',
];
return (
<div className="avatar-group-demo">
<AvatarGroup max={5} collapseAvatar={<UserAddIcon style={{ fontSize: '24px' }} />}>
{imageList.map((url, index) => (
<Avatar key={`action-${index}`} shape="circle" image={url} />
))}
</AvatarGroup>
</div>
);
}
Loading
Loading