From e346b13acb1f13091a3bd17807fd5a3429896bb2 Mon Sep 17 00:00:00 2001 From: "X.Q. Chen" <31237954+brenner8023@users.noreply.github.com> Date: Thu, 7 Apr 2022 00:17:31 +0800 Subject: [PATCH] feat(comp:tag-group): add tagGroup component --- .../components/config/src/defaultConfig.ts | 4 + packages/components/config/src/types.ts | 6 + packages/components/index.ts | 3 +- .../components/style/variable/prefix.less | 1 + .../__snapshots__/table.spec.ts.snap | 40 +++++-- .../__tests__/__snapshots__/tag.spec.ts.snap | 5 +- .../__snapshots__/tagGroup.spec.ts.snap | 12 ++ .../components/tag/__tests__/tagGroup.spec.ts | 105 ++++++++++++++++++ packages/components/tag/demo/Clickable.md | 9 ++ packages/components/tag/demo/Clickable.vue | 45 ++++++++ packages/components/tag/demo/RemovableTag.md | 9 ++ packages/components/tag/demo/RemovableTag.vue | 39 +++++++ packages/components/tag/docs/Index.zh.md | 26 ++++- packages/components/tag/index.ts | 17 ++- packages/components/tag/src/Tag.tsx | 2 + packages/components/tag/src/TagGroup.tsx | 85 ++++++++++++++ packages/components/tag/src/types.ts | 26 ++++- packages/components/tag/style/index.less | 6 + packages/components/types.d.ts | 3 +- 19 files changed, 425 insertions(+), 18 deletions(-) create mode 100644 packages/components/tag/__tests__/__snapshots__/tagGroup.spec.ts.snap create mode 100644 packages/components/tag/__tests__/tagGroup.spec.ts create mode 100644 packages/components/tag/demo/Clickable.md create mode 100644 packages/components/tag/demo/Clickable.vue create mode 100644 packages/components/tag/demo/RemovableTag.md create mode 100644 packages/components/tag/demo/RemovableTag.vue create mode 100644 packages/components/tag/src/TagGroup.tsx 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`] = ` +"
+
A + +
+
B + +
+
" +`; 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