From 1e15f9d66161a8029ccaa8459d4950637bd9ef12 Mon Sep 17 00:00:00 2001 From: Rainbow <1256734885@qq.com> Date: Mon, 5 Jan 2026 09:18:22 +0800 Subject: [PATCH] feat(components): [avatar-group] new component (#23211) * feat(components): [avatar-group] new component * feat: update * feat: update * feat: update * docs: add an example * refactor: remove unnecessary computed * refactor: extract props to avatar-group-props * feat: update * fix: handle the rendering issue of hiddenAvatars * style: use inline-flex instead of flex * test: update test case * docs: update the default values of size and shape * refactor: apply rabbit comment * refactor: apply rabbit comment * chore: update description * style: use getCssVar * style: css variables take effect on collapse-avatars --- docs/en-US/component/avatar.md | 40 ++++++++-- docs/examples/avatar/group.vue | 51 ++++++++++++ packages/components/avatar-group/style/css.ts | 3 + .../components/avatar-group/style/index.ts | 3 + .../avatar/__tests__/avatar.test.tsx | 59 ++++++++++++++ packages/components/avatar/index.ts | 15 +++- .../avatar/src/avatar-group-props.ts | 70 +++++++++++++++++ .../components/avatar/src/avatar-group.tsx | 77 +++++++++++++++++++ packages/components/avatar/src/avatar.ts | 2 - packages/components/avatar/src/avatar.vue | 21 +++-- packages/components/avatar/src/constants.ts | 11 +++ packages/components/avatar/src/instance.ts | 2 + packages/element-plus/component.ts | 3 +- packages/theme-chalk/src/avatar-group.scss | 25 ++++++ packages/theme-chalk/src/common/var.scss | 9 +++ packages/theme-chalk/src/index.scss | 1 + typings/global.d.ts | 1 + 17 files changed, 374 insertions(+), 19 deletions(-) create mode 100644 docs/examples/avatar/group.vue create mode 100644 packages/components/avatar-group/style/css.ts create mode 100644 packages/components/avatar-group/style/index.ts create mode 100644 packages/components/avatar/src/avatar-group-props.ts create mode 100644 packages/components/avatar/src/avatar-group.tsx create mode 100644 packages/components/avatar/src/constants.ts create mode 100644 packages/theme-chalk/src/avatar-group.scss diff --git a/docs/en-US/component/avatar.md b/docs/en-US/component/avatar.md index cf364593ed..7c739deedf 100644 --- a/docs/en-US/component/avatar.md +++ b/docs/en-US/component/avatar.md @@ -47,28 +47,56 @@ avatar/fit ::: -## API +## Avatar Group ^(2.13.1) -### Attributes +Displayed as a avatar group. + +:::demo Use tag `` to group your avatars. + +avatar/group + +::: + +## Avatar API + +### Avatar Attributes | Name | Description | Type | Default | | ------- | --------------------------------------------------------- | ----------------------------------------------------------------- | ------- | | icon | representation type to icon, more info on icon component. | ^[string] / ^[Component] | — | -| size | avatar size. | ^[number] / ^[enum]`'large' \| 'default' \| 'small'` | default | -| shape | avatar shape. | ^[enum]`'circle' \| 'square'` | circle | +| size | avatar size. | ^[number] / ^[enum]`'large' \| 'default' \| 'small'` | — | +| shape | avatar shape. | ^[enum]`'circle' \| 'square'` | — | | src | the source of the image for an image avatar. | `string` | — | | src-set | native attribute `srcset` of image avatar. | `string` | — | | alt | native attribute `alt` of image avatar. | `string` | — | | fit | set how the image fit its container for an image avatar. | ^[enum]`'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down'` | cover | -### Events +### Avatar Events | Name | Description | Type | | ----- | ------------------------------ | ------------------------------- | | error | trigger when image load error. | ^[Function]`(e: Event) => void` | -### Slots +### Avatar Slots | Name | Description | | ------- | ------------------------- | | default | customize avatar content. | + +## AvatarGroup API ^(2.13.1) + +### AvatarGroup Attributes + +| Name | Description | Type | Default | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| size | control the size of avatars in this avatar-group | ^[number] / ^[enum]`'large' \| 'default' \| 'small'` | — | +| shape | control the shape of avatars in this avatar-group | ^[enum]`'circle' \| 'square'` | — | +| collapse-avatars | whether to collapse avatars | ^[boolean] | false | +| collapse-avatars-tooltip | whether show all collapsed avatars when mouse hover text of the collapse-avatar. To use this, `collapse-avatars` must be true | ^[boolean] | false | +| max-collapse-avatars | the max avatars number to be shown. To use this, `collapse-avatars` must be true | ^[number] | 1 | +| effect | tooltip theme, built-in theme: `dark` / `light` | ^[enum]`'dark' \| 'light'` / ^[string] | light | +| placement | placement of tooltip | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | top | +| popper-class | custom class name for tooltip | ^[string] | '' | +| popper-style | custom style for tooltip | ^[string] / ^[object] | — | +| collapse-class | custom class name for the collapse-avatar | ^[string] | '' | +| collapse-style | custom style for the collapse-avatar | ^[string] / ^[object] | — | diff --git a/docs/examples/avatar/group.vue b/docs/examples/avatar/group.vue new file mode 100644 index 0000000000..f18a7f33e5 --- /dev/null +++ b/docs/examples/avatar/group.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/components/avatar-group/style/css.ts b/packages/components/avatar-group/style/css.ts new file mode 100644 index 0000000000..07f35a0667 --- /dev/null +++ b/packages/components/avatar-group/style/css.ts @@ -0,0 +1,3 @@ +import '@element-plus/components/base/style/css' +import '@element-plus/theme-chalk/el-avatar-group.css' +import '@element-plus/components/tooltip/style/css' diff --git a/packages/components/avatar-group/style/index.ts b/packages/components/avatar-group/style/index.ts new file mode 100644 index 0000000000..bae21f4790 --- /dev/null +++ b/packages/components/avatar-group/style/index.ts @@ -0,0 +1,3 @@ +import '@element-plus/components/base/style' +import '@element-plus/theme-chalk/src/avatar-group.scss' +import '@element-plus/components/tooltip/style' diff --git a/packages/components/avatar/__tests__/avatar.test.tsx b/packages/components/avatar/__tests__/avatar.test.tsx index d0b1601de6..9b02a277e7 100644 --- a/packages/components/avatar/__tests__/avatar.test.tsx +++ b/packages/components/avatar/__tests__/avatar.test.tsx @@ -9,6 +9,7 @@ import { } from '@element-plus/test-utils/mock' import { stableLoad } from '@element-plus/test-utils/stable-load' import Avatar from '../src/avatar.vue' +import AvatarGroup from '../src/avatar-group' describe('Avatar.vue', () => { mockImageEvent() @@ -96,3 +97,61 @@ describe('Avatar.vue', () => { expect(wrapper.find('img').exists()).toBe(true) }) }) + +describe('Avatar Group', () => { + test('render test', () => { + const wrapper = mount( + + + + + + + + ) + + expect(wrapper.findAll('.el-avatar').length).toBe(5) + expect(wrapper.findAll('.el-avatar--small').length).toBe(3) + expect(wrapper.findAll('.el-avatar--large').length).toBe(2) + expect(wrapper.findAll('.el-avatar--circle').length).toBe(2) + expect(wrapper.findAll('.el-avatar--square').length).toBe(3) + }) + + test('collapse-class & collapse-style', () => { + const collapseStyle = 'background-color: red;' + const wrapper = mount( + + + + + ) + + const collapseAvatar = wrapper.findAll('.el-avatar')[1] + expect(collapseAvatar.text()).toBe('+ 1') + expect(collapseAvatar.classes()).toContain('collapse-avatar') + expect(collapseAvatar.attributes('style')).toContain( + 'background-color: red;' + ) + }) + + test('collapse-avatars-tooltip', async () => { + const wrapper = mount( + + + + + ) + + const collapseAvatar = wrapper.findAll('.el-avatar')[1] + await collapseAvatar.trigger('mouseenter') + await nextTick() + + const tooltip = wrapper.findComponent({ name: 'ElTooltip' }) + expect(tooltip.exists()).toBe(true) + expect(tooltip.html()).toContain('el-avatar') + }) +}) diff --git a/packages/components/avatar/index.ts b/packages/components/avatar/index.ts index e24e05eea5..7488882e7d 100644 --- a/packages/components/avatar/index.ts +++ b/packages/components/avatar/index.ts @@ -1,10 +1,19 @@ -import { withInstall } from '@element-plus/utils' +import { withInstall, withNoopInstall } from '@element-plus/utils' import Avatar from './src/avatar.vue' +import AvatarGroup from './src/avatar-group' import type { SFCWithInstall } from '@element-plus/utils' -export const ElAvatar: SFCWithInstall = withInstall(Avatar) +export const ElAvatar: SFCWithInstall & { + AvatarGroup: typeof AvatarGroup +} = withInstall(Avatar, { + AvatarGroup, +}) +export const ElAvatarGroup: SFCWithInstall = + withNoopInstall(AvatarGroup) export default ElAvatar export * from './src/avatar' -export type { AvatarInstance } from './src/instance' +export * from './src/constants' +export * from './src/avatar-group-props' +export type { AvatarInstance, AvatarGroupInstance } from './src/instance' diff --git a/packages/components/avatar/src/avatar-group-props.ts b/packages/components/avatar/src/avatar-group-props.ts new file mode 100644 index 0000000000..3463461069 --- /dev/null +++ b/packages/components/avatar/src/avatar-group-props.ts @@ -0,0 +1,70 @@ +import { placements } from '@popperjs/core' +import { useTooltipContentProps } from '@element-plus/components/tooltip' +import { buildProps, definePropType } from '@element-plus/utils' +import { avatarProps } from './avatar' + +import type { ExtractPropTypes, ExtractPublicPropTypes, StyleValue } from 'vue' +import type { Placement, PopperEffect } from '@element-plus/components/popper' + +export const avatarGroupProps = buildProps({ + /** + * @description control the size of avatars in this avatar-group + */ + size: avatarProps.size, + /** + * @description control the shape of avatars in this avatar-group + */ + shape: avatarProps.shape, + /** + * @description whether to collapse avatars + */ + collapseAvatars: Boolean, + /** + * @description whether show all collapsed avatars when mouse hover text of the collapse-avatar. To use this, `collapse-avatars` must be true + */ + collapseAvatarsTooltip: Boolean, + /** + * @description the max avatars number to be shown. To use this, `collapse-avatars` must be true + */ + maxCollapseAvatars: { + type: Number, + default: 1, + }, + /** + * @description tooltip theme, built-in theme: `dark` / `light` + */ + effect: { + type: definePropType(String), + default: 'light', + }, + /** + * @description placement of tooltip + */ + placement: { + type: definePropType(String), + values: placements, + default: 'top', + }, + /** + * @description custom class name for tooltip + */ + popperClass: useTooltipContentProps.popperClass, + /** + * @description custom style for tooltip + */ + popperStyle: useTooltipContentProps.popperStyle, + /** + * @description custom class name for the collapse-avatar + */ + collapseClass: String, + /** + * @description custom style for the collapse-avatar + */ + collapseStyle: { + type: definePropType([String, Array, Object]), + }, +} as const) +export type AvatarGroupProps = ExtractPropTypes +export type AvatarGroupPropsPublic = ExtractPublicPropTypes< + typeof avatarGroupProps +> diff --git a/packages/components/avatar/src/avatar-group.tsx b/packages/components/avatar/src/avatar-group.tsx new file mode 100644 index 0000000000..40bc53aaf7 --- /dev/null +++ b/packages/components/avatar/src/avatar-group.tsx @@ -0,0 +1,77 @@ +import { + cloneVNode, + defineComponent, + isVNode, + provide, + reactive, + toRef, +} from 'vue' +import { flattedChildren } from '@element-plus/utils' +import ElTooltip from '@element-plus/components/tooltip' +import { useNamespace } from '@element-plus/hooks' +import ElAvatar from './avatar.vue' +import { avatarGroupContextKey } from './constants' +import { avatarGroupProps } from './avatar-group-props' + +export default defineComponent({ + name: 'ElAvatarGroup', + props: avatarGroupProps, + setup(props, { slots }) { + const ns = useNamespace('avatar-group') + + provide( + avatarGroupContextKey, + reactive({ + size: toRef(props, 'size'), + shape: toRef(props, 'shape'), + }) + ) + + return () => { + const avatars = flattedChildren(slots.default?.() ?? []) + let visibleAvatars = avatars + + const showCollapseAvatar = + props.collapseAvatars && avatars.length > props.maxCollapseAvatars + + if (showCollapseAvatar) { + visibleAvatars = avatars.slice(0, props.maxCollapseAvatars) + const hiddenAvatars = avatars.slice(props.maxCollapseAvatars) + + visibleAvatars.push( + + {{ + default: () => ( + + + {hiddenAvatars.length} + + ), + content: () => ( +
+ {hiddenAvatars.map((node, idx) => + isVNode(node) + ? cloneVNode(node, { key: node.key ?? idx }) + : node + )} +
+ ), + }} +
+ ) + } + + return
{visibleAvatars}
+ } + }, +}) diff --git a/packages/components/avatar/src/avatar.ts b/packages/components/avatar/src/avatar.ts index 485434fc8f..20502c8e61 100644 --- a/packages/components/avatar/src/avatar.ts +++ b/packages/components/avatar/src/avatar.ts @@ -19,7 +19,6 @@ export const avatarProps = buildProps({ size: { type: [Number, String], values: componentSizes, - default: '', validator: (val: unknown): val is number => isNumber(val), }, /** @@ -28,7 +27,6 @@ export const avatarProps = buildProps({ shape: { type: String, values: ['circle', 'square'], - default: 'circle', }, /** * @description representation type to icon, more info on icon component. diff --git a/packages/components/avatar/src/avatar.vue b/packages/components/avatar/src/avatar.vue index 80d15f4f00..392538b7f2 100644 --- a/packages/components/avatar/src/avatar.vue +++ b/packages/components/avatar/src/avatar.vue @@ -16,11 +16,12 @@