From e86161568affa4fb71d36f75ad5b8045cdbef699 Mon Sep 17 00:00:00 2001 From: saller Date: Sat, 19 Mar 2022 17:51:35 +0800 Subject: [PATCH] feat(comp:transfer): add transfer component (#794) * add private component CheckableList fix #782 --- .../__snapshots__/checkableList.spec.ts.snap | 108 + .../checkableListItem.spec.ts.snap | 7 + .../__tests__/checkableList.spec.ts | 100 + .../__tests__/checkableListItem.spec.ts | 120 + .../_private/checkable-list/index.ts | 22 + .../checkable-list/src/CheckableList.tsx | 123 + .../checkable-list/src/CheckableListItem.tsx | 71 + .../_private/checkable-list/src/token.ts | 14 + .../_private/checkable-list/src/types.ts | 71 + .../_private/checkable-list/style/index.less | 57 + .../checkable-list/style/themes/default.less | 4 + .../checkable-list/style/themes/default.ts | 6 + .../style/themes/default.variable.less | 15 + .../components/config/src/defaultConfig.ts | 6 + packages/components/config/src/types.ts | 9 + packages/components/default.less | 2 + packages/components/icon/demo/all.ts | 2 + packages/components/icon/src/definitions.ts | 12 +- packages/components/icon/src/dependencies.ts | 10 +- packages/components/index.ts | 2 + .../components/locales/src/langs/en-US.ts | 5 + .../components/locales/src/langs/zh-CN.ts | 5 + packages/components/locales/src/types.ts | 7 + .../components/style/variable/prefix.less | 2 + .../__snapshots__/transfer.spec.ts.snap | 169 + .../__snapshots__/transferSlots.spec.ts.snap | 4346 +++++++++++++++++ .../transfer/__tests__/transfer.spec.ts | 432 ++ .../transfer/__tests__/transferSlots.spec.ts | 236 + packages/components/transfer/demo/Basic.md | 14 + packages/components/transfer/demo/Basic.vue | 30 + .../transfer/demo/CustomHeaderFooter.md | 14 + .../transfer/demo/CustomHeaderFooter.vue | 36 + .../components/transfer/demo/CustomLabel.md | 14 + .../components/transfer/demo/CustomLabel.vue | 31 + .../transfer/demo/CustomListBody.md | 14 + .../transfer/demo/CustomListBody.vue | 92 + .../transfer/demo/CustomOperations.md | 14 + .../transfer/demo/CustomOperations.vue | 82 + .../components/transfer/demo/NoOperations.md | 14 + .../components/transfer/demo/NoOperations.vue | 30 + .../components/transfer/demo/Pagination.md | 14 + .../components/transfer/demo/Pagination.vue | 34 + packages/components/transfer/demo/Remote.md | 14 + packages/components/transfer/demo/Remote.vue | 76 + .../components/transfer/demo/Searchable.md | 14 + .../components/transfer/demo/Searchable.vue | 26 + .../components/transfer/demo/VirtualScroll.md | 14 + .../transfer/demo/VirtualScroll.vue | 49 + .../components/transfer/docs/Design.en.md | 3 + .../components/transfer/docs/Design.zh.md | 3 + packages/components/transfer/docs/Index.en.md | 29 + packages/components/transfer/docs/Index.zh.md | 178 + packages/components/transfer/index.ts | 37 + packages/components/transfer/src/Transfer.tsx | 106 + .../transfer/src/TransferOperations.tsx | 61 + .../transfer/src/composables/useGetRowKey.ts | 36 + .../transfer/src/composables/usePagination.ts | 93 + .../transfer/src/composables/useSearchable.ts | 32 + .../src/composables/useTransferBindings.ts | 122 + .../src/composables/useTransferData.ts | 192 + .../composables/useTransferDataStrategies.ts | 106 + .../src/composables/useTransferOperations.ts | 97 + .../src/composables/useTransferSelectState.ts | 230 + .../transfer/src/list/TransferList.tsx | 79 + .../transfer/src/list/TransferListBody.tsx | 108 + .../transfer/src/list/TransferListFooter.tsx | 59 + .../transfer/src/list/TransferListHeader.tsx | 167 + packages/components/transfer/src/token.ts | 35 + packages/components/transfer/src/types.ts | 181 + packages/components/transfer/src/utils.ts | 21 + packages/components/transfer/style/index.less | 34 + packages/components/transfer/style/list.less | 118 + .../transfer/style/themes/default.less | 4 + .../transfer/style/themes/default.ts | 11 + .../style/themes/default.variable.less | 30 + packages/components/types.d.ts | 2 + scripts/gulp/icons/assets/clear.svg | 2 +- scripts/gulp/icons/assets/left-double.svg | 1 + scripts/gulp/icons/assets/right-double.svg | 1 + 79 files changed, 8763 insertions(+), 4 deletions(-) create mode 100644 packages/components/_private/checkable-list/__tests__/__snapshots__/checkableList.spec.ts.snap create mode 100644 packages/components/_private/checkable-list/__tests__/__snapshots__/checkableListItem.spec.ts.snap create mode 100644 packages/components/_private/checkable-list/__tests__/checkableList.spec.ts create mode 100644 packages/components/_private/checkable-list/__tests__/checkableListItem.spec.ts create mode 100644 packages/components/_private/checkable-list/index.ts create mode 100644 packages/components/_private/checkable-list/src/CheckableList.tsx create mode 100644 packages/components/_private/checkable-list/src/CheckableListItem.tsx create mode 100644 packages/components/_private/checkable-list/src/token.ts create mode 100644 packages/components/_private/checkable-list/src/types.ts create mode 100644 packages/components/_private/checkable-list/style/index.less create mode 100644 packages/components/_private/checkable-list/style/themes/default.less create mode 100644 packages/components/_private/checkable-list/style/themes/default.ts create mode 100644 packages/components/_private/checkable-list/style/themes/default.variable.less create mode 100644 packages/components/transfer/__tests__/__snapshots__/transfer.spec.ts.snap create mode 100644 packages/components/transfer/__tests__/__snapshots__/transferSlots.spec.ts.snap create mode 100644 packages/components/transfer/__tests__/transfer.spec.ts create mode 100644 packages/components/transfer/__tests__/transferSlots.spec.ts create mode 100644 packages/components/transfer/demo/Basic.md create mode 100644 packages/components/transfer/demo/Basic.vue create mode 100644 packages/components/transfer/demo/CustomHeaderFooter.md create mode 100644 packages/components/transfer/demo/CustomHeaderFooter.vue create mode 100644 packages/components/transfer/demo/CustomLabel.md create mode 100644 packages/components/transfer/demo/CustomLabel.vue create mode 100644 packages/components/transfer/demo/CustomListBody.md create mode 100644 packages/components/transfer/demo/CustomListBody.vue create mode 100644 packages/components/transfer/demo/CustomOperations.md create mode 100644 packages/components/transfer/demo/CustomOperations.vue create mode 100644 packages/components/transfer/demo/NoOperations.md create mode 100644 packages/components/transfer/demo/NoOperations.vue create mode 100644 packages/components/transfer/demo/Pagination.md create mode 100644 packages/components/transfer/demo/Pagination.vue create mode 100644 packages/components/transfer/demo/Remote.md create mode 100644 packages/components/transfer/demo/Remote.vue create mode 100644 packages/components/transfer/demo/Searchable.md create mode 100644 packages/components/transfer/demo/Searchable.vue create mode 100644 packages/components/transfer/demo/VirtualScroll.md create mode 100644 packages/components/transfer/demo/VirtualScroll.vue create mode 100644 packages/components/transfer/docs/Design.en.md create mode 100644 packages/components/transfer/docs/Design.zh.md create mode 100644 packages/components/transfer/docs/Index.en.md create mode 100644 packages/components/transfer/docs/Index.zh.md create mode 100644 packages/components/transfer/index.ts create mode 100644 packages/components/transfer/src/Transfer.tsx create mode 100644 packages/components/transfer/src/TransferOperations.tsx create mode 100644 packages/components/transfer/src/composables/useGetRowKey.ts create mode 100644 packages/components/transfer/src/composables/usePagination.ts create mode 100644 packages/components/transfer/src/composables/useSearchable.ts create mode 100644 packages/components/transfer/src/composables/useTransferBindings.ts create mode 100644 packages/components/transfer/src/composables/useTransferData.ts create mode 100644 packages/components/transfer/src/composables/useTransferDataStrategies.ts create mode 100644 packages/components/transfer/src/composables/useTransferOperations.ts create mode 100644 packages/components/transfer/src/composables/useTransferSelectState.ts create mode 100644 packages/components/transfer/src/list/TransferList.tsx create mode 100644 packages/components/transfer/src/list/TransferListBody.tsx create mode 100644 packages/components/transfer/src/list/TransferListFooter.tsx create mode 100644 packages/components/transfer/src/list/TransferListHeader.tsx create mode 100644 packages/components/transfer/src/token.ts create mode 100644 packages/components/transfer/src/types.ts create mode 100644 packages/components/transfer/src/utils.ts create mode 100644 packages/components/transfer/style/index.less create mode 100644 packages/components/transfer/style/list.less create mode 100644 packages/components/transfer/style/themes/default.less create mode 100644 packages/components/transfer/style/themes/default.ts create mode 100644 packages/components/transfer/style/themes/default.variable.less create mode 100644 scripts/gulp/icons/assets/left-double.svg create mode 100644 scripts/gulp/icons/assets/right-double.svg diff --git a/packages/components/_private/checkable-list/__tests__/__snapshots__/checkableList.spec.ts.snap b/packages/components/_private/checkable-list/__tests__/__snapshots__/checkableList.spec.ts.snap new file mode 100644 index 000000000..936c5bc38 --- /dev/null +++ b/packages/components/_private/checkable-list/__tests__/__snapshots__/checkableList.spec.ts.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CheckableList render work 1`] = ` +"
+ +
" +`; diff --git a/packages/components/_private/checkable-list/__tests__/__snapshots__/checkableListItem.spec.ts.snap b/packages/components/_private/checkable-list/__tests__/__snapshots__/checkableListItem.spec.ts.snap new file mode 100644 index 000000000..1b6e6c699 --- /dev/null +++ b/packages/components/_private/checkable-list/__tests__/__snapshots__/checkableListItem.spec.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CheckableListItem render work 1`] = ` +"
  • " +`; diff --git a/packages/components/_private/checkable-list/__tests__/checkableList.spec.ts b/packages/components/_private/checkable-list/__tests__/checkableList.spec.ts new file mode 100644 index 000000000..bd41bf03a --- /dev/null +++ b/packages/components/_private/checkable-list/__tests__/checkableList.spec.ts @@ -0,0 +1,100 @@ +import { MountingOptions, mount } from '@vue/test-utils' + +import { renderWork } from '@tests' + +import { CdkVirtualScroll } from '@idux/cdk/scroll' +import { IxCheckbox } from '@idux/components/checkbox' + +import CheckableList from '../src/CheckableList' +import CheckableListItem from '../src/CheckableListItem' +import { CheckableListProps } from '../src/types' + +const mockedDataSource = Array.from(new Array(20)).map((_, idx) => ({ + key: idx, + label: `Option-${idx}`, +})) + +describe('CheckableList', () => { + const CheckableListMount = (options?: MountingOptions>) => + mount(CheckableList, { ...(options as MountingOptions) }) + + renderWork(CheckableList, { + props: { dataSource: mockedDataSource }, + }) + + test('checked work', async () => { + const wrapper = CheckableListMount({ + props: { dataSource: mockedDataSource, checked: item => [1, 2, 3, 4].includes(item.key as number) }, + }) + + expect( + wrapper + .findAllComponents(CheckableListItem) + .filter(item => [1, 2, 3, 4].includes(item.props().value)) + .every(item => item.props().checked === true), + ).toBeTruthy() + expect( + wrapper + .findAllComponents(CheckableListItem) + .filter(item => ![1, 2, 3, 4].includes(item.props().value)) + .every(item => item.props().checked === false), + ).toBeTruthy() + }) + + test('disabled work', async () => { + const wrapper = CheckableListMount({ + props: { dataSource: mockedDataSource, disabled: item => [1, 2, 3, 4].includes(item.key as number) }, + }) + + expect( + wrapper + .findAllComponents(CheckableListItem) + .filter(item => [1, 2, 3, 4].includes(item.props().value)) + .every(item => item.props().disabled === true), + ).toBeTruthy() + expect( + wrapper + .findAllComponents(CheckableListItem) + .filter(item => ![1, 2, 3, 4].includes(item.props().value)) + .every(item => item.props().disabled === false), + ).toBeTruthy() + }) + + test('virtual work', async () => { + const wrapper = CheckableListMount({ + props: { dataSource: mockedDataSource, virtual: true, scroll: { height: 100, fullHeight: true } }, + }) + + expect(wrapper.classes()).toContain('ix-checkable-list-virtual') + expect(wrapper.findComponent(CdkVirtualScroll).exists()).toBeTruthy() + }) + + test('onCheckChange work', async () => { + const onCheckChange = jest.fn() + const wrapper = CheckableListMount({ + props: { dataSource: mockedDataSource, onCheckChange, checkable: true }, + }) + + await wrapper + .findAllComponents(CheckableListItem) + .find(item => item.props().value === 3) + ?.findComponent(IxCheckbox) + .find('input') + .setValue(true) + expect(onCheckChange).toBeCalledWith({ key: 3, label: 'Option-3' }, true) + }) + + test('onRemove work', async () => { + const onRemove = jest.fn() + const wrapper = CheckableListMount({ + props: { dataSource: mockedDataSource, onRemove, removable: true }, + }) + + await wrapper + .findAllComponents(CheckableListItem) + .find(item => item.props().value === 3) + ?.find('.ix-checkable-list-item-close-icon') + .trigger('click') + expect(onRemove).toBeCalledWith({ key: 3, label: 'Option-3' }) + }) +}) diff --git a/packages/components/_private/checkable-list/__tests__/checkableListItem.spec.ts b/packages/components/_private/checkable-list/__tests__/checkableListItem.spec.ts new file mode 100644 index 000000000..097193fed --- /dev/null +++ b/packages/components/_private/checkable-list/__tests__/checkableListItem.spec.ts @@ -0,0 +1,120 @@ +import { MountingOptions, mount } from '@vue/test-utils' +import { computed, h } from 'vue' + +import { renderWork } from '@tests' + +import { IxCheckbox } from '@idux/components/checkbox' + +import CheckableListItem from '../src/CheckableListItem' +import { checkableListContext } from '../src/token' +import { CheckableListItemProps } from '../src/types' + +const mountGlobalOpts = { + provide: { + [checkableListContext as symbol]: { + mergedPrefixCls: computed(() => 'ix-checkable-list'), + }, + }, +} + +describe('CheckableListItem', () => { + const CheckableListItemMount = (options?: MountingOptions>) => + mount(CheckableListItem, { ...(options as MountingOptions), global: mountGlobalOpts }) + + renderWork(CheckableListItem, { + props: { + value: '1', + label: 'Option-1', + checkable: true, + removable: true, + disabled: false, + }, + global: mountGlobalOpts, + }) + + test('checkable work', async () => { + const wrapper = CheckableListItemMount({ + props: { + value: '1', + label: 'Option-1', + checked: true, + checkable: true, + removable: true, + disabled: false, + }, + }) + + const checkboxComp = wrapper.findComponent(IxCheckbox) + expect(checkboxComp.exists()).toBeTruthy() + expect(checkboxComp.vm.checked).toBe(true) + + await wrapper.setProps({ checkable: false }) + expect(wrapper.findComponent(IxCheckbox).exists()).toBeFalsy() + }) + + test('removable work', async () => { + const onRemove = jest.fn() + const wrapper = CheckableListItemMount({ + props: { + value: '1', + label: 'Option-1', + checkable: true, + removable: true, + disabled: false, + onRemove, + }, + }) + + const removeTrigger = wrapper.find('.ix-checkable-list-item-close-icon') + expect(removeTrigger.exists()).toBeTruthy() + await removeTrigger.trigger('click') + expect(onRemove).toBeCalled() + + await wrapper.setProps({ removable: false }) + expect(wrapper.find('.ix-checkable-list-item-close-icon').exists()).toBeFalsy() + }) + + test('disabled work', async () => { + const onCheckChange = jest.fn() + const wrapper = CheckableListItemMount({ + props: { + value: '1', + label: 'Option-1', + checkable: true, + removable: true, + disabled: true, + onCheckChange, + }, + }) + + expect(wrapper.classes()).toContain('ix-checkable-list-item-disabled') + expect(wrapper.find('.ix-checkable-list-item-close-icon').exists()).toBeFalsy() + await wrapper.findComponent(IxCheckbox).find('input').setValue(true) + expect(onCheckChange).not.toBeCalled() + + await wrapper.findComponent(IxCheckbox).find('input').setValue(false) + + await wrapper.setProps({ disabled: false }) + expect(wrapper.classes()).not.toContain('ix-checkable-list-item-disabled') + expect(wrapper.find('.ix-checkable-list-item-close-icon').exists()).toBeTruthy() + await wrapper.findComponent(IxCheckbox).find('input').setValue(true) + expect(onCheckChange).toBeCalledWith(true) + }) + + test('slot work', async () => { + const wrapper = CheckableListItemMount({ + props: { + value: '1', + label: 'Option-1', + checkable: true, + removable: true, + disabled: true, + }, + slots: { + default: () => h('span', { class: 'custom-label-slot' }), + }, + }) + + expect(wrapper.find('.custom-label-slot').exists()).toBeTruthy() + }) +}) diff --git a/packages/components/_private/checkable-list/index.ts b/packages/components/_private/checkable-list/index.ts new file mode 100644 index 000000000..7f5481003 --- /dev/null +++ b/packages/components/_private/checkable-list/index.ts @@ -0,0 +1,22 @@ +/** + * @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 type { CheckableListComponent } from './src/types' + +import CheckableList from './src/CheckableList' + +const ɵCheckableList = CheckableList as unknown as CheckableListComponent + +export { ɵCheckableList } + +export type { + CheckableListData as ɵCheckableListData, + CheckableListScroll as ɵCheckableListScroll, + CheckableListInstance as ɵCheckableListInstance, + CheckableListComponent as ɵCheckableListComponent, + CheckableListPublicProps as ɵCheckableListProps, +} from './src/types' diff --git a/packages/components/_private/checkable-list/src/CheckableList.tsx b/packages/components/_private/checkable-list/src/CheckableList.tsx new file mode 100644 index 000000000..10ae1bc24 --- /dev/null +++ b/packages/components/_private/checkable-list/src/CheckableList.tsx @@ -0,0 +1,123 @@ +/** + * @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 type { VirtualScrollInstance } from '@idux/cdk/scroll' + +import { computed, defineComponent, normalizeClass, provide, ref } from 'vue' + +import { CdkVirtualScroll } from '@idux/cdk/scroll' +import { VKey, callEmit } from '@idux/cdk/utils' +import { useGlobalConfig } from '@idux/components/config' + +import CheckableListItem from './CheckableListItem' +import { checkableListContext } from './token' +import { type CheckableListApi, type CheckableListData, checkableListProps } from './types' + +export default defineComponent({ + name: 'IxCheckableList', + props: checkableListProps, + setup(props, { slots, expose }) { + const common = useGlobalConfig('common') + const mergedPrefixCls = computed(() => `${common.prefixCls}-checkable-list`) + const virtualScrollRef = ref() + + provide(checkableListContext, { + mergedPrefixCls, + }) + + const checkableListApi: CheckableListApi = { + scrollTo: (...params) => virtualScrollRef.value?.scrollTo(...params), + } + + expose(checkableListApi) + + const getRowKey = (item: CheckableListData) => (props.getRowKey?.(item) ?? item.key)! + + const handleScroll = (evt: Event) => { + callEmit(props.onScroll, evt) + } + const handleScrolledBottom = () => { + callEmit(props.onScrolledBottom) + } + const handleScrolledChange = (startIndex: number, endIndex: number, visibleData: unknown[]) => { + callEmit(props.onScrolledChange, startIndex, endIndex, visibleData) + } + + const renderListItem = (item: CheckableListData) => { + const key = getRowKey(item) + const onCheckChange = (checked: boolean) => { + callEmit(props.onCheckChange, item, checked) + } + const onRemove = () => { + callEmit(props.onRemove, item) + } + + return ( + slots.label?.(item)) }} + onCheckChange={onCheckChange} + onRemove={onRemove} + {...(item.additional ?? {})} + /> + ) + } + + const renderBody = () => { + const { dataSource, virtual, scroll } = props + const data = dataSource ?? [] + + if (data.length <= 0) { + return + } + + if (virtual && scroll) { + const { height, fullHeight } = scroll + return ( + VKey} + itemRender={({ item }) => renderListItem(item)} + virtual + onScroll={handleScroll} + onScrolledBottom={handleScrolledBottom} + onScrolledChange={handleScrolledChange} + /> + ) + } + + return ( +
      + {data.map(item => renderListItem(item))} +
    + ) + } + + const classes = computed(() => { + const prefixCls = mergedPrefixCls.value + + return normalizeClass({ + [prefixCls]: true, + [`${prefixCls}-virtual`]: !!props.virtual, + }) + }) + + return () => { + return
    {renderBody()}
    + } + }, +}) diff --git a/packages/components/_private/checkable-list/src/CheckableListItem.tsx b/packages/components/_private/checkable-list/src/CheckableListItem.tsx new file mode 100644 index 000000000..b808fc2be --- /dev/null +++ b/packages/components/_private/checkable-list/src/CheckableListItem.tsx @@ -0,0 +1,71 @@ +/** + * @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, inject, normalizeClass } from 'vue' + +import { callEmit } from '@idux/cdk/utils' +import { IxCheckbox } from '@idux/components/checkbox' +import { IxIcon } from '@idux/components/icon' + +import { checkableListContext } from './token' +import { checkableListItemProps } from './types' + +export default defineComponent({ + props: checkableListItemProps, + setup(props, { slots }) { + const { mergedPrefixCls } = inject(checkableListContext)! + + const onCheckChange = (value: number | boolean | string) => { + callEmit(props.onCheckChange, !!value) + } + const onRemove = () => { + callEmit(props.onRemove) + } + + const renderLabel = (prefixCls: string) => { + const { checked, value, disabled, checkable } = props + + if (checkable) { + return ( + + ) + } + + return + } + + const classes = computed(() => { + const prefixCls = `${mergedPrefixCls.value}-item` + + return normalizeClass({ + [prefixCls]: true, + [`${prefixCls}-disabled`]: props.disabled, + }) + }) + + return () => { + const prefixCls = `${mergedPrefixCls.value}-item` + + return ( +
  • + {renderLabel(prefixCls)} + {props.removable && !props.disabled && ( + + )} +
  • + ) + } + }, +}) diff --git a/packages/components/_private/checkable-list/src/token.ts b/packages/components/_private/checkable-list/src/token.ts new file mode 100644 index 000000000..4959dcede --- /dev/null +++ b/packages/components/_private/checkable-list/src/token.ts @@ -0,0 +1,14 @@ +/** + * @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 type { ComputedRef, InjectionKey } from 'vue' + +export interface CheckableListContext { + mergedPrefixCls: ComputedRef +} + +export const checkableListContext: InjectionKey = Symbol('checkableListContext') diff --git a/packages/components/_private/checkable-list/src/types.ts b/packages/components/_private/checkable-list/src/types.ts new file mode 100644 index 000000000..ceae14e97 --- /dev/null +++ b/packages/components/_private/checkable-list/src/types.ts @@ -0,0 +1,71 @@ +/** + * @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 type { VirtualScrollToFn } from '@idux/cdk/scroll' +import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils' +import type { DefineComponent, HTMLAttributes } from 'vue' + +import { IxPropTypes, type VKey } from '@idux/cdk/utils' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface CheckableListData extends Record { + key?: VKey + label?: string + disabled?: boolean + additional?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + class?: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + style?: any + [key: string]: unknown + } +} + +export interface CheckableListScroll { + height?: string | number + fullHeight?: boolean +} + +export const checkableListProps = { + dataSource: IxPropTypes.array(), + checkable: IxPropTypes.bool.def(true), + removable: IxPropTypes.bool.def(false), + checked: IxPropTypes.func<(item: CheckableListData) => boolean>(), + disabled: IxPropTypes.func<(item: CheckableListData) => boolean>(), + getRowKey: IxPropTypes.func<(item: CheckableListData) => VKey>(), + virtual: IxPropTypes.bool, + scroll: IxPropTypes.object(), + onCheckChange: IxPropTypes.emit<(item: CheckableListData, checked: boolean) => void>(), + onRemove: IxPropTypes.emit<(item: CheckableListData) => void>(), + onScroll: IxPropTypes.emit<(evt: Event) => void>(), + onScrolledChange: IxPropTypes.emit<(startIndex: number, endIndex: number, visibleData: unknown[]) => void>(), + onScrolledBottom: IxPropTypes.emit<() => void>(), +} + +export const checkableListItemProps = { + checked: IxPropTypes.bool.def(false), + checkable: IxPropTypes.bool.def(true), + removable: IxPropTypes.bool.def(false), + disabled: IxPropTypes.bool.def(false), + label: IxPropTypes.string, + value: IxPropTypes.oneOfType([String, Number, Symbol]).isRequired, + onCheckChange: IxPropTypes.emit<(checked: boolean) => void>(), + onRemove: IxPropTypes.emit<() => void>(), +} + +export interface CheckableListApi { + scrollTo: VirtualScrollToFn +} + +export type CheckableListProps = ExtractInnerPropTypes +export type CheckableListItemProps = ExtractInnerPropTypes +export type CheckableListPublicProps = ExtractPublicPropTypes +export type CheckableListComponent = DefineComponent< + Omit & CheckableListPublicProps, + CheckableListApi +> +export type CheckableListInstance = InstanceType> diff --git a/packages/components/_private/checkable-list/style/index.less b/packages/components/_private/checkable-list/style/index.less new file mode 100644 index 000000000..886207e5f --- /dev/null +++ b/packages/components/_private/checkable-list/style/index.less @@ -0,0 +1,57 @@ +@import '../../../style/mixins/reset.less'; + +.@{checkable-list-prefix} { + width: 100%; + height: 100%; + overflow: hidden; + .reset-scroll(); + + &.@{checkable-list-prefix}-virtual { + height: auto; + } + + &-inner { + height: 100%; + display: flex; + flex-direction: column; + overflow: auto; + flex-shrink: 0; + } + + &-item { + display: flex; + align-items: center; + justify-content: flex-start; + min-height: @checkable-list-item-min-height; + padding: @checkable-list-item-padding; + line-height: @checkable-list-item-line-height; + font-size: @checkable-list-item-font-size; + + &-disabled { + color: @checkable-list-disabled-color; + cursor: not-allowed; + } + &-disabled &-label { + cursor: not-allowed; + } + + &-checkbox { + flex: auto; + } + + &-close-icon { + margin-left: auto; + font-size: @checkable-list-close-icon-font-size; + color: @checkable-list-close-icon-color; + &:hover { + color: @checkable-list-close-icon-hover-color; + } + &:focus, + &:active { + color: @checkable-list-close-icon-active-color; + } + + cursor: pointer; + } + } +} diff --git a/packages/components/_private/checkable-list/style/themes/default.less b/packages/components/_private/checkable-list/style/themes/default.less new file mode 100644 index 000000000..2e9d4d3d3 --- /dev/null +++ b/packages/components/_private/checkable-list/style/themes/default.less @@ -0,0 +1,4 @@ +@import '../../../../style/themes/default.less'; +@import '../../../../form/style/themes/default.variable.less'; +@import '../index.less'; +@import './default.variable.less'; diff --git a/packages/components/_private/checkable-list/style/themes/default.ts b/packages/components/_private/checkable-list/style/themes/default.ts new file mode 100644 index 000000000..3ea95c037 --- /dev/null +++ b/packages/components/_private/checkable-list/style/themes/default.ts @@ -0,0 +1,6 @@ +// style dependencies +import '@idux/components/style/core/default' +import '@idux/components/checkbox/style/themes/default' +import '@idux/components/icon/style/themes/default' + +import './default.less' diff --git a/packages/components/_private/checkable-list/style/themes/default.variable.less b/packages/components/_private/checkable-list/style/themes/default.variable.less new file mode 100644 index 000000000..489da5c67 --- /dev/null +++ b/packages/components/_private/checkable-list/style/themes/default.variable.less @@ -0,0 +1,15 @@ +@import '../../../../style/themes/default.less'; + +@checkable-list-color: @text-color; +@checkable-list-disabled-color: @text-color-disabled; + +@checkable-list-item-font-size: @font-size-md; +@checkable-list-item-min-height: @height-md; +@checkable-list-item-padding: 0 @spacing-md; +@checkable-list-item-line-height: @line-height-base; + +@checkable-list-clear-icon-font-size: @font-size-md; +@checkable-list-close-icon-font-size: @font-size-md; +@checkable-list-close-icon-color: @color-graphite-d20; +@checkable-list-close-icon-hover-color: @color-primary; +@checkable-list-close-icon-active-color: @color-primary; \ No newline at end of file diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts index d5578e0c0..4d2789d1b 100644 --- a/packages/components/config/src/defaultConfig.ts +++ b/packages/components/config/src/defaultConfig.ts @@ -296,6 +296,12 @@ export const defaultConfig: GlobalConfig = { allowInput: true, format: 'HH:mm:ss', }, + transfer: { + getKey: 'key', + clearable: true, + clearIcon: 'clear', + showSelectAll: true, + }, tooltip: { autoAdjust: true, delay: 100, diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index ed2bda6c5..0fd924117 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -83,6 +83,7 @@ export interface GlobalConfig { textarea: TextareaConfig timePicker: TimePickerConfig timeRangePicker: TimeRangePickerConfig + transfer: TransferConfig tooltip: TooltipConfig tree: TreeConfig treeSelect: TreeSelectConfig @@ -439,6 +440,14 @@ export interface TimePickerConfig { export type TimeRangePickerConfig = TimePickerConfig +export interface TransferConfig { + getKey: string + searchable?: boolean | { source: boolean; target: boolean } + clearable: boolean + clearIcon: string + showSelectAll: boolean +} + export interface TooltipConfig { autoAdjust: boolean delay: number | [number | null, number | null] diff --git a/packages/components/default.less b/packages/components/default.less index 688b57f86..cd8dac3fd 100644 --- a/packages/components/default.less +++ b/packages/components/default.less @@ -1,5 +1,6 @@ @import './style/core/default.less'; +@import './_private/checkable-list/style/themes/default.less'; @import './_private/collapse-transition/style/themes/default.less'; @import './_private/mask/style/themes/default.less'; @import './_private/overlay/style/themes/default.less'; @@ -57,6 +58,7 @@ @import './textarea/style/themes/default.less'; @import './time-picker/style/themes/default.less'; @import './timeline/style/themes/default.less'; +@import './transfer/style/themes/default.less'; @import './tooltip/style/themes/default.less'; @import './tree/style/themes/default.less'; @import './tree-select/style/themes/default.less'; diff --git a/packages/components/icon/demo/all.ts b/packages/components/icon/demo/all.ts index 12ade3939..b3394c641 100644 --- a/packages/components/icon/demo/all.ts +++ b/packages/components/icon/demo/all.ts @@ -106,6 +106,7 @@ export const allIcons = [ 'key', 'layout', 'left-circle', + 'left-double', 'left', 'like-filled', 'like', @@ -153,6 +154,7 @@ export const allIcons = [ 'redo', 'reload', 'right-circle', + 'right-double', 'right', 'ring-chart', 'robot', diff --git a/packages/components/icon/src/definitions.ts b/packages/components/icon/src/definitions.ts index 9743f37ca..941f5c171 100644 --- a/packages/components/icon/src/definitions.ts +++ b/packages/components/icon/src/definitions.ts @@ -147,7 +147,7 @@ export const Check = { export const Clear = { name: 'clear', - svg: '', + svg: '', } export const ClockCircle = { @@ -540,6 +540,11 @@ export const LeftCircle = { svg: '', } +export const LeftDouble = { + name: 'left-double', + svg: '', +} + export const Left = { name: 'left', svg: '', @@ -775,6 +780,11 @@ export const RightCircle = { svg: '', } +export const RightDouble = { + name: 'right-double', + svg: '', +} + export const Right = { name: 'right', svg: '', diff --git a/packages/components/icon/src/dependencies.ts b/packages/components/icon/src/dependencies.ts index d5cb373e8..78317811b 100644 --- a/packages/components/icon/src/dependencies.ts +++ b/packages/components/icon/src/dependencies.ts @@ -14,6 +14,7 @@ import { Check, CheckCircle, CheckCircleFilled, + Clear, ClockCircle, Close, CloseCircle, @@ -29,6 +30,7 @@ import { InfoCircle, InfoCircleFilled, Left, + LeftDouble, Loading, Menu, MenuFold, @@ -38,6 +40,7 @@ import { QuestionCircle, QuestionCircleFilled, Right, + RightDouble, RotateLeft, RotateRight, Search, @@ -57,8 +60,9 @@ export const IDUX_ICON_DEPENDENCIES: IconDefinition[] = [ Check, // Progress Stepper CheckCircle, // Result Message Alert Notification CheckCircleFilled, // Progress Modal FormItem + Clear, // Transfer ClockCircle, // TimePicker - Close, // Stepper Modal Drawer Image Message Alert + Close, // Stepper Modal Drawer Image Message Alert Transfer CloseCircle, // TimePicker TimeRangePicker Input Textarea Notification CloseCircleFilled, // Modal FormItem DoubleLeft, // Pagination @@ -72,6 +76,7 @@ export const IDUX_ICON_DEPENDENCIES: IconDefinition[] = [ InfoCircle, // Message Result Alert Notification InfoCircleFilled, // Modal Left, // date-panel + LeftDouble, // Transfer Loading, // Message Button Spin FormItem Switch Timeline Tree Menu, // Layout MenuFold, // Layout @@ -81,9 +86,10 @@ export const IDUX_ICON_DEPENDENCIES: IconDefinition[] = [ QuestionCircle, // FormItem QuestionCircleFilled, // Modal Right, // Tree Menu Collapse + RightDouble, // Transfer RotateLeft, // Image RotateRight, // Image - Search, // Select + Search, // Select Transfer StarFilled, // Rate User, // Avatar TreeExpand, // TreeSelect diff --git a/packages/components/index.ts b/packages/components/index.ts index 882367387..e503afaba 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -59,6 +59,7 @@ import { IxTextarea } from '@idux/components/textarea' import { IxTimePicker, IxTimeRangePicker } from '@idux/components/time-picker' import { IxTimeline, IxTimelineItem } from '@idux/components/timeline' import { IxTooltip } from '@idux/components/tooltip' +import { IxTransfer } from '@idux/components/transfer' import { IxTree } from '@idux/components/tree' import { IxTreeSelect } from '@idux/components/tree-select' import { IxTypography } from '@idux/components/typography' @@ -146,6 +147,7 @@ const components = [ IxTextarea, IxTimePicker, IxTimeRangePicker, + IxTransfer, IxTimeline, IxTimelineItem, IxTooltip, diff --git a/packages/components/locales/src/langs/en-US.ts b/packages/components/locales/src/langs/en-US.ts index 82385f866..3312e5068 100644 --- a/packages/components/locales/src/langs/en-US.ts +++ b/packages/components/locales/src/langs/en-US.ts @@ -92,6 +92,11 @@ const enUS: Locale = { separator: 'To', placeholder: ['Start time', 'End time'], }, + transfer: { + toSelect: 'To select', + selected: 'Selected', + searchPlaceholder: ['Key words', 'Key words'], + }, upload: { uploading: 'Uploading...', error: 'Upload error', diff --git a/packages/components/locales/src/langs/zh-CN.ts b/packages/components/locales/src/langs/zh-CN.ts index 86b9ca364..901e28338 100755 --- a/packages/components/locales/src/langs/zh-CN.ts +++ b/packages/components/locales/src/langs/zh-CN.ts @@ -92,6 +92,11 @@ const zhCN: Locale = { separator: '至', placeholder: ['起始时间', '结束时间'], }, + transfer: { + toSelect: '待选', + selected: '已选', + searchPlaceholder: ['搜索关键字', '搜索关键字'], + }, upload: { uploading: '正在上传...', error: '上传失败', diff --git a/packages/components/locales/src/types.ts b/packages/components/locales/src/types.ts index 86646d710..68363f702 100644 --- a/packages/components/locales/src/types.ts +++ b/packages/components/locales/src/types.ts @@ -98,6 +98,12 @@ export interface TimeRangePickerLocale { placeholder: [string, string] } +export interface TransferLocale { + toSelect: string + selected: string + searchPlaceholder: [string, string] +} + export interface UploadLocale { uploading: string error: string @@ -121,6 +127,7 @@ export interface Locale { table: TableLocale timePicker: TimePickerLocale timeRangePicker: TimeRangePickerLocale + transfer: TransferLocale upload: UploadLocale } diff --git a/packages/components/style/variable/prefix.less b/packages/components/style/variable/prefix.less index e3c02ea0b..331452991 100644 --- a/packages/components/style/variable/prefix.less +++ b/packages/components/style/variable/prefix.less @@ -63,6 +63,7 @@ @textarea-prefix: ~'@{idux-prefix}-textarea'; @time-picker-prefix: ~'@{idux-prefix}-time-picker'; @time-range-picker-prefix: ~'@{idux-prefix}-time-range-picker'; +@transfer-prefix: ~'@{idux-prefix}-transfer'; @tree-select-prefix: ~'@{idux-prefix}-tree-select'; @tree-select-option-prefix: ~'@{idux-prefix}-tree-select-option'; @upload-prefix: ~'@{idux-prefix}-upload'; @@ -88,6 +89,7 @@ @affix-prefix: ~'@{idux-prefix}-affix'; // Private +@checkable-list-prefix: ~'@{idux-prefix}-checkable-list'; @collapse-transition-prefix: ~'@{idux-prefix}-collapse-transition'; @date-panel-prefix: ~'@{idux-prefix}-date-panel'; @mask-prefix: ~'@{idux-prefix}-mask'; diff --git a/packages/components/transfer/__tests__/__snapshots__/transfer.spec.ts.snap b/packages/components/transfer/__tests__/__snapshots__/transfer.spec.ts.snap new file mode 100644 index 000000000..b046f6d29 --- /dev/null +++ b/packages/components/transfer/__tests__/__snapshots__/transfer.spec.ts.snap @@ -0,0 +1,169 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transfer render work 1`] = ` +"
    +
    +
    +
    待选 (20) + + + +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    已选 (0) + + +
    +
    +
    +
    +
    +
    +
    暂无数据
    + +
    +
    +
    + +
    +
    " +`; diff --git a/packages/components/transfer/__tests__/__snapshots__/transferSlots.spec.ts.snap b/packages/components/transfer/__tests__/__snapshots__/transferSlots.spec.ts.snap new file mode 100644 index 000000000..fb76f37bb --- /dev/null +++ b/packages/components/transfer/__tests__/__snapshots__/transferSlots.spec.ts.snap @@ -0,0 +1,4346 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transfer clearIcon slot work 1`] = ` +
    +
    + +
    +
    + + + 待选 (20) + + + + +
    +
    +
    +
    +
      + +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + 已选 (0) + + + + +
    + + + +
    +
    +
    +
    +
    +
    + +
    +
    + 暂无数据 +
    + +
    +
    +
    + + +
    +
    +`; + +exports[`Transfer default slot work 1`] = ` +
    +
    + +
    +
    + + + 待选 (20) + + + + + + + + + + + + + +
    +
    +
    + +
    + + 20 + + + 20 + + + 20 + + + 20 + + + 20 + + + 20 + + + 20 + + + 0 + + + 0 + + + 0 + + + 4 + + + false + + + false-false + + + true + + + true + + + + + +
    + +
    + + +
    +
    + +
    + + + + +
    + +
    +
    + +
    +
    + + + 已选 (0) + + + + + + + + + + + + + + + +
    +
    +
    + +
    + + 0 + + + 0 + + + 0 + + + 20 + + + 20 + + + 20 + + + 20 + + + 0 + + + 0 + + + 0 + + + 0 + + + true + + + false-false + + + true + + + true + + + + + +
    + +
    + + +
    +
    +`; + +exports[`Transfer empty slot work 1`] = ` +
    +
    + +
    +
    + + + 待选 (20) + + + + +
    +
    +
    +
    +
      + +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + 已选 (0) + + + + + + +
    +
    +
    +
    + +
    + +
    +
    + + +
    +
    +`; + +exports[`Transfer footer slot work 1`] = ` +
    +
    + +
    +
    + + + 待选 (20) + + + + + + + + + + + + + +
    +
    +
    +
    +
      + +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • + +
    +
    +
    + + +
    +
    + +
    + + + + +
    + +
    +
    + +
    +
    + + + 已选 (0) + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    + +
    +
    + 暂无数据 +
    + +
    +
    +
    + + +
    +
    +`; + +exports[`Transfer headerLabel slot work 1`] = ` +
    +
    + +
    +
    + + + +
    + 20 +
    + +
    + + + +
    +
    +
    +
    +
      + +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + +
    + 0 +
    + +
    + + + + + +
    +
    +
    +
    +
    +
    + +
    +
    + 暂无数据 +
    + +
    +
    +
    + + +
    +
    +`; + +exports[`Transfer label slot work 1`] = ` +
    +
    + +
    +
    + + + 待选 (20) + + + + +
    +
    +
    +
    +
      + +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + 已选 (0) + + + + + + +
    +
    +
    +
    +
    +
    + +
    +
    + 暂无数据 +
    + +
    +
    +
    + + +
    +
    +`; diff --git a/packages/components/transfer/__tests__/transfer.spec.ts b/packages/components/transfer/__tests__/transfer.spec.ts new file mode 100644 index 000000000..56674d233 --- /dev/null +++ b/packages/components/transfer/__tests__/transfer.spec.ts @@ -0,0 +1,432 @@ +import { MountingOptions, mount } from '@vue/test-utils' + +import { renderWork } from '@tests' + +import CheckableListItem from '@idux/components/_private/checkable-list/src/CheckableListItem' +import { ɵInput } from '@idux/components/_private/input' +import { IxButton } from '@idux/components/button' +import { IxCheckbox } from '@idux/components/checkbox' +import { IxSpin } from '@idux/components/spin' + +import Transfer from '../src/Transfer' +import TransferOperations from '../src/TransferOperations' +import TransferList from '../src/list/TransferList' +import TransferListHeader from '../src/list/TransferListHeader' +import { TransferProps } from '../src/types' + +const mockedDataSource = Array.from(new Array(20)).map((_, idx) => ({ + key: idx, + value: idx, + disabled: [1, 6, 12, 16].includes(idx), +})) + +describe('Transfer', () => { + const TransferMount = (options?: MountingOptions>) => + mount(Transfer, { ...(options as MountingOptions) }) + + renderWork(Transfer, { + props: { + dataSource: mockedDataSource, + }, + }) + + test('dataSource work', async () => { + const wrapper = TransferMount({ props: { dataSource: mockedDataSource } }) + + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + expect(sourceList.findAllComponents(CheckableListItem).length).toBe(20) + expect(targetList.findAllComponents(CheckableListItem).length).toBe(0) + }) + + test('v-model:value work', async () => { + const wrapper = TransferMount({ props: { dataSource: mockedDataSource, value: [1] } }) + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + expect(sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === 1) > -1).toBeFalsy() + expect( + targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === 1) > -1, + ).toBeTruthy() + + await wrapper.setProps({ value: [2] }) + expect( + sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === 1) > -1, + ).toBeTruthy() + expect(targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === 1) > -1).toBeFalsy() + expect(sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === 2) > -1).toBeFalsy() + expect( + targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === 2) > -1, + ).toBeTruthy() + }) + + test('transfer work', async () => { + const onChange = jest.fn() + const wrapper = TransferMount({ props: { dataSource: mockedDataSource, onChange } }) + + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + const [appendTrigger, removeTrigger] = wrapper.findComponent(TransferOperations).findAllComponents(IxButton) + await Promise.all( + sourceList + .findAllComponents(CheckableListItem) + .filter(item => [1, 2, 3, 4, 5, 6].includes(item.props().value)) + .map(item => item.findComponent(IxCheckbox).find('input').setValue(true)), + ) + await appendTrigger.trigger('click') + + expect(onChange).toBeCalledWith([2, 3, 4, 5], []) + expect( + [2, 3, 4, 5].some( + value => sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeFalsy() + expect( + [2, 3, 4, 5].every( + value => targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeTruthy() + expect( + [1, 6].every( + value => sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeTruthy() + expect( + [1, 6].some( + value => targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeFalsy() + + await Promise.all( + targetList + .findAllComponents(CheckableListItem) + .filter(item => [3, 4].includes(item.props().value)) + .map(item => item.findComponent(IxCheckbox).find('input').setValue(true)), + ) + await removeTrigger.trigger('click') + + expect(onChange).toBeCalledWith([2, 5], [2, 3, 4, 5]) + expect( + [2, 5].some( + value => sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeFalsy() + expect( + [1, 3, 4, 6].some( + value => targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeFalsy() + expect( + [2, 5].every( + value => targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeTruthy() + expect( + [1, 3, 4, 6].every( + value => sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeTruthy() + + onChange.mockClear() + await Promise.all( + sourceList + .findAllComponents(CheckableListItem) + .filter(item => [13, 14].includes(item.props().value)) + .map(item => item.findComponent(IxCheckbox).find('input').setValue(true)), + ) + await wrapper.setProps({ disabled: true }) + await appendTrigger.trigger('click') + + expect(onChange).not.toBeCalled() + expect( + [13, 14].every( + value => sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeTruthy() + expect( + [13, 14].some( + value => targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeFalsy() + + onChange.mockClear() + await wrapper.setProps({ disabled: false }) + await Promise.all( + targetList + .findAllComponents(CheckableListItem) + .filter(item => [2, 5].includes(item.props().value)) + .map(item => item.findComponent(IxCheckbox).find('input').setValue(true)), + ) + await wrapper.setProps({ disabled: true }) + await removeTrigger.trigger('click') + + expect(onChange).not.toBeCalled() + expect( + [2, 5].every( + value => targetList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeTruthy() + expect( + [2, 5].some( + value => sourceList.findAllComponents(CheckableListItem).findIndex(item => item.props().value === value) > -1, + ), + ).toBeFalsy() + }) + + test('selectAll work', async () => { + const onSelectAll = jest.fn() + const wrapper = TransferMount({ props: { dataSource: mockedDataSource, onSelectAll } }) + + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + const sourceSelectAll = sourceList.findComponent(TransferListHeader).findComponent(IxCheckbox) + const targetSelectAll = targetList.findComponent(TransferListHeader).findComponent(IxCheckbox) + const [appendTrigger, removeTrigger] = wrapper.findComponent(TransferOperations).findAllComponents(IxButton) + + await wrapper.setProps({ disabled: true }) + + await sourceSelectAll.find('input').setValue(true) + await appendTrigger.trigger('click') + + expect(onSelectAll).not.toBeCalled() + expect(targetList.findAllComponents(CheckableListItem).map(item => item.props().value)).toEqual([]) + + await sourceSelectAll.find('input').setValue(false) + await wrapper.setProps({ disabled: false }) + + await sourceSelectAll.find('input').setValue(true) + expect(onSelectAll).toBeCalledWith(true, true) + await sourceSelectAll.find('input').setValue(false) + expect(onSelectAll).toBeCalledWith(true, false) + await sourceSelectAll.find('input').setValue(true) + await appendTrigger.trigger('click') + + expect(sourceList.findAllComponents(CheckableListItem).map(item => item.props().value)).toEqual([1, 6, 12, 16]) + + onSelectAll.mockClear() + await wrapper.setProps({ disabled: true }) + + await targetSelectAll.find('input').setValue(true) + await removeTrigger.trigger('click') + + expect(onSelectAll).not.toBeCalled() + expect(sourceList.findAllComponents(CheckableListItem).map(item => item.props().value)).toEqual([1, 6, 12, 16]) + + await targetSelectAll.find('input').setValue(false) + await wrapper.setProps({ disabled: false }) + + await targetSelectAll.find('input').setValue(true) + expect(onSelectAll).toBeCalledWith(false, true) + await targetSelectAll.find('input').setValue(false) + expect(onSelectAll).toBeCalledWith(false, false) + await targetSelectAll.find('input').setValue(true) + await removeTrigger.trigger('click') + + expect(targetList.findAllComponents(CheckableListItem).map(item => item.props().value)).toEqual([]) + }) + + test('clear work', async () => { + const onClear = jest.fn() + const onChange = jest.fn() + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource, value: [2, 3, 4, 5], disabled: true, onClear, 'onUpdate:value': onChange }, + }) + const clearBtn = wrapper.find('.ix-transfer-list-header-clear-icon') + + await clearBtn.trigger('click') + expect(onClear).not.toBeCalled() + expect(onChange).not.toBeCalled() + expect(clearBtn.classes()).toContain('ix-transfer-list-header-clear-icon-disabled') + + await wrapper.setProps({ disabled: false }) + await clearBtn.trigger('click') + expect(onClear).toBeCalled() + expect(onChange).toBeCalledWith([]) + expect(clearBtn.classes()).not.toContain('ix-transfer-list-header-clear-icon-disabled') + }) + + test('clearable work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource, clearable: false }, + }) + + expect(wrapper.find('.ix-transfer-list-header-clear-icon').exists()).toBeFalsy() + + await wrapper.setProps({ clearable: true }) + + expect(wrapper.find('.ix-transfer-list-header-clear-icon').exists()).toBeTruthy() + }) + + test('clearIcon work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource, clearIcon: 'up' }, + }) + + expect(wrapper.find('.ix-transfer-list-header-clear-icon').find('.ix-icon-up').exists()).toBeTruthy() + }) + + test('pagination work', async () => { + const onPageChange = jest.fn() + const wrapper = TransferMount({ + props: { + dataSource: Array.from(new Array(50)).map((_, idx) => ({ + key: idx, + value: idx, + disabled: [1, 6, 12, 16].includes(idx), + })), + value: Array.from(new Array(25)).map((_, idx) => idx), + pagination: false, + }, + }) + + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + + expect(sourceList.findAllComponents(CheckableListItem).length).toBe(25) + expect(targetList.findAllComponents(CheckableListItem).length).toBe(25) + + await wrapper.setProps({ + pagination: { + pageIndex: [1, 1], + pageSize: [10, 10], + }, + }) + + expect(sourceList.findAllComponents(CheckableListItem).length).toBe(10) + expect(targetList.findAllComponents(CheckableListItem).length).toBe(10) + + await wrapper.setProps({ + pagination: { + pageIndex: [3, 3], + pageSize: [10, 10], + }, + }) + + expect(sourceList.findAllComponents(CheckableListItem).length).toBe(5) + expect(targetList.findAllComponents(CheckableListItem).length).toBe(5) + + await wrapper.setProps({ + pagination: { + pageIndex: [1, 1], + pageSize: [20, 20], + }, + }) + + expect(sourceList.findAllComponents(CheckableListItem).length).toBe(20) + expect(targetList.findAllComponents(CheckableListItem).length).toBe(20) + + await wrapper.setProps({ + pagination: { + pageIndex: [1, 1], + pageSize: [10, 10], + onChange: onPageChange, + }, + }) + + const [sourcePrev, , sourceNext] = sourceList.findAll('.ix-pagination-item') + const [targetPrev, , targetNext] = targetList.findAll('.ix-pagination-item') + await sourceNext.trigger('click') + expect(onPageChange).toBeCalledWith(true, 2, 10) + + await sourcePrev.trigger('click') + expect(onPageChange).toBeCalledWith(true, 1, 10) + + await targetNext.trigger('click') + expect(onPageChange).toBeCalledWith(false, 2, 10) + + await targetPrev.trigger('click') + expect(onPageChange).toBeCalledWith(false, 1, 10) + + onPageChange.mockClear() + await wrapper.setProps({ + pagination: { + pageIndex: [1, 1], + pageSize: [10, 10], + onChange: onPageChange, + }, + disabled: true, + }) + + await sourceNext.trigger('click') + await sourcePrev.trigger('click') + await targetNext.trigger('click') + await targetPrev.trigger('click') + expect(onPageChange).not.toBeCalled() + }) + + test('searchable work', async () => { + const onSearch = jest.fn() + const wrapper = TransferMount({ + props: { + dataSource: mockedDataSource, + value: [1, 2, 3, 4, 5], + searchable: true, + searchFn(isSource, item, searchValue) { + return `${item.value}`.indexOf(searchValue) > -1 + }, + onSearch, + }, + }) + + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + + const sourceSearchInput = sourceList.findComponent(ɵInput).find('input') + await sourceSearchInput.setValue('9') + await sourceSearchInput.trigger('keydown.enter') + expect(onSearch).toBeCalledWith(true, '9') + expect(sourceList.findAllComponents(CheckableListItem).length).toBe(2) + + const targetSearchInput = targetList.findComponent(ɵInput).find('input') + await targetSearchInput.setValue('2') + await targetSearchInput.trigger('keydown.enter') + expect(onSearch).toBeCalledWith(false, '2') + expect(targetList.findAllComponents(CheckableListItem).length).toBe(1) + }) + + test('transferBySelect work', async () => { + const onChange = jest.fn() + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource, mode: 'transferBySelect', 'onUpdate:value': onChange }, + }) + + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + const triggerSelect = (num: number, checked: boolean) => + sourceList + .findAllComponents(CheckableListItem) + .filter(item => item.props().value === num)[0] + .findComponent(IxCheckbox) + .find('input') + .setValue(checked) + + await triggerSelect(1, true) + expect(onChange).not.toBeCalled() + + await triggerSelect(2, true) + expect(onChange).toBeCalledWith([2]) + + await triggerSelect(3, true) + expect(onChange).toBeCalledWith([2, 3]) + + await triggerSelect(2, false) + expect(onChange).toBeCalledWith([3]) + + await targetList + .findAllComponents(CheckableListItem) + .filter(item => item.props().value === 3)[0] + .find('.ix-checkable-list-item-close-icon') + .trigger('click') + expect(onChange).toBeCalledWith([]) + }) + + test('spin work', async () => { + const wrapper = TransferMount({ props: { spin: true } }) + const [sourceList, targetList] = wrapper.findAllComponents(TransferList) + + expect(sourceList.findComponent(IxSpin).exists()).toBeTruthy() + expect(targetList.findComponent(IxSpin).exists()).toBeTruthy() + + await wrapper.setProps({ spin: { source: false, target: true } }) + + expect(sourceList.findComponent(IxSpin).props().spinning).toBe(false) + expect(targetList.findComponent(IxSpin).props().spinning).toBe(true) + + await wrapper.setProps({ spin: { source: false, target: false } }) + + expect(sourceList.findComponent(IxSpin).props().spinning).toBe(false) + expect(targetList.findComponent(IxSpin).props().spinning).toBe(false) + }) +}) diff --git a/packages/components/transfer/__tests__/transferSlots.spec.ts b/packages/components/transfer/__tests__/transferSlots.spec.ts new file mode 100644 index 000000000..53ed954e8 --- /dev/null +++ b/packages/components/transfer/__tests__/transferSlots.spec.ts @@ -0,0 +1,236 @@ +import { MountingOptions, mount } from '@vue/test-utils' +import { h } from 'vue' + +import CheckableListItem from '@idux/components/_private/checkable-list/src/CheckableListItem' + +import Transfer from '../src/Transfer' +import TransferList from '../src/list/TransferList' +import { TransferListSlotParams, TransferOperationsSlotParams, TransferProps } from '../src/types' + +const mockedDataSource = Array.from(new Array(20)).map((_, idx) => ({ + key: idx, + value: idx, + disabled: [1, 6, 12, 16].includes(idx), +})) + +describe('Transfer', () => { + const TransferMount = (options?: MountingOptions>) => + mount(Transfer, { ...(options as MountingOptions) }) + + const testSlot = async (slot: string) => { + const getCls = (isSource: boolean, cls: string) => `${isSource ? 'source' : 'target'}-${cls}` + const selectAll = true + let selectedKeys: number[] = [] + let searchValue = '' + const wrapper = TransferMount({ + props: { + dataSource: mockedDataSource, + searchable: true, + searchFn(isSource, item, searchValue) { + return `${item.value}`.indexOf(searchValue) > -1 + }, + }, + slots: { + [slot]: (params: TransferListSlotParams) => + h('div', [ + h('span', { class: getCls(params.isSource, 'data') }, params.data.length), + h('span', { class: getCls(params.isSource, 'filteredData') }, params.filteredData.length), + h('span', { class: getCls(params.isSource, 'paginatedData') }, params.paginatedData.length), + h('span', { class: getCls(params.isSource, 'dataSource') }, params.dataSource.length), + h('span', { class: getCls(params.isSource, 'dataKeyMap') }, params.dataKeyMap.size), + h('span', { class: getCls(params.isSource, 'filteredDataSource') }, params.filteredDataSource.length), + h('span', { class: getCls(params.isSource, 'paginatedDataSource') }, params.paginatedDataSource.length), + h('span', { class: getCls(params.isSource, 'targetKeySet') }, params.targetKeySet.size), + h('span', { class: getCls(params.isSource, 'selectedKeys') }, params.selectedKeys.length), + h('span', { class: getCls(params.isSource, 'selectedKeySet') }, params.selectedKeySet.size), + h('span', { class: getCls(params.isSource, 'disabledKeys') }, params.disabledKeys.size), + h('span', { class: getCls(params.isSource, 'selectAllDisabled') }, params.selectAllDisabled), + h( + 'span', + { class: getCls(params.isSource, 'selectAllStatus') }, + `${params.selectAllStatus.checked}-${params.selectAllStatus.indeterminate}`, + ), + h('span', { class: getCls(params.isSource, 'showSelectAll') }, params.showSelectAll), + h('span', { class: getCls(params.isSource, 'searchable') }, params.searchable), + h('span', { + class: getCls(params.isSource, 'handleSelectChange'), + onClick: () => params.handleSelectChange(selectedKeys), + }), + h('span', { class: getCls(params.isSource, 'selectAll'), onClick: () => params.selectAll(selectAll) }), + h('span', { class: getCls(params.isSource, 'searchValue') }, params.searchValue), + h('span', { + class: getCls(params.isSource, 'handleSearchChange'), + onClick: () => params.handleSearchChange(searchValue), + }), + ]), + operations: (params: TransferOperationsSlotParams) => + h('div', [ + h('span', { class: 'triggerAppend', onClick: () => params.triggerAppend() }), + h('span', { class: 'triggerRemove', onClick: () => params.triggerRemove() }), + h('span', { + class: 'triggerAppendAll', + onClick: () => params.triggerAppendAll(), + }), + h('span', { class: 'triggerClear', onClick: () => params.triggerClear() }), + ]), + }, + }) + + expect(wrapper.element).toMatchSnapshot() + + expect(wrapper.find('.source-data').text()).toBe('20') + expect(wrapper.find('.source-filteredData').text()).toBe('20') + expect(wrapper.find('.source-paginatedData').text()).toBe('20') + expect(wrapper.find('.source-dataSource').text()).toBe('20') + expect(wrapper.find('.source-dataKeyMap').text()).toBe('20') + expect(wrapper.find('.source-filteredDataSource').text()).toBe('20') + expect(wrapper.find('.source-paginatedDataSource').text()).toBe('20') + expect(wrapper.find('.source-targetKeySet').text()).toBe('0') + expect(wrapper.find('.source-selectedKeys').text()).toBe('0') + expect(wrapper.find('.source-selectedKeySet').text()).toBe('0') + expect(wrapper.find('.source-disabledKeys').text()).toBe('4') + expect(wrapper.find('.source-selectAllDisabled').text()).toBe('false') + expect(wrapper.find('.source-selectAllStatus').text()).toBe('false-false') + expect(wrapper.find('.source-showSelectAll').text()).toBe('true') + expect(wrapper.find('.source-searchable').text()).toBe('true') + expect(wrapper.find('.source-searchValue').text()).toBe('') + + expect(wrapper.find('.target-data').text()).toBe('0') + expect(wrapper.find('.target-filteredData').text()).toBe('0') + expect(wrapper.find('.target-paginatedData').text()).toBe('0') + expect(wrapper.find('.target-dataSource').text()).toBe('20') + expect(wrapper.find('.target-dataKeyMap').text()).toBe('20') + expect(wrapper.find('.target-filteredDataSource').text()).toBe('20') + expect(wrapper.find('.target-paginatedDataSource').text()).toBe('20') + expect(wrapper.find('.target-targetKeySet').text()).toBe('0') + expect(wrapper.find('.target-selectedKeys').text()).toBe('0') + expect(wrapper.find('.target-selectedKeySet').text()).toBe('0') + expect(wrapper.find('.target-disabledKeys').text()).toBe('0') + expect(wrapper.find('.target-selectAllDisabled').text()).toBe('true') + expect(wrapper.find('.target-selectAllStatus').text()).toBe('false-false') + expect(wrapper.find('.target-showSelectAll').text()).toBe('true') + expect(wrapper.find('.target-searchable').text()).toBe('true') + expect(wrapper.find('.target-searchValue').text()).toBe('') + + await wrapper.find('.source-selectAll').trigger('click') + expect(wrapper.find('.source-selectedKeys').text()).toBe('16') + expect(wrapper.find('.source-selectedKeySet').text()).toBe('16') + + selectedKeys = [7, 8, 9, 10, 11, 12] + await wrapper.find('.source-handleSelectChange').trigger('click') + expect(wrapper.find('.source-selectedKeys').text()).toBe('5') + expect(wrapper.find('.source-selectedKeySet').text()).toBe('5') + + await wrapper.find('.triggerAppend').trigger('click') + expect(wrapper.find('.source-selectedKeys').text()).toBe('0') + expect(wrapper.find('.source-selectedKeySet').text()).toBe('0') + expect(wrapper.find('.source-data').text()).toBe('15') + expect(wrapper.find('.target-data').text()).toBe('5') + + await wrapper.find('.triggerAppendAll').trigger('click') + expect(wrapper.find('.source-data').text()).toBe('4') + expect(wrapper.find('.target-data').text()).toBe('16') + + searchValue = '9' + await wrapper.find('.target-handleSearchChange').trigger('click') + expect(wrapper.find('.target-searchValue').text()).toBe('9') + expect(wrapper.find('.target-filteredData').text()).toBe('2') + + await wrapper.find('.target-selectAll').trigger('click') + expect(wrapper.find('.target-selectedKeys').text()).toBe('16') + expect(wrapper.find('.target-selectedKeySet').text()).toBe('16') + + selectedKeys = [7, 8, 9, 10, 11] + await wrapper.find('.target-handleSelectChange').trigger('click') + expect(wrapper.find('.target-selectedKeys').text()).toBe('5') + expect(wrapper.find('.target-selectedKeySet').text()).toBe('5') + + await wrapper.find('.triggerRemove').trigger('click') + expect(wrapper.find('.target-selectedKeys').text()).toBe('0') + expect(wrapper.find('.target-selectedKeySet').text()).toBe('0') + expect(wrapper.find('.source-data').text()).toBe('9') + expect(wrapper.find('.target-data').text()).toBe('11') + + await wrapper.find('.triggerClear').trigger('click') + expect(wrapper.find('.source-data').text()).toBe('20') + expect(wrapper.find('.target-data').text()).toBe('0') + + searchValue = '9' + await wrapper.find('.source-handleSearchChange').trigger('click') + expect(wrapper.find('.source-searchValue').text()).toBe('9') + expect(wrapper.find('.source-filteredData').text()).toBe('2') + } + + test('default slot work', async () => { + await testSlot('default') + }) + + test('footer slot work', async () => { + await testSlot('footer') + }) + + test('label slot work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource }, + slots: { + label: () => h('div', { class: 'custom-label-slot-test' }), + }, + }) + + expect(wrapper.element).toMatchSnapshot() + expect( + wrapper.findAllComponents(CheckableListItem).every(item => item.find('.custom-label-slot-test').exists()), + ).toBeTruthy() + }) + + test('headerLabel slot work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource }, + slots: { + headerLabel: ({ data, isSource }) => + h('div', { class: `custom-header-label-slot-test-${isSource ? 'source' : 'target'}` }, data.length), + }, + }) + + expect(wrapper.element).toMatchSnapshot() + expect(wrapper.find('.custom-header-label-slot-test-source').text()).toBe('20') + expect(wrapper.find('.custom-header-label-slot-test-target').text()).toBe('0') + }) + + test('headerSuffix slot work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource }, + slots: { + headerSuffix: ({ isSource }) => + h('div', { class: `custom-header-suffix-slot-test-${isSource ? 'source' : 'target'}` }), + }, + }) + + expect(wrapper.find('.custom-header-suffix-slot-test-source').exists()).toBeTruthy() + expect(wrapper.find('.custom-header-suffix-slot-test-target').exists()).toBeTruthy() + }) + + test('empty slot work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource }, + slots: { + empty: () => h('div', { class: 'custom-empty-slot-test' }), + }, + }) + + expect(wrapper.element).toMatchSnapshot() + expect(wrapper.findAllComponents(TransferList)[1].find('.custom-empty-slot-test').exists()).toBeTruthy() + }) + + test('clearIcon slot work', async () => { + const wrapper = TransferMount({ + props: { dataSource: mockedDataSource }, + slots: { + clearIcon: () => h('div', { class: 'custom-clear-icon-slot-test' }), + }, + }) + + expect(wrapper.element).toMatchSnapshot() + expect(wrapper.findAllComponents(TransferList)[1].find('.custom-clear-icon-slot-test').exists()).toBeTruthy() + }) +}) diff --git a/packages/components/transfer/demo/Basic.md b/packages/components/transfer/demo/Basic.md new file mode 100644 index 000000000..0ddf241aa --- /dev/null +++ b/packages/components/transfer/demo/Basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh: 基本使用 + en: Basic usage +--- + +## zh + +最简单的用法。 + +## en + +The simplest usage. diff --git a/packages/components/transfer/demo/Basic.vue b/packages/components/transfer/demo/Basic.vue new file mode 100644 index 000000000..f1b97b987 --- /dev/null +++ b/packages/components/transfer/demo/Basic.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/components/transfer/demo/CustomHeaderFooter.md b/packages/components/transfer/demo/CustomHeaderFooter.md new file mode 100644 index 000000000..36b0be390 --- /dev/null +++ b/packages/components/transfer/demo/CustomHeaderFooter.md @@ -0,0 +1,14 @@ +--- +order: 7 +title: + zh: 自定义header、footer + en: Custom header and footer +--- + +## zh + +通过 `headerLabel`, `headerSuffix` 和 `footer` 插槽自定义 header 和 footer。 + +## en + +Customize header and footer via `header` and `footer` slot. diff --git a/packages/components/transfer/demo/CustomHeaderFooter.vue b/packages/components/transfer/demo/CustomHeaderFooter.vue new file mode 100644 index 000000000..f8ccedb11 --- /dev/null +++ b/packages/components/transfer/demo/CustomHeaderFooter.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/components/transfer/demo/CustomLabel.md b/packages/components/transfer/demo/CustomLabel.md new file mode 100644 index 000000000..2464c212d --- /dev/null +++ b/packages/components/transfer/demo/CustomLabel.md @@ -0,0 +1,14 @@ +--- +order: 6 +title: + zh: 自定义label + en: Custom label +--- + +## zh + +通过 `label` 插槽自定义label显示。 + +## en + +Customize label via `label` slot. diff --git a/packages/components/transfer/demo/CustomLabel.vue b/packages/components/transfer/demo/CustomLabel.vue new file mode 100644 index 000000000..b2472b84a --- /dev/null +++ b/packages/components/transfer/demo/CustomLabel.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/components/transfer/demo/CustomListBody.md b/packages/components/transfer/demo/CustomListBody.md new file mode 100644 index 000000000..c07412332 --- /dev/null +++ b/packages/components/transfer/demo/CustomListBody.md @@ -0,0 +1,14 @@ +--- +order: 9 +title: + zh: 自定义列表内容 + en: Custom list body +--- + +## zh + +通过默认插槽自定义列表内容。 + +## en + +Customize list body via `default` slot. diff --git a/packages/components/transfer/demo/CustomListBody.vue b/packages/components/transfer/demo/CustomListBody.vue new file mode 100644 index 000000000..89239f6e2 --- /dev/null +++ b/packages/components/transfer/demo/CustomListBody.vue @@ -0,0 +1,92 @@ +