diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts
index 57636ac72..cf9438242 100644
--- a/packages/components/config/src/defaultConfig.ts
+++ b/packages/components/config/src/defaultConfig.ts
@@ -271,6 +271,10 @@ export const defaultConfig: GlobalConfig = {
},
},
tag: {},
+ tagGroup: {
+ gap: 'sm',
+ wrap: true,
+ },
textarea: {
autoRows: false,
clearable: false,
diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts
index 571c66e62..7a8b2dfe5 100644
--- a/packages/components/config/src/types.ts
+++ b/packages/components/config/src/types.ts
@@ -80,6 +80,7 @@ export interface GlobalConfig {
stepper: StepperConfig
table: TableConfig
tag: TagConfig
+ tagGroup: TagGroupConfig
textarea: TextareaConfig
timePicker: TimePickerConfig
timeRangePicker: TimeRangePickerConfig
@@ -417,6 +418,11 @@ export interface TagConfig {
shape?: TagShape
}
+export interface TagGroupConfig {
+ gap: number | [number | string, number | string] | SpaceSize
+ wrap: boolean
+}
+
export interface TextareaConfig {
autoRows: boolean | TextareaAutoRows
clearable: boolean
diff --git a/packages/components/index.ts b/packages/components/index.ts
index 1b4f4ae44..97027ae9e 100644
--- a/packages/components/index.ts
+++ b/packages/components/index.ts
@@ -55,7 +55,7 @@ import { IxStepper, IxStepperItem } from '@idux/components/stepper'
import { IxSwitch } from '@idux/components/switch'
import { IxTable, IxTableColumn } from '@idux/components/table'
import { IxTab, IxTabs } from '@idux/components/tabs'
-import { IxTag } from '@idux/components/tag'
+import { IxTag, IxTagGroup } from '@idux/components/tag'
import { IxTextarea } from '@idux/components/textarea'
import { IxTimePicker, IxTimeRangePicker } from '@idux/components/time-picker'
import { IxTimeline, IxTimelineItem } from '@idux/components/timeline'
@@ -146,6 +146,7 @@ const components = [
IxTab,
IxTabs,
IxTag,
+ IxTagGroup,
IxTextarea,
IxTimePicker,
IxTimeRangePicker,
diff --git a/packages/components/style/variable/prefix.less b/packages/components/style/variable/prefix.less
index 8f22e5a0b..fc68e3b69 100644
--- a/packages/components/style/variable/prefix.less
+++ b/packages/components/style/variable/prefix.less
@@ -7,6 +7,7 @@
@header-prefix: ~'@{idux-prefix}-header';
@icon-prefix: ~'@{idux-prefix}-icon';
@tag-prefix: ~'@{idux-prefix}-tag';
+@tag-group-prefix: ~'@{idux-prefix}-tag-group';
@typography-prefix: ~'@{idux-prefix}-typography';
// Layout
diff --git a/packages/components/table/__tests__/__snapshots__/table.spec.ts.snap b/packages/components/table/__tests__/__snapshots__/table.spec.ts.snap
index 0dd0bd760..4c2884e8f 100644
--- a/packages/components/table/__tests__/__snapshots__/table.spec.ts.snap
+++ b/packages/components/table/__tests__/__snapshots__/table.spec.ts.snap
@@ -56,7 +56,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 0
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -82,7 +84,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 1
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -108,7 +112,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 2
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -132,7 +138,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 3
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -158,7 +166,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 4
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -184,7 +194,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 5
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -208,7 +220,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 6
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -234,7 +248,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 7
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -260,7 +276,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 8
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
@@ -284,7 +302,9 @@ exports[`Table basic work render work 1`] = `
London Park no. 9
|
- NICEDEVELOPER
+ NICE
+ DEVELOPER
+
|
diff --git a/packages/components/tag/__tests__/__snapshots__/tag.spec.ts.snap b/packages/components/tag/__tests__/__snapshots__/tag.spec.ts.snap
index e0ed3c1d3..3b3927a1b 100644
--- a/packages/components/tag/__tests__/__snapshots__/tag.spec.ts.snap
+++ b/packages/components/tag/__tests__/__snapshots__/tag.spec.ts.snap
@@ -1,3 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Tag render work 1`] = `"test tag"`;
+exports[`Tag render work 1`] = `
+"test tag
+"
+`;
diff --git a/packages/components/tag/__tests__/__snapshots__/tagGroup.spec.ts.snap b/packages/components/tag/__tests__/__snapshots__/tagGroup.spec.ts.snap
new file mode 100644
index 000000000..28c4ec141
--- /dev/null
+++ b/packages/components/tag/__tests__/__snapshots__/tagGroup.spec.ts.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TagGroup render work 1`] = `
+""
+`;
diff --git a/packages/components/tag/__tests__/tagGroup.spec.ts b/packages/components/tag/__tests__/tagGroup.spec.ts
new file mode 100644
index 000000000..0c6639bc2
--- /dev/null
+++ b/packages/components/tag/__tests__/tagGroup.spec.ts
@@ -0,0 +1,105 @@
+import { type MountingOptions, mount } from '@vue/test-utils'
+import { h, ref } from 'vue'
+
+import { renderWork } from '@tests'
+
+import { IxIcon } from '@idux/components/icon'
+
+import IxTagGroup from '../src/TagGroup'
+import { TagGroupProps } from '../src/types'
+
+describe('TagGroup', () => {
+ const TagGroupMount = (options?: MountingOptions>) => mount(IxTagGroup, { ...options })
+ const dataSource = [
+ { key: 'a', text: 'A' },
+ { key: 'b', text: 'B' },
+ ]
+
+ renderWork(IxTagGroup, { props: { dataSource } })
+
+ test('prop clickable work', async () => {
+ const clickFn = jest.fn()
+ const wrapper = TagGroupMount({ props: { dataSource, clickable: false, onClick: clickFn } })
+ await wrapper.find('.ix-tag').trigger('click')
+ expect(clickFn).toBeCalledTimes(0)
+ expect(wrapper.find('.ix-tag-group-clickable').exists()).toBe(false)
+
+ await wrapper.setProps({ clickable: true })
+ await wrapper.find('.ix-tag').trigger('click')
+ expect(clickFn).toBeCalledTimes(1)
+ expect(clickFn.mock.calls[0][0]).toBe('a')
+ expect(wrapper.find('.ix-tag-group-clickable').exists()).toBe(true)
+ })
+
+ test('prop activeKeys work', async () => {
+ const activeKeys = ref([])
+ const wrapper = mount({
+ components: { IxTagGroup },
+ template: `
+ `,
+ setup() {
+ return { activeKeys, dataSource }
+ },
+ })
+
+ await wrapper.find('.ix-tag').trigger('click')
+ expect(activeKeys.value.length).toBe(1)
+ expect(activeKeys.value[0]).toBe('a')
+
+ await wrapper.find('.ix-tag').trigger('click')
+ expect(activeKeys.value.length).toBe(0)
+ })
+
+ test('prop closable work', async () => {
+ const closeFn = jest.fn()
+ const wrapper = TagGroupMount({ props: { dataSource, onClose: closeFn } })
+ expect(wrapper.find('.ix-icon-close').exists()).toBe(false)
+
+ await wrapper.setProps({ closable: true })
+ await wrapper.find('.ix-icon-close').trigger('click')
+ expect(closeFn).toBeCalledTimes(1)
+ expect(closeFn.mock.calls[0][0]).toBe('a')
+ })
+
+ test('prop gap work', async () => {
+ const wrapper = TagGroupMount({ props: { dataSource, gap: 16 } })
+ const element = wrapper.find('.ix-space-item').element as HTMLElement
+
+ expect(element.style.marginRight).toBe('16px')
+
+ await wrapper.setProps({ gap: 24 })
+ expect(element.style.marginRight).toBe('24px')
+ })
+
+ test('prop wrap work', async () => {
+ const wrapper = TagGroupMount({ props: { dataSource, wrap: true } })
+
+ expect(wrapper.find('.ix-space-nowrap').exists()).toBe(false)
+
+ await wrapper.setProps({ wrap: false })
+ expect(wrapper.find('.ix-space-nowrap').exists()).toBe(true)
+ })
+
+ test('prop shape work', async () => {
+ const wrapper = TagGroupMount({ props: { dataSource, shape: 'round' } })
+ const tags = wrapper.findAll('.ix-tag')
+
+ tags.forEach(item => expect(item.classes()).toContain('ix-tag-round'))
+
+ await wrapper.setProps({ shape: 'rect' })
+ tags.forEach(item => expect(item.classes()).toContain('ix-tag-rect'))
+ })
+
+ test('closeIcon slot work', () => {
+ const wrapper = TagGroupMount({
+ props: { closable: true, dataSource },
+ slots: { closeIcon: () => h(IxIcon, { name: 'up' }) },
+ })
+
+ expect(wrapper.find('.ix-icon-close').exists()).not.toBe(true)
+ expect(wrapper.find('.ix-icon-up').exists()).toBe(true)
+ })
+})
diff --git a/packages/components/tag/demo/Clickable.md b/packages/components/tag/demo/Clickable.md
new file mode 100644
index 000000000..e1368b619
--- /dev/null
+++ b/packages/components/tag/demo/Clickable.md
@@ -0,0 +1,9 @@
+---
+order: 7
+title:
+ zh: 一组标签
+---
+
+## zh
+
+常用于表格的筛选功能,可以通过点击选中。
diff --git a/packages/components/tag/demo/Clickable.vue b/packages/components/tag/demo/Clickable.vue
new file mode 100644
index 000000000..5321125bf
--- /dev/null
+++ b/packages/components/tag/demo/Clickable.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
diff --git a/packages/components/tag/demo/RemovableTag.md b/packages/components/tag/demo/RemovableTag.md
new file mode 100644
index 000000000..7a581c384
--- /dev/null
+++ b/packages/components/tag/demo/RemovableTag.md
@@ -0,0 +1,9 @@
+---
+order: 8
+title:
+ zh: 可移除的标签
+---
+
+## zh
+
+`closable`属性定义一个标签是否可以移除,当标签被移除时会触发`close`事件。
diff --git a/packages/components/tag/demo/RemovableTag.vue b/packages/components/tag/demo/RemovableTag.vue
new file mode 100644
index 000000000..edc1b4198
--- /dev/null
+++ b/packages/components/tag/demo/RemovableTag.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
diff --git a/packages/components/tag/docs/Index.zh.md b/packages/components/tag/docs/Index.zh.md
index 35fe1f1ef..1f7c93695 100644
--- a/packages/components/tag/docs/Index.zh.md
+++ b/packages/components/tag/docs/Index.zh.md
@@ -19,6 +19,30 @@ order: 0
| `icon` | 标签图标 | `string \| #icon` | - | - | - |
| `shape` | 标签形状 | `round \| rect` | `rect` | ✅ | shape在 `number` 属性提供的前提下不生效 |
+### IxTagGroup
+
+#### TagGroupProps
+
+| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
+| --- | --- | --- | --- | --- | --- |
+| `v-model:activeKeys` | 激活的标签的key | `VKey` | `[]` | - | - |
+| `clickable` | 标签是否可点击 | `boolean` | `false` | - | - |
+| `closable` | 标签是否可关闭 | `boolean` | `false` | - | - |
+| `closeIcon` | 标签是否可关闭 | `string \| #closeIcon` | `close` | - | - |
+| `dataSource` | 数据源 | `TagData[]` | - | - | - |
+| `gap` | 每个标签的间隔 | `number \| 'sm' \| 'md' \| 'lg'` | `sm` | ✅ | 参考`IxSpace` |
+| `wrap` | 是否自动换行 | `boolean` | `true` | ✅ | 参考`IxSpace` |
+| `shape` | 标签形状 | `'round' \| 'rect'` | - | - | - |
+| `onClick` | 标签被点击 | `(key:VKey, evt: MouseEvent) => void` | - | - | - |
+| `onClose` | 标签的关闭图标被点击 | `(key:VKey, evt: MouseEvent) => void` | - | - | - |
+
+```ts
+export interface TagData extends Omit {
+ key: VKey
+ text: string
+}
+```
+
## 主题变量
@@ -38,4 +62,4 @@ order: 0
| `@tag-round-border-radius` | `20px` | - | - |
| `@tag-default-border-color` | `transparent` | - | - |
| `@tag-default-background-color` | `#f0f1f2` | - | - |
-
\ No newline at end of file
+
diff --git a/packages/components/tag/index.ts b/packages/components/tag/index.ts
index a23cabb54..0f5c35ea6 100644
--- a/packages/components/tag/index.ts
+++ b/packages/components/tag/index.ts
@@ -5,12 +5,23 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { TagComponent } from './src/types'
+import type { TagComponent, TagGroupComponent } from './src/types'
import Tag from './src/Tag'
+import TagGroup from './src/TagGroup'
const IxTag = Tag as unknown as TagComponent
+const IxTagGroup = TagGroup as unknown as TagGroupComponent
-export { IxTag }
+export { IxTag, IxTagGroup }
-export type { TagInstance, TagComponent, TagPublicProps as TagProps, TagShape } from './src/types'
+export type {
+ TagInstance,
+ TagComponent,
+ TagPublicProps as TagProps,
+ TagShape,
+ TagGroupInstance,
+ TagGroupComponent,
+ TagGroupPublicProps as TagGroupProps,
+ TagData,
+} from './src/types'
diff --git a/packages/components/tag/src/Tag.tsx b/packages/components/tag/src/Tag.tsx
index d9d807fe0..48cb0c5c1 100644
--- a/packages/components/tag/src/Tag.tsx
+++ b/packages/components/tag/src/Tag.tsx
@@ -55,11 +55,13 @@ export default defineComponent({
const prefixCls = mergedPrefixCls.value
const { icon, number } = props
const icoNode = slots.icon ? slots.icon() : icon &&
+
return (
{renderNumericPrefix(prefixCls, number, style.value)}
{icoNode}
{slots.default?.()}
+ {slots.suffix?.()}
)
}
diff --git a/packages/components/tag/src/TagGroup.tsx b/packages/components/tag/src/TagGroup.tsx
new file mode 100644
index 000000000..197910fe3
--- /dev/null
+++ b/packages/components/tag/src/TagGroup.tsx
@@ -0,0 +1,85 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import { computed, defineComponent } from 'vue'
+
+import { type VKey, callEmit } from '@idux/cdk/utils'
+import { useGlobalConfig } from '@idux/components/config'
+import { IxIcon } from '@idux/components/icon'
+import { IxSpace } from '@idux/components/space'
+import { IxTag } from '@idux/components/tag'
+
+import { tagGroupProps } from './types'
+
+export default defineComponent({
+ name: 'IxTagGroup',
+ props: tagGroupProps,
+ setup(props, { slots }) {
+ const common = useGlobalConfig('common')
+ const mergedPrefixCls = computed(() => `${common.prefixCls}-tag-group`)
+ const config = useGlobalConfig('tagGroup')
+ const gap = computed(() => props.gap ?? config.gap)
+ const wrap = computed(() => props.wrap ?? config.wrap)
+
+ const containerCls = computed(() => {
+ const prefixCls = mergedPrefixCls.value
+
+ return {
+ [prefixCls]: true,
+ [`${prefixCls}-clickable`]: props.clickable,
+ }
+ })
+
+ const onTagClick = (key: VKey, evt: MouseEvent) => {
+ if (props.clickable) {
+ callEmit(props.onClick, key, evt)
+ const activeKeys = props.activeKeys
+ const targetIndex = activeKeys.findIndex(currentKey => currentKey === key)
+ targetIndex === -1 ? activeKeys.push(key) : activeKeys.splice(targetIndex, 1)
+ callEmit(props['onUpdate:activeKeys'], activeKeys)
+ }
+ }
+ const onCloseIconClick = (key: VKey, evt: MouseEvent) => {
+ callEmit(props.onClose, key, evt)
+ }
+
+ return () => {
+ const prefixCls = mergedPrefixCls.value
+ const closeIconNode = slots.closeIcon?.() ??
+
+ return (
+
+ {props.dataSource?.map(tagData => {
+ const suffixSlot = {
+ suffix: () => (
+ onCloseIconClick(tagData.key, evt)}>
+ {closeIconNode}
+
+ ),
+ }
+
+ return (
+ onTagClick(tagData.key, evt)}
+ >
+ {{
+ default: () => tagData.text,
+ ...(props.closable && suffixSlot),
+ }}
+
+ )
+ })}
+
+ )
+ }
+ },
+})
diff --git a/packages/components/tag/src/types.ts b/packages/components/tag/src/types.ts
index 954152a4d..29a5062b7 100644
--- a/packages/components/tag/src/types.ts
+++ b/packages/components/tag/src/types.ts
@@ -5,7 +5,7 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils'
+import type { ExtractInnerPropTypes, ExtractPublicPropTypes, VKey } from '@idux/cdk/utils'
import type { DefineComponent, HTMLAttributes } from 'vue'
import { IxPropTypes } from '@idux/cdk/utils'
@@ -23,3 +23,27 @@ export type TagProps = ExtractInnerPropTypes
export type TagPublicProps = ExtractPublicPropTypes
export type TagComponent = DefineComponent & TagPublicProps>
export type TagInstance = InstanceType>
+
+export interface TagData extends Omit {
+ key: VKey
+ text: string
+}
+
+export const tagGroupProps = {
+ activeKeys: IxPropTypes.array().def(() => []),
+ clickable: IxPropTypes.bool.def(false),
+ closable: IxPropTypes.bool.def(false),
+ closeIcon: IxPropTypes.string.def('close'),
+ dataSource: IxPropTypes.array(),
+ gap: IxPropTypes.oneOfType([Number, String, IxPropTypes.array()]),
+ wrap: IxPropTypes.bool,
+ shape: IxPropTypes.oneOf(['rect', 'round']),
+ 'onUpdate:activeKeys': IxPropTypes.emit<(activeKeys: VKey[]) => void>(),
+ onClick: IxPropTypes.emit<(key: VKey, evt: MouseEvent) => void>(),
+ onClose: IxPropTypes.emit<(key: VKey, evt: MouseEvent) => void>(),
+}
+
+export type TagGroupProps = ExtractInnerPropTypes
+export type TagGroupPublicProps = ExtractPublicPropTypes
+export type TagGroupComponent = DefineComponent & TagGroupPublicProps>
+export type TagGroupInstance = InstanceType>
diff --git a/packages/components/tag/style/index.less b/packages/components/tag/style/index.less
index 3c9ec5b48..0e43a147a 100644
--- a/packages/components/tag/style/index.less
+++ b/packages/components/tag/style/index.less
@@ -67,3 +67,9 @@
border: 1px solid @text-color-inverse;
}
}
+
+.@{tag-group-prefix} {
+ &-clickable, &-close-icon {
+ cursor: pointer;
+ }
+}
diff --git a/packages/components/types.d.ts b/packages/components/types.d.ts
index 3dcee514f..9125fba86 100644
--- a/packages/components/types.d.ts
+++ b/packages/components/types.d.ts
@@ -63,7 +63,7 @@ import type { StepperComponent, StepperItemComponent } from '@idux/components/st
import type { SwitchComponent } from '@idux/components/switch'
import type { TableColumnComponent, TableComponent } from '@idux/components/table'
import type { TabComponent, TabsComponent } from '@idux/components/tabs'
-import type { TagComponent } from '@idux/components/tag'
+import type { TagComponent, TagGroupComponent } from '@idux/components/tag'
import type { TextareaComponent } from '@idux/components/textarea'
import type { TimePickerComponent } from '@idux/components/time-picker'
import type { TimelineComponent, TimelineItemComponent } from '@idux/components/timeline'
@@ -150,6 +150,7 @@ declare module 'vue' {
IxTableColumn: TableColumnComponent
IxTabs: TabsComponent
IxTag: TagComponent
+ IxTagGroup: TagGroupComponent
IxTextarea: TextareaComponent
IxTimeline: TimelineComponent
IxTimelineItem: TimelineItemComponent
|