From d9e80c36d4a3a2c0fd30ce0eb2b8a7b079861208 Mon Sep 17 00:00:00 2001 From: ajuner <106791576@qq.com> Date: Wed, 30 Jun 2021 16:43:16 +0800 Subject: [PATCH 1/5] refactor(transfer): use composition api (#4135) * refactor(transfer): use composition api * fix: remove console --- components/style/themes/default.less | 3 + components/transfer/ListItem.tsx | 153 ++++--- components/transfer/index.tsx | 495 ++++++++++------------ components/transfer/list.tsx | 496 ++++++++++++++--------- components/transfer/operation.tsx | 31 +- components/transfer/renderListBody.tsx | 277 ++++++++----- components/transfer/search.tsx | 78 ++-- components/transfer/style/customize.less | 31 +- components/transfer/style/index.less | 184 +++++---- components/transfer/style/index.ts | 3 + components/transfer/style/rtl.less | 69 ++++ 11 files changed, 1044 insertions(+), 776 deletions(-) create mode 100644 components/transfer/style/rtl.less diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 0510b4a8b8..33940707f9 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -775,9 +775,12 @@ // Transfer // --- @transfer-header-height: 40px; +@transfer-item-height: @height-base; @transfer-disabled-bg: @disabled-bg; @transfer-list-height: 200px; @transfer-item-hover-bg: @item-hover-bg; +@transfer-item-padding-vertical: 6px; +@transfer-list-search-icon-top: 12px; // Message // --- diff --git a/components/transfer/ListItem.tsx b/components/transfer/ListItem.tsx index 66e03f0d90..6ef271f496 100644 --- a/components/transfer/ListItem.tsx +++ b/components/transfer/ListItem.tsx @@ -1,66 +1,117 @@ import PropTypes, { withUndefined } from '../_util/vue-types'; import classNames from '../_util/classNames'; +import { TransferLocale } from '.'; +import DeleteOutlined from '@ant-design/icons-vue/DeleteOutlined'; +import defaultLocale from '../locale/default'; import Lazyload from '../vc-lazy-load'; import Checkbox from '../checkbox'; -import { defineComponent } from 'vue'; +import TransButton from '../_util/transButton'; +import LocaleReceiver from '../locale-provider/LocaleReceiver'; +import { defineComponent, ExtractPropTypes } from 'vue'; function noop() {} +export const transferListItemProps = { + renderedText: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + renderedEl: PropTypes.any, + item: PropTypes.any, + lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])), + checked: PropTypes.looseBool, + prefixCls: PropTypes.string, + disabled: PropTypes.looseBool, + showRemove: PropTypes.looseBool, + onClick: PropTypes.func, + onRemove: PropTypes.func, +}; + +export type TransferListItemProps = Partial>; + export default defineComponent({ name: 'ListItem', inheritAttrs: false, - props: { - renderedText: PropTypes.any, - renderedEl: PropTypes.any, - item: PropTypes.any, - lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])), - checked: PropTypes.looseBool, - prefixCls: PropTypes.string, - disabled: PropTypes.looseBool, - onClick: PropTypes.func, - }, - render() { - const { renderedText, renderedEl, item, lazy, checked, disabled, prefixCls } = this.$props; + props: transferListItemProps, + emits: ['click'], + setup(props, { emit }) { + return () => { + const { + renderedText, + renderedEl, + item, + lazy, + checked, + disabled, + prefixCls, + showRemove, + onRemove, + } = props; + const className = classNames({ + [`${prefixCls}-content-item`]: true, + [`${prefixCls}-content-item-disabled`]: disabled || item.disabled, + }); - const className = classNames({ - [`${prefixCls}-content-item`]: true, - [`${prefixCls}-content-item-disabled`]: disabled || item.disabled, - }); + let title: string; + if (typeof renderedText === 'string' || typeof renderedText === 'number') { + title = String(renderedText); + } - let title; - if (typeof renderedText === 'string' || typeof renderedText === 'number') { - title = String(renderedText); - } + const listItem = ( + + {(transferLocale: TransferLocale) => { + const labelNode = {renderedEl}; + if (showRemove) { + return ( +
  • { + onRemove?.(item); + }} + > + {labelNode} + + + +
  • + ); + } - const listItem = ( -
  • { - this.$emit('click', item); - } - } - > - - {renderedEl} -
  • - ); - let children = null; - if (lazy) { - const lazyProps = { - height: 32, - offset: 500, - throttle: 0, - debounce: false, - ...(lazy as any), - }; - children = {listItem}; - } else { - children = listItem; - } - return children; + return ( +
  • { + emit('click', item); + } + } + > + + {labelNode} +
  • + ); + }} +
    + ); + let children = null; + if (lazy) { + const lazyProps = { + height: 32, + offset: 500, + throttle: 0, + debounce: false, + ...(lazy as any), + }; + children = {listItem}; + } else { + children = listItem; + } + return children; + }; }, }); diff --git a/components/transfer/index.tsx b/components/transfer/index.tsx index e1f9eded93..0278f29d4d 100644 --- a/components/transfer/index.tsx +++ b/components/transfer/index.tsx @@ -1,28 +1,28 @@ -import { defineComponent, inject } from 'vue'; +import { defineComponent, ExtractPropTypes, ref, watch } from 'vue'; import PropTypes from '../_util/vue-types'; -import { hasProp, getOptionProps, getComponent } from '../_util/props-util'; +import { getPropsSlot } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; -import BaseMixin from '../_util/BaseMixin'; import classNames from '../_util/classNames'; import List from './list'; import Operation from './operation'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; import defaultLocale from '../locale-provider/default'; -import { defaultConfigProvider, RenderEmptyHandler } from '../config-provider'; +import { RenderEmptyHandler } from '../config-provider'; import { withInstall } from '../_util/type'; +import useConfigInject from '../_util/hooks/useConfigInject'; export type TransferDirection = 'left' | 'right'; -export const TransferItem = { +export const transferItem = { key: PropTypes.string.isRequired, title: PropTypes.string.isRequired, description: PropTypes.string, disabled: PropTypes.looseBool, }; -export const TransferProps = { +export const transferProps = { prefixCls: PropTypes.string, - dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose), + dataSource: PropTypes.arrayOf(PropTypes.shape(transferItem).loose), disabled: PropTypes.looseBool, targetKeys: PropTypes.arrayOf(PropTypes.string), selectedKeys: PropTypes.arrayOf(PropTypes.string), @@ -39,164 +39,121 @@ export const TransferProps = { rowKey: PropTypes.func, lazy: PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool]), showSelectAll: PropTypes.looseBool, + selectAllLabels: PropTypes.any, children: PropTypes.any, + oneWay: PropTypes.looseBool, + pagination: PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool]), onChange: PropTypes.func, onSelectChange: PropTypes.func, onSearchChange: PropTypes.func, onSearch: PropTypes.func, onScroll: PropTypes.func, + ['onUpdate:selectedKeys']: PropTypes.func, + ['onUpdate:targetKeys']: PropTypes.func, }; +export type TransferProps = Partial>; + export interface TransferLocale { - titles: string[]; - notFoundContent: string; + titles: any[]; + notFoundContent?: any; searchPlaceholder: string; itemUnit: string; itemsUnit: string; + remove: string; + selectAll: string; + selectCurrent: string; + selectInvert: string; + removeAll: string; + removeCurrent: string; } const Transfer = defineComponent({ name: 'ATransfer', - mixins: [BaseMixin], inheritAttrs: false, - props: initDefaultProps(TransferProps, { + props: initDefaultProps(transferProps, { dataSource: [], locale: {}, showSearch: false, listStyle: () => {}, }), - setup() { - return { - separatedDataSource: null, - configProvider: inject('configProvider', defaultConfigProvider), - }; - }, - data() { - // vue 中 通过slot,不方便传递,保留notFoundContent及searchPlaceholder - // warning( - // !(getComponent(this, 'notFoundContent') || hasProp(this, 'searchPlaceholder')), - // 'Transfer[notFoundContent] and Transfer[searchPlaceholder] will be removed, ' + - // 'please use Transfer[locale] instead.', - // ) - const { selectedKeys = [], targetKeys = [] } = this; - return { - leftFilter: '', - rightFilter: '', - sourceSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) === -1), - targetSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) > -1), + emits: ['update:selectedKeys', 'update:targetKeys', 'change', 'search', 'scroll', 'selectChange'], + setup(props, { emit, attrs, slots, expose }) { + const { configProvider, prefixCls, direction } = useConfigInject('transfer', props); + const sourceSelectedKeys = ref( + props.selectedKeys?.filter((key) => props.targetKeys.indexOf(key) === -1) ?? [], + ); + const targetSelectedKeys = ref( + props.selectedKeys?.filter((key) => props.targetKeys.indexOf(key) > -1) ?? [], + ); + + watch( + () => props.selectedKeys, + () => { + sourceSelectedKeys.value = props.selectedKeys?.filter( + (key) => props.targetKeys.indexOf(key) === -1, + ); + targetSelectedKeys.value = props.selectedKeys?.filter( + (key) => props.targetKeys.indexOf(key) > -1, + ); + }, + ); + + const getTitles = (transferLocale: TransferLocale) => { + return props?.titles ?? (transferLocale.titles || ['', '']); }; - }, - watch: { - targetKeys() { - this.updateState(); - if (this.selectedKeys) { - const targetKeys = this.targetKeys || []; - this.setState({ - sourceSelectedKeys: this.selectedKeys.filter(key => !targetKeys.includes(key)), - targetSelectedKeys: this.selectedKeys.filter(key => targetKeys.includes(key)), - }); - } - }, - dataSource() { - this.updateState(); - }, - selectedKeys() { - if (this.selectedKeys) { - const targetKeys = this.targetKeys || []; - this.setState({ - sourceSelectedKeys: this.selectedKeys.filter(key => !targetKeys.includes(key)), - targetSelectedKeys: this.selectedKeys.filter(key => targetKeys.includes(key)), - }); - } - }, - }, - mounted() { - // this.currentProps = { ...this.$props } - }, - methods: { - getSelectedKeysName(direction) { - return direction === 'left' ? 'sourceSelectedKeys' : 'targetSelectedKeys'; - }, - - getTitles(transferLocale: TransferLocale) { - if (this.titles) { - return this.titles; - } - return transferLocale.titles || ['', '']; - }, - getLocale(transferLocale: TransferLocale, renderEmpty: RenderEmptyHandler) { + const getLocale = (transferLocale: TransferLocale, renderEmpty: RenderEmptyHandler) => { // Keep old locale props still working. const oldLocale: { notFoundContent?: any; searchPlaceholder?: string } = { notFoundContent: renderEmpty('Transfer'), }; - const notFoundContent = getComponent(this, 'notFoundContent'); + const notFoundContent = getPropsSlot(slots, props, 'notFoundContent'); if (notFoundContent) { oldLocale.notFoundContent = notFoundContent; } - if (hasProp(this, 'searchPlaceholder')) { - oldLocale.searchPlaceholder = this.$props.searchPlaceholder; + if ('searchPlaceholder' in props) { + oldLocale.searchPlaceholder = props.searchPlaceholder; } - return { ...transferLocale, ...oldLocale, ...this.$props.locale }; - }, - updateState() { - const { sourceSelectedKeys, targetSelectedKeys } = this; - this.separatedDataSource = null; - if (!this.selectedKeys) { - // clear key nolonger existed - // clear checkedKeys according to targetKeys - const { dataSource, targetKeys = [] } = this; - - const newSourceSelectedKeys = []; - const newTargetSelectedKeys = []; - dataSource.forEach(({ key }) => { - if (sourceSelectedKeys.includes(key) && !targetKeys.includes(key)) { - newSourceSelectedKeys.push(key); - } - if (targetSelectedKeys.includes(key) && targetKeys.includes(key)) { - newTargetSelectedKeys.push(key); - } - }); - this.setState({ - sourceSelectedKeys: newSourceSelectedKeys, - targetSelectedKeys: newTargetSelectedKeys, - }); - } - }, + return { ...transferLocale, ...oldLocale, ...props.locale }; + }; - moveTo(direction: TransferDirection) { - const { targetKeys = [], dataSource = [] } = this.$props; - const { sourceSelectedKeys, targetSelectedKeys } = this; - const moveKeys = direction === 'right' ? sourceSelectedKeys : targetSelectedKeys; + const moveTo = (direction: TransferDirection) => { + const { targetKeys = [], dataSource = [] } = props; + const moveKeys = direction === 'right' ? sourceSelectedKeys.value : targetSelectedKeys.value; // filter the disabled options const newMoveKeys = moveKeys.filter( - key => !dataSource.some(data => !!(key === data.key && data.disabled)), + (key) => !dataSource.some((data) => !!(key === data.key && data.disabled)), ); // move items to target box const newTargetKeys = direction === 'right' ? newMoveKeys.concat(targetKeys) - : targetKeys.filter(targetKey => newMoveKeys.indexOf(targetKey) === -1); + : targetKeys.filter((targetKey) => newMoveKeys.indexOf(targetKey) === -1); // empty checked keys const oppositeDirection = direction === 'right' ? 'left' : 'right'; - this.setState({ - [this.getSelectedKeysName(oppositeDirection)]: [], - }); - this.handleSelectChange(oppositeDirection, []); + direction === 'right' ? (sourceSelectedKeys.value = []) : (targetSelectedKeys.value = []); + emit('update:targetKeys', newTargetKeys); + handleSelectChange(oppositeDirection, []); + emit('change', newTargetKeys, direction, newMoveKeys); + }; - this.$emit('change', newTargetKeys, direction, newMoveKeys); - }, - moveToLeft() { - this.moveTo('left'); - }, - moveToRight() { - this.moveTo('right'); - }, + const moveToLeft = () => { + moveTo('left'); + }; + const moveToRight = () => { + moveTo('right'); + }; - onItemSelectAll(direction: TransferDirection, selectedKeys: string[], checkAll: boolean) { - const originalSelectedKeys = this.$data[this.getSelectedKeysName(direction)] || []; + const onItemSelectAll = ( + direction: TransferDirection, + selectedKeys: string[], + checkAll: boolean, + ) => { + const originalSelectedKeys = + (direction === 'left' ? sourceSelectedKeys.value : targetSelectedKeys.value) || []; let mergedCheckedKeys = []; if (checkAll) { @@ -204,78 +161,59 @@ const Transfer = defineComponent({ mergedCheckedKeys = Array.from(new Set([...originalSelectedKeys, ...selectedKeys])); } else { // Remove current keys from origin keys - mergedCheckedKeys = originalSelectedKeys.filter(key => selectedKeys.indexOf(key) === -1); + mergedCheckedKeys = originalSelectedKeys.filter((key) => selectedKeys.indexOf(key) === -1); } - this.handleSelectChange(direction, mergedCheckedKeys); + handleSelectChange(direction, mergedCheckedKeys); + }; + + const onLeftItemSelectAll = (selectedKeys: string[], checkAll: boolean) => { + return onItemSelectAll('left', selectedKeys, checkAll); + }; + + const onRightItemSelectAll = (selectedKeys: string[], checkAll: boolean) => { + return onItemSelectAll('right', selectedKeys, checkAll); + }; - if (!this.$props.selectedKeys) { - this.setState({ - [this.getSelectedKeysName(direction)]: mergedCheckedKeys, - }); + const handleSelectChange = (direction: TransferDirection, holder: string[]) => { + if (direction === 'left') { + sourceSelectedKeys.value = holder; + emit('update:selectedKeys', [...targetSelectedKeys.value, ...holder]); + emit('selectChange', holder, targetSelectedKeys); + } else { + targetSelectedKeys.value = holder; + emit('update:selectedKeys', [...sourceSelectedKeys.value, ...holder]); + emit('selectChange', sourceSelectedKeys, holder); } - }, + }; - handleSelectAll(direction, filteredDataSource, checkAll) { - this.onItemSelectAll( - direction, - filteredDataSource.map(({ key }) => key), - !checkAll, - ); - }, + const handleFilter = (direction: TransferDirection, e) => { + const value = e.target.value; + emit('search', direction, value); + }; - // [Legacy] Old prop `body` pass origin check as arg. It's confusing. - // TODO: Remove this in next version. - handleLeftSelectAll(filteredDataSource, checkAll) { - return this.handleSelectAll('left', filteredDataSource, !checkAll); - }, + const handleLeftFilter = (e: Event) => { + handleFilter('left', e); + }; + const handleRightFilter = (e: Event) => { + handleFilter('right', e); + }; - handleRightSelectAll(filteredDataSource, checkAll) { - return this.handleSelectAll('right', filteredDataSource, !checkAll); - }, + const handleClear = (direction: TransferDirection) => { + emit('search', direction, ''); + }; - onLeftItemSelectAll(selectedKeys, checkAll) { - return this.onItemSelectAll('left', selectedKeys, checkAll); - }, + const handleLeftClear = () => { + handleClear('left'); + }; - onRightItemSelectAll(selectedKeys, checkAll) { - return this.onItemSelectAll('right', selectedKeys, checkAll); - }, + const handleRightClear = () => { + handleClear('right'); + }; - handleFilter(direction, e) { - const value = e.target.value; - // if (getListeners(this).searchChange) { - // warning( - // false, - // 'Transfer', - // '`searchChange` in Transfer is deprecated. Please use `search` instead.', - // ); - // this.$emit('searchChange', direction, e); - // } - this.$emit('search', direction, value); - }, - - handleLeftFilter(e) { - this.handleFilter('left', e); - }, - handleRightFilter(e) { - this.handleFilter('right', e); - }, - - handleClear(direction) { - this.$emit('search', direction, ''); - }, - - handleLeftClear() { - this.handleClear('left'); - }, - handleRightClear() { - this.handleClear('right'); - }, - - onItemSelect(direction, selectedKey, checked) { - const { sourceSelectedKeys, targetSelectedKeys } = this; - const holder = direction === 'left' ? [...sourceSelectedKeys] : [...targetSelectedKeys]; + const onItemSelect = (direction: TransferDirection, selectedKey: string, checked: boolean) => { + const holder = + direction === 'left' ? [...sourceSelectedKeys.value] : [...targetSelectedKeys.value]; const index = holder.indexOf(selectedKey); if (index > -1) { holder.splice(index, 1); @@ -283,68 +221,45 @@ const Transfer = defineComponent({ if (checked) { holder.push(selectedKey); } - this.handleSelectChange(direction, holder); + handleSelectChange(direction, holder); + }; - if (!this.selectedKeys) { - this.setState({ - [this.getSelectedKeysName(direction)]: holder, - }); - } - }, - - // handleSelect(direction, selectedItem, checked) { - // warning(false, 'Transfer', '`handleSelect` will be removed, please use `onSelect` instead.'); - // this.onItemSelect(direction, selectedItem.key, checked); - // }, - - // handleLeftSelect(selectedItem, checked) { - // return this.handleSelect('left', selectedItem, checked); - // }, - - // handleRightSelect(selectedItem, checked) { - // return this.handleSelect('right', selectedItem, checked); - // }, - - onLeftItemSelect(selectedKey, checked) { - return this.onItemSelect('left', selectedKey, checked); - }, - onRightItemSelect(selectedKey, checked) { - return this.onItemSelect('right', selectedKey, checked); - }, - - handleScroll(direction, e) { - this.$emit('scroll', direction, e); - }, - - handleLeftScroll(e) { - this.handleScroll('left', e); - }, - handleRightScroll(e) { - this.handleScroll('right', e); - }, - - handleSelectChange(direction: TransferDirection, holder: string[]) { - const { sourceSelectedKeys, targetSelectedKeys } = this; + const onLeftItemSelect = (selectedKey: string, checked: boolean) => { + return onItemSelect('left', selectedKey, checked); + }; + const onRightItemSelect = (selectedKey: string, checked: boolean) => { + return onItemSelect('right', selectedKey, checked); + }; + const onRightItemRemove = (targetedKeys: string[]) => { + const { targetKeys = [] } = props; + const newTargetKeys = targetKeys.filter((key) => !targetedKeys.includes(key)); + emit('update:targetKeys', newTargetKeys); + emit('change', newTargetKeys, 'left', [...targetedKeys]); + }; - if (direction === 'left') { - this.$emit('selectChange', holder, targetSelectedKeys); - } else { - this.$emit('selectChange', sourceSelectedKeys, holder); - } - }, - handleListStyle(listStyle, direction) { + const handleScroll = (direction: TransferDirection, e: Event) => { + emit('scroll', direction, e); + }; + + const handleLeftScroll = (e: Event) => { + handleScroll('left', e); + }; + const handleRightScroll = (e: Event) => { + handleScroll('right', e); + }; + const handleListStyle = (listStyle, direction) => { if (typeof listStyle === 'function') { return listStyle({ direction }); } return listStyle; - }, + }; - separateDataSource() { - const { dataSource, rowKey, targetKeys = [] } = this.$props; + const separateDataSource = () => { + const { dataSource, rowKey, targetKeys = [] } = props; const leftDataSource = []; const rightDataSource = new Array(targetKeys.length); - dataSource.forEach(record => { + dataSource.forEach((record) => { if (rowKey) { record.key = rowKey(record); } @@ -363,12 +278,12 @@ const Transfer = defineComponent({ leftDataSource, rightDataSource, }; - }, + }; + + expose({ handleSelectChange }); - renderTransfer(transferLocale: TransferLocale) { - const props = getOptionProps(this); + const renderTransfer = (transferLocale: TransferLocale) => { const { - prefixCls: customizePrefixCls, disabled, operations = [], showSearch, @@ -377,108 +292,106 @@ const Transfer = defineComponent({ filterOption, lazy, showSelectAll, + selectAllLabels = [], + oneWay, + pagination, } = props; - const { class: className, style } = this.$attrs; - const children = getComponent(this, 'children', {}, false); - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('transfer', customizePrefixCls); - - const renderEmpty = this.configProvider.renderEmpty; - const locale = this.getLocale(transferLocale, renderEmpty); - const { sourceSelectedKeys, targetSelectedKeys, $slots } = this; - const { body, footer } = $slots; - const renderItem = props.render || this.$slots.render; - const { leftDataSource, rightDataSource } = this.separateDataSource(); - const leftActive = targetSelectedKeys.length > 0; - const rightActive = sourceSelectedKeys.length > 0; - - const cls = classNames(prefixCls, className, { - [`${prefixCls}-disabled`]: disabled, - [`${prefixCls}-customize-list`]: !!children, + const { class: className, style } = attrs; + + const children = slots.children; + const mergedPagination = !children && pagination; + + const renderEmpty = configProvider.renderEmpty; + const locale = getLocale(transferLocale, renderEmpty); + const { body, footer } = slots; + const renderItem = props.render || slots.render; + const { leftDataSource, rightDataSource } = separateDataSource(); + const leftActive = targetSelectedKeys.value.length > 0; + const rightActive = sourceSelectedKeys.value.length > 0; + + const cls = classNames(prefixCls.value, className, { + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-customize-list`]: !!children, }); - const titles = this.getTitles(locale); + const titles = getTitles(locale); + return (
    ); - }, - }, - render() { - return ( + }; + return () => ( ); }, diff --git a/components/transfer/list.tsx b/components/transfer/list.tsx index 269f34cdaf..38b2a3362d 100644 --- a/components/transfer/list.tsx +++ b/components/transfer/list.tsx @@ -1,18 +1,20 @@ import classNames from '../_util/classNames'; import PropTypes, { withUndefined } from '../_util/vue-types'; -import { isValidElement, splitAttrs, findDOMNode, filterEmpty } from '../_util/props-util'; +import { isValidElement, splitAttrs, filterEmpty } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; -import BaseMixin from '../_util/BaseMixin'; +import DownOutlined from '@ant-design/icons-vue/DownOutlined'; import Checkbox from '../checkbox'; +import Menu from '../menu'; +import Dropdown from '../dropdown'; import Search from './search'; import defaultRenderList from './renderListBody'; import triggerEvent from '../_util/triggerEvent'; -import { defineComponent, nextTick, VNode, VNodeTypes } from 'vue'; +import { defineComponent, ExtractPropTypes, onBeforeUnmount, ref, VNode, VNodeTypes } from 'vue'; import { RadioChangeEvent } from '../radio/interface'; const defaultRender = () => null; -const TransferItem = { +const transferItem = { key: PropTypes.string.isRequired, title: PropTypes.string.isRequired, description: PropTypes.string, @@ -34,16 +36,18 @@ function isRenderResultPlainObject(result: VNode) { ); } -export const TransferListProps = { +function getEnabledItemKeys(items: RecordType[]) { + return items.filter((data) => !data.disabled).map((data) => data.key); +} + +export const transferListProps = { prefixCls: PropTypes.string, titleText: PropTypes.string, - dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose), + dataSource: PropTypes.arrayOf(PropTypes.shape(transferItem).loose), filter: PropTypes.string, filterOption: PropTypes.func, checkedKeys: PropTypes.arrayOf(PropTypes.string), handleFilter: PropTypes.func, - handleSelect: PropTypes.func, - handleSelectAll: PropTypes.func, handleClear: PropTypes.func, renderItem: PropTypes.func, showSearch: PropTypes.looseBool, @@ -58,83 +62,76 @@ export const TransferListProps = { disabled: PropTypes.looseBool, direction: PropTypes.string, showSelectAll: PropTypes.looseBool, + titles: PropTypes.any, + remove: PropTypes.string, + selectAll: PropTypes.string, + selectCurrent: PropTypes.string, + selectInvert: PropTypes.string, + removeAll: PropTypes.string, + removeCurrent: PropTypes.string, + selectAllLabel: PropTypes.any, + showRemove: PropTypes.looseBool, + pagination: PropTypes.any, onItemSelect: PropTypes.func, onItemSelectAll: PropTypes.func, + onItemRemove: PropTypes.func, onScroll: PropTypes.func, }; -function renderListNode(renderList: Function, props: any) { - let bodyContent = renderList ? renderList(props) : null; - const customize = !!bodyContent && filterEmpty(bodyContent).length > 0; - if (!customize) { - bodyContent = defaultRenderList(props); - } - return { - customize, - bodyContent, - }; -} +export type TransferListProps = Partial>; export default defineComponent({ name: 'TransferList', - mixins: [BaseMixin], inheritAttrs: false, - props: initDefaultProps(TransferListProps, { + props: initDefaultProps(transferListProps, { dataSource: [], titleText: '', showSearch: false, lazy: {}, }), - setup() { - return { - timer: null, - triggerScrollTimer: null, - scrollEvent: null, - }; - }, - data() { - return { - filterValue: '', - }; - }, - beforeUnmount() { - clearTimeout(this.triggerScrollTimer); - // if (this.scrollEvent) { - // this.scrollEvent.remove(); - // } - }, - updated() { - nextTick(() => { - if (this.scrollEvent) { - this.scrollEvent.remove(); - } + emits: ['scroll', 'itemSelectAll'], + setup(props, { emit, attrs }) { + const filterValue = ref(''); + const transferNode = ref(); + const defaultListBodyRef = ref(); + const triggerScrollTimer = ref(); + onBeforeUnmount(() => { + window.clearTimeout(triggerScrollTimer.value); }); - }, - methods: { - handleScroll(e: Event) { - this.$emit('scroll', e); - }, - getCheckStatus(filteredItems: DataSourceItem[]) { - const { checkedKeys } = this.$props; + + const renderListNode = (renderList: any, props: any) => { + let bodyContent = renderList ? renderList(props) : null; + const customize = !!bodyContent && filterEmpty(bodyContent).length > 0; + if (!customize) { + bodyContent = defaultRenderList(props, defaultListBodyRef); + } + return { + customize, + bodyContent, + }; + }; + + const getCheckStatus = (filteredItems: DataSourceItem[]) => { + const { checkedKeys } = props; if (checkedKeys.length === 0) { return 'none'; } - if (filteredItems.every(item => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)) { + if (filteredItems.every((item) => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)) { return 'all'; } return 'part'; - }, + }; - getFilteredItems(dataSource: DataSourceItem[], filterValue: string) { + const getFilteredItems = (dataSource: DataSourceItem[], filterValue: string) => { const filteredItems = []; const filteredRenderItems = []; - dataSource.forEach(item => { - const renderedItem = this.renderItemHtml(item); + dataSource.forEach((item) => { + const renderedItem = renderItemHtml(item); const { renderedText } = renderedItem; // Filter skip - if (filterValue && filterValue.trim() && !this.matchFilter(renderedText, item)) { + if (filterValue && filterValue.trim() && !matchFilter(renderedText, item)) { return null; } @@ -143,9 +140,92 @@ export default defineComponent({ }); return { filteredItems, filteredRenderItems }; - }, + }; + + const getCheckBox = ( + filteredItems: DataSourceItem[], + showSelectAll: boolean, + disabled?: boolean, + prefixCls?: string, + ) => { + const checkStatus = getCheckStatus(filteredItems); + const checkedAll = checkStatus === 'all'; + const checkAllCheckbox = showSelectAll !== false && ( + { + // Only select enabled items + emit( + 'itemSelectAll', + filteredItems.filter((item) => !item.disabled).map(({ key }) => key), + !checkedAll, + ); + }} + /> + ); - getListBody( + return checkAllCheckbox; + }; + + const _handleFilter = (e: RadioChangeEvent) => { + const { handleFilter } = props; + const { + target: { value: filter }, + } = e; + handleFilter(e); + filterValue.value = filter; + if (!filter) { + return; + } + // Manually trigger scroll event for lazy search bug + // https://github.com/ant-design/ant-design/issues/5631 + triggerScrollTimer.value = window.setTimeout(() => { + const listNode = transferNode.value.querySelectorAll('.ant-transfer-list-content')[0]; + if (listNode) { + triggerEvent(listNode, 'scroll'); + } + }, 0); + }; + const _handleClear = (e: Event) => { + filterValue.value = ''; + props?.handleClear(e); + }; + const matchFilter = (text: string, item: DataSourceItem) => { + const { filterOption } = props; + if (filterOption) { + return filterOption(filterValue.value, item); + } + return text.indexOf(filterValue.value) >= 0; + }; + const renderItemHtml = (item: DataSourceItem) => { + const { renderItem = defaultRender } = props; + const renderResult = renderItem(item); + const isRenderResultPlain = isRenderResultPlainObject(renderResult); + return { + renderedText: isRenderResultPlain ? renderResult.value : renderResult, + renderedEl: isRenderResultPlain ? renderResult.label : renderResult, + item, + }; + }; + const getSelectAllLabel = (selectedCount: number, totalCount: number) => { + const { itemsUnit, itemUnit, selectAllLabel } = props; + if (selectAllLabel) { + return typeof selectAllLabel === 'function' + ? selectAllLabel({ selectedCount, totalCount }) + : selectAllLabel; + } + const unit = totalCount > 1 ? itemsUnit : itemUnit; + return ( + <> + {(selectedCount > 0 ? `${selectedCount}/` : '') + totalCount} {unit} + + ); + }; + + const getListBody = ( prefixCls: string, searchPlaceholder: string, filterValue: string, @@ -157,13 +237,13 @@ export default defineComponent({ renderList: Function, showSearch: boolean, disabled: boolean, - ) { + ) => { const search = showSearch ? (
    {search} {bodyNode} @@ -206,152 +287,173 @@ export default defineComponent({ ); } return listBody; - }, + }; + return () => { + const { + prefixCls, + dataSource, + titleText, + checkedKeys, + disabled, + body, + footer, + showSearch, + searchPlaceholder, + notFoundContent, + selectAll, + selectCurrent, + selectInvert, + removeAll, + removeCurrent, + renderList, + onItemSelectAll, + onItemRemove, + showSelectAll, + showRemove, + pagination, + } = props; - getCheckBox(filteredItems: DataSourceItem[], showSelectAll: boolean, disabled: boolean) { - const checkStatus = this.getCheckStatus(filteredItems); - const checkedAll = checkStatus === 'all'; - const checkAllCheckbox = showSelectAll !== false && ( - { - // Only select enabled items - this.$emit( - 'itemSelectAll', - filteredItems.filter(item => !item.disabled).map(({ key }) => key), - !checkedAll, - ); - }} - /> - ); + // Custom Layout + const footerDom = footer && footer({ ...props }); + const bodyDom = body && body({ ...props }); - return checkAllCheckbox; - }, - - _handleSelect(selectedItem: DataSourceItem) { - const { checkedKeys } = this.$props; - const result = checkedKeys.some(key => key === selectedItem.key); - this.handleSelect(selectedItem, !result); - }, - _handleFilter(e: RadioChangeEvent) { - const { handleFilter } = this.$props; - const { - target: { value: filterValue }, - } = e; - this.setState({ filterValue }); - handleFilter(e); - if (!filterValue) { - return; - } - // Manually trigger scroll event for lazy search bug - // https://github.com/ant-design/ant-design/issues/5631 - this.triggerScrollTimer = setTimeout(() => { - const transferNode = findDOMNode(this); - const listNode = transferNode.querySelectorAll('.ant-transfer-list-content')[0]; - if (listNode) { - triggerEvent(listNode, 'scroll'); - } - }, 0); - }, - _handleClear(e: Event) { - this.setState({ filterValue: '' }); - this.handleClear(e); - }, - matchFilter(text: string, item: DataSourceItem) { - const { filterValue } = this.$data; - const { filterOption } = this.$props; - if (filterOption) { - return filterOption(filterValue, item); - } - return text.indexOf(filterValue) >= 0; - }, - renderItemHtml(item: DataSourceItem) { - const { renderItem = defaultRender } = this.$props; - const renderResult = renderItem(item); - const isRenderResultPlain = isRenderResultPlainObject(renderResult); - return { - renderedText: isRenderResultPlain ? renderResult.value : renderResult, - renderedEl: isRenderResultPlain ? renderResult.label : renderResult, - item, - }; - }, - filterNull(arr: unknown[]) { - return arr.filter(item => { - return item !== null; + const listCls = classNames(prefixCls, { + [`${prefixCls}-with-pagination`]: !!pagination, + [`${prefixCls}-with-footer`]: !!footerDom, }); - }, - }, - render() { - const { filterValue } = this.$data; - const { - prefixCls, - dataSource, - titleText, - checkedKeys, - disabled, - body, - footer, - showSearch, - searchPlaceholder, - notFoundContent, - itemUnit, - itemsUnit, - renderList, - showSelectAll, - } = this.$props; - - // Custom Layout - const footerDom = footer && footer({ ...this.$props }); - const bodyDom = body && body({ ...this.$props }); - - const listCls = classNames(prefixCls, { - [`${prefixCls}-with-footer`]: !!footerDom, - }); + // ====================== Get filtered, checked item list ====================== - // ====================== Get filtered, checked item list ====================== + const { filteredItems, filteredRenderItems } = getFilteredItems( + dataSource, + filterValue.value, + ); - const { filteredItems, filteredRenderItems } = this.getFilteredItems(dataSource, filterValue); + // ================================= List Body ================================= - // ================================= List Body ================================= + const listBody = getListBody( + prefixCls, + searchPlaceholder, + filterValue.value, + filteredItems, + notFoundContent, + bodyDom, + filteredRenderItems, + checkedKeys, + renderList, + showSearch, + disabled, + ); + + const listFooter = footerDom ?
    {footerDom}
    : null; + + const checkAllCheckbox = + !showRemove && + !pagination && + getCheckBox(filteredItems, showSelectAll, disabled, prefixCls); + + let menu = null; + if (showRemove) { + menu = ( + + {/* Remove Current Page */} + {pagination && ( + { + const pageKeys = getEnabledItemKeys( + (defaultListBodyRef.value.getItems?.() || []).map((entity) => entity.item), + ); + onItemRemove?.(pageKeys); + }} + > + {removeCurrent} + + )} + + {/* Remove All */} + { + onItemRemove?.(getEnabledItemKeys(filteredItems)); + }} + > + {removeAll} + + + ); + } else { + menu = ( + + { + const keys = getEnabledItemKeys(filteredItems); + onItemSelectAll(keys, keys.length !== checkedKeys.length); + }} + > + {selectAll} + + {pagination && ( + { + const pageItems = defaultListBodyRef.value?.getItems() || []; + onItemSelectAll(getEnabledItemKeys(pageItems.map((entity) => entity.item)), true); + }} + > + {selectCurrent} + + )} + { + let availableKeys: string[]; + if (pagination) { + availableKeys = getEnabledItemKeys( + (defaultListBodyRef.value?.getItems() || []).map((entity) => entity.item), + ); + } else { + availableKeys = getEnabledItemKeys(filteredItems); + } - const unit = dataSource.length > 1 ? itemsUnit : itemUnit; + const checkedKeySet = new Set(checkedKeys); + const newCheckedKeys: string[] = []; + const newUnCheckedKeys: string[] = []; - const listBody = this.getListBody( - prefixCls, - searchPlaceholder, - filterValue, - filteredItems, - notFoundContent, - bodyDom, - filteredRenderItems, - checkedKeys, - renderList, - showSearch, - disabled, - ); + availableKeys.forEach((key) => { + if (checkedKeySet.has(key)) { + newUnCheckedKeys.push(key); + } else { + newCheckedKeys.push(key); + } + }); - const listFooter = footerDom ?
    {footerDom}
    : null; + onItemSelectAll(newCheckedKeys, true); + onItemSelectAll(newUnCheckedKeys, false); + }} + > + {selectInvert} +
    +
    + ); + } - const checkAllCheckbox = this.getCheckBox(filteredItems, showSelectAll, disabled); + const dropdown = ( + + + + ); - return ( -
    -
    - {checkAllCheckbox} - - - {(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + filteredItems.length}{' '} - {unit} + return ( +
    +
    + {checkAllCheckbox} + {dropdown} + + {getSelectAllLabel(checkedKeys.length, filteredItems.length)} + {titleText} - {titleText} - +
    + {listBody} + {listFooter}
    - {listBody} - {listFooter} -
    - ); + ); + }; }, }); diff --git a/components/transfer/operation.tsx b/components/transfer/operation.tsx index 0c73ec75b0..17cd765547 100644 --- a/components/transfer/operation.tsx +++ b/components/transfer/operation.tsx @@ -2,11 +2,12 @@ import { CSSProperties, FunctionalComponent } from 'vue'; import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; import RightOutlined from '@ant-design/icons-vue/RightOutlined'; import Button from '../button'; +import { Direction } from '../config-provider'; function noop() {} export interface TransferOperationProps { - class?: any; + class?: string; leftArrowText?: string; rightArrowText?: string; moveToLeft?: (e: MouseEvent) => void; @@ -15,9 +16,11 @@ export interface TransferOperationProps { rightActive?: boolean; style?: CSSProperties | string; disabled?: boolean; + direction?: Direction; + oneWay?: boolean; } -const Operation: FunctionalComponent = props => { +const Operation: FunctionalComponent = (props) => { const { disabled, moveToLeft = noop, @@ -28,6 +31,8 @@ const Operation: FunctionalComponent = props => { rightActive, class: className, style, + direction, + oneWay, } = props; return ( @@ -37,19 +42,21 @@ const Operation: FunctionalComponent = props => { size="small" disabled={disabled || !rightActive} onClick={moveToRight} - icon={} + icon={direction !== 'rtl' ? : } > {rightArrowText} - + {!oneWay && ( + + )}
    ); }; diff --git a/components/transfer/renderListBody.tsx b/components/transfer/renderListBody.tsx index c73968e620..acc036d5b6 100644 --- a/components/transfer/renderListBody.tsx +++ b/components/transfer/renderListBody.tsx @@ -1,113 +1,200 @@ -import { defineComponent, nextTick } from 'vue'; +import { + defineComponent, + ExtractPropTypes, + nextTick, + computed, + ref, + watch, + onMounted, + onBeforeMount, +} from 'vue'; +import classNames from '../_util/classNames'; import raf from '../_util/raf'; import ListItem from './ListItem'; +import Pagination from '../pagination'; import PropTypes, { withUndefined } from '../_util/vue-types'; -import { findDOMNode } from '../_util/props-util'; -import { getTransitionGroupProps, TransitionGroup } from '../_util/transition'; import { DataSourceItem } from './list'; + +export const transferListBodyProps = { + prefixCls: PropTypes.string, + filteredRenderItems: PropTypes.array.def([]), + lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])), + selectedKeys: PropTypes.array, + disabled: PropTypes.looseBool, + showRemove: PropTypes.looseBool, + pagination: PropTypes.any, + onItemSelect: PropTypes.func, + onItemSelectAll: PropTypes.func, + onScroll: PropTypes.func, + onItemRemove: PropTypes.func, +}; + +export type TransferListBodyProps = Partial>; + +function parsePagination(pagination) { + if (!pagination) { + return null; + } + + const defaultPagination = { + pageSize: 10, + }; + + if (typeof pagination === 'object') { + return { + ...defaultPagination, + ...pagination, + }; + } + + return defaultPagination; +} + const ListBody = defineComponent({ name: 'ListBody', inheritAttrs: false, - props: { - prefixCls: PropTypes.string, - filteredRenderItems: PropTypes.array.def([]), - lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])), - selectedKeys: PropTypes.array, - disabled: PropTypes.looseBool, - onItemSelect: PropTypes.func, - onItemSelectAll: PropTypes.func, - onScroll: PropTypes.func, - }, - setup() { - return { - mountId: null, - lazyId: null, + props: transferListBodyProps, + emits: ['itemSelect', 'itemRemove', 'scroll'], + setup(props, { emit, expose }) { + const mounted = ref(false); + const mountId = ref(null); + const lazyId = ref(null); + const container = ref(null); + const current = ref(1); + const itemsLength = computed(() => { + return props.filteredRenderItems ? props.filteredRenderItems.length : 0; + }); + + const handleItemSelect = (item: DataSourceItem) => { + const { selectedKeys } = props; + const checked = selectedKeys.indexOf(item.key) >= 0; + emit('itemSelect', item.key, !checked); }; - }, - data() { - return { - mounted: false, + + const handleItemRemove = (item: DataSourceItem) => { + emit('itemRemove', item.key); }; - }, - computed: { - itemsLength(): number { - return this.filteredRenderItems ? this.filteredRenderItems.length : 0; - }, - }, - watch: { - itemsLength() { - nextTick(() => { - const { lazy } = this.$props; - if (lazy !== false) { - const container = findDOMNode(this); - raf.cancel(this.lazyId); - this.lazyId = raf(() => { - if (container) { - const scrollEvent = new Event('scroll', { bubbles: true }); - container.dispatchEvent(scrollEvent); - } - }); - } + + const handleScroll = (e: Event) => { + emit('scroll', e); + }; + + const getItems = () => { + const { pagination, filteredRenderItems } = props; + + const mergedPagination = parsePagination(pagination); + + let displayItems = filteredRenderItems; + + if (mergedPagination) { + displayItems = filteredRenderItems.slice( + (current.value - 1) * mergedPagination.pageSize, + current.value * mergedPagination.pageSize, + ); + } + + return displayItems; + }; + + const onPageChange = (cur: number) => { + current.value = cur; + }; + + expose({ getItems }); + + onMounted(() => { + mountId.value = raf(() => { + mounted.value = true; }); - }, - }, - mounted() { - this.mountId = raf(() => { - this.mounted = true; }); - }, - - beforeUnmount() { - raf.cancel(this.mountId); - raf.cancel(this.lazyId); - }, - methods: { - handleItemSelect(item: DataSourceItem) { - const { selectedKeys } = this.$props; - const checked = selectedKeys.indexOf(item.key) >= 0; - this.$emit('itemSelect', item.key, !checked); - }, - handleScroll(e: Event) { - this.$emit('scroll', e); - }, - }, - render() { - const { mounted } = this.$data; - const { - prefixCls, - filteredRenderItems, - lazy, - selectedKeys, - disabled: globalDisabled, - } = this.$props; - const items = filteredRenderItems.map(({ renderedEl, renderedText, item }: any) => { - const { disabled } = item; - const checked = selectedKeys.indexOf(item.key) >= 0; - return ( - - ); + onBeforeMount(() => { + raf.cancel(mountId.value); + raf.cancel(lazyId.value); }); - const transitionProps = getTransitionGroupProps( - mounted ? `${prefixCls}-content-item-highlight` : '', - { - tag: 'ul', - class: `${prefixCls}-content`, - onScroll: this.handleScroll, + + watch( + () => itemsLength.value, + () => { + nextTick(() => { + const { lazy } = props; + if (lazy !== false) { + raf.cancel(lazyId); + lazyId.value = raf(() => { + if (container) { + const scrollEvent = new Event('scroll', { bubbles: true }); + container.value.dispatchEvent(scrollEvent); + } + }); + } + }); }, ); - return {items}; + + return () => { + const { + prefixCls, + filteredRenderItems, + lazy, + selectedKeys, + disabled: globalDisabled, + showRemove, + pagination, + } = props; + + const mergedPagination = parsePagination(pagination); + let paginationNode = null; + + if (mergedPagination) { + paginationNode = ( + + ); + } + + const items = getItems().map(({ renderedEl, renderedText, item }: any) => { + const { disabled } = item; + const checked = selectedKeys.indexOf(item.key) >= 0; + + return ( + + ); + }); + return ( + <> +
      + {items} +
    + {paginationNode} + + ); + }; }, }); -export default props => ; +export default (props: TransferListBodyProps, ref) => ; diff --git a/components/transfer/search.tsx b/components/transfer/search.tsx index c4ac218402..573fb8875f 100644 --- a/components/transfer/search.tsx +++ b/components/transfer/search.tsx @@ -1,62 +1,64 @@ import PropTypes from '../_util/vue-types'; -import { getOptionProps } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; import Input from '../input'; -import { defineComponent } from 'vue'; +import { defineComponent, ExtractPropTypes } from 'vue'; -export const TransferSearchProps = { +export const transferSearchProps = { prefixCls: PropTypes.string, placeholder: PropTypes.string, - value: PropTypes.any, + value: PropTypes.string, handleClear: PropTypes.func, disabled: PropTypes.looseBool, onChange: PropTypes.func, }; +export type TransferSearchProps = Partial>; + export default defineComponent({ name: 'Search', inheritAttrs: false, - props: initDefaultProps(TransferSearchProps, { + props: initDefaultProps(transferSearchProps, { placeholder: '', }), - methods: { - handleChange(e: Event) { - this.$emit('change', e); - }, - handleClear2(e: Event) { - e.preventDefault(); - const { handleClear, disabled } = this.$props; + emits: ['change'], + setup(props, { emit }) { + const handleChange = (e: Event) => { + emit('change', e); + }; + + const handleClearFn = (e: Event) => { + const { handleClear, disabled } = props; if (!disabled && handleClear) { handleClear(e); } - }, - }, - render() { - const { placeholder, value, prefixCls, disabled } = getOptionProps(this); - const icon = - value && value.length > 0 ? ( - - - - ) : ( - - - - ); + }; - return ( - <> - - {icon} - - ); + return () => { + const { placeholder, value, prefixCls, disabled } = props; + const icon = + value && value.length > 0 ? ( + + + + ) : ( + + + + ); + return ( + <> + + {icon} + + ); + }; }, }); diff --git a/components/transfer/style/customize.less b/components/transfer/style/customize.less index 632c43708f..1e78a900f2 100644 --- a/components/transfer/style/customize.less +++ b/components/transfer/style/customize.less @@ -1,36 +1,14 @@ @import './index.less'; @table-prefix-cls: ~'@{ant-prefix}-table'; +@input-prefix-cls: ~'@{ant-prefix}-input'; .@{transfer-prefix-cls}-customize-list { - display: flex; - - .@{transfer-prefix-cls}-operation { - flex: none; - align-self: center; - } - .@{transfer-prefix-cls}-list { - flex: auto; + flex: 1 1 50%; width: auto; height: auto; min-height: @transfer-list-height; - - &-body { - &-with-search { - padding-top: 0; - } - - // Search box in customize mode do not need fix top - &-search-wrapper { - position: relative; - padding-bottom: 0; - } - - &-customize-wrapper { - padding: 12px; - } - } } // =================== Hook Components =================== @@ -59,4 +37,9 @@ margin: 16px 0 4px; } } + .@{input-prefix-cls} { + &[disabled] { + background-color: transparent; + } + } } diff --git a/components/transfer/style/index.less b/components/transfer/style/index.less index ce75e43b73..1a4cf0ca62 100644 --- a/components/transfer/style/index.less +++ b/components/transfer/style/index.less @@ -1,18 +1,20 @@ @import '../../style/themes/index'; @import '../../style/mixins/index'; @import '../../checkbox/style/mixin'; -@import './customize.less'; +@import './customize'; @transfer-prefix-cls: ~'@{ant-prefix}-transfer'; -@transfer-header-vertical-padding: ( - (@transfer-header-height - 1px - (@font-size-base * @line-height-base)) / 2 +@transfer-header-vertical-padding: ceil( + ((@transfer-header-height - 1px - @font-size-base * @line-height-base) / 2) ); .@{transfer-prefix-cls} { .reset-component(); position: relative; + display: flex; + align-items: stretch; &-disabled { .@{transfer-prefix-cls}-list { @@ -21,17 +23,16 @@ } &-list { - position: relative; - display: inline-block; + display: flex; + flex-direction: column; width: 180px; height: @transfer-list-height; - padding-top: @transfer-header-height; - vertical-align: middle; border: @border-width-base @border-style-base @border-color-base; border-radius: @border-radius-base; - &-with-footer { - padding-bottom: 34px; + &-with-pagination { + width: 250px; + height: auto; } &-search { @@ -39,13 +40,14 @@ padding-left: @control-padding-horizontal-sm; &-action { position: absolute; - top: 12px; + top: @transfer-list-search-icon-top; right: 12px; bottom: 12px; width: 28px; color: @disabled-color; line-height: @input-height-base; text-align: center; + .@{iconfont-css-prefix} { color: @disabled-color; transition: all 0.3s; @@ -60,75 +62,128 @@ } &-header { - position: absolute; - top: 0; - left: 0; - width: 100%; + display: flex; + flex: none; + align-items: center; + height: @transfer-header-height; // border-top is on the transfer dom. We should minus 1px for this padding: (@transfer-header-vertical-padding - 1px) @control-padding-horizontal @transfer-header-vertical-padding; - overflow: hidden; color: @text-color; background: @component-background; border-bottom: @border-width-base @border-style-base @border-color-split; border-radius: @border-radius-base @border-radius-base 0 0; + > *:not(:last-child) { + margin-right: 4px; + } + + > * { + flex: none; + } + &-title { - position: absolute; - right: 12px; + flex: auto; + overflow: hidden; + white-space: nowrap; + text-align: right; + text-overflow: ellipsis; } - .@{ant-prefix}-checkbox-wrapper + span { - padding-left: 8px; + &-dropdown { + font-size: 10px; + transform: translateY(10%); + cursor: pointer; + + &[disabled='true'] { + cursor: not-allowed; + } } } &-body { - position: relative; - height: 100%; + display: flex; + flex: auto; + flex-direction: column; + overflow: hidden; font-size: @font-size-base; &-search-wrapper { - position: absolute; - top: 0; - left: 0; - width: 100%; - padding: 12px; + position: relative; + flex: none; + padding: @padding-sm; } } - &-body-with-search { - padding-top: @input-height-base + 24px; - } - &-content { - height: 100%; + flex: auto; margin: 0; padding: 0; overflow: auto; list-style: none; - > .LazyLoad { - animation: transferHighlightIn 1s; - } &-item { - min-height: 32px; - padding: 6px @control-padding-horizontal; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; + display: flex; + align-items: center; + min-height: @transfer-item-height; + padding: @transfer-item-padding-vertical @control-padding-horizontal; + line-height: @transfer-item-height - 2 * @transfer-item-padding-vertical; transition: all 0.3s; - > span { - padding-right: 0; + + > *:not(:last-child) { + margin-right: 8px; } + + > * { + flex: none; + } + &-text { - padding-left: 8px; + flex: auto; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &-remove { + .operation-unit(); + position: relative; + color: @border-color-base; + + &::after { + position: absolute; + top: -@transfer-item-padding-vertical; + right: -50%; + bottom: -@transfer-item-padding-vertical; + left: -50%; + content: ''; + } + + &:hover { + color: @link-hover-color; + } } } - &-item:not(&-item-disabled):hover { - background-color: @transfer-item-hover-bg; - cursor: pointer; + &-item:not(&-item-disabled) { + &:hover { + background-color: @transfer-item-hover-bg; + cursor: pointer; + } + + &.@{transfer-prefix-cls}-list-content-item-checked:hover { + background-color: darken(@item-active-bg, 2%); + } + } + + // Do not change hover style when `oneWay` mode + &-show-remove &-item:not(&-item-disabled):hover { + background: transparent; + cursor: default; + } + + &-item-checked { + background-color: @item-active-bg; } &-item-disabled { @@ -137,35 +192,31 @@ } } + &-pagination { + padding: @padding-xs 0; + text-align: right; + border-top: @border-width-base @border-style-base @border-color-split; + } + &-body-not-found { - position: absolute; - top: 50%; + flex: none; width: 100%; - padding-top: 0; + margin: auto 0; color: @disabled-color; text-align: center; - transform: translateY(-50%); - - // with filter should offset the search box height - .@{transfer-prefix-cls}-list-body-with-search & { - margin-top: (@input-height-base / 2); - } } &-footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; border-top: @border-width-base @border-style-base @border-color-split; - border-radius: 0 0 @border-radius-base @border-radius-base; } } &-operation { - display: inline-block; + display: flex; + flex: none; + flex-direction: column; + align-self: center; margin: 0 8px; - overflow: hidden; vertical-align: middle; .@{ant-prefix}-btn { @@ -180,13 +231,10 @@ } } } -} -@keyframes transferHighlightIn { - 0% { - background: @primary-2; - } - 100% { - background: transparent; + .@{ant-prefix}-empty-image { + max-height: (@transfer-header-height / 2) - 22; } } + +@import './rtl'; diff --git a/components/transfer/style/index.ts b/components/transfer/style/index.ts index b7eb1d7896..0cb16d7ce6 100644 --- a/components/transfer/style/index.ts +++ b/components/transfer/style/index.ts @@ -6,3 +6,6 @@ import '../../empty/style'; import '../../checkbox/style'; import '../../button/style'; import '../../input/style'; +import '../../menu/style'; +import '../../dropdown/style'; +import '../../pagination/style'; diff --git a/components/transfer/style/rtl.less b/components/transfer/style/rtl.less new file mode 100644 index 0000000000..88562310c2 --- /dev/null +++ b/components/transfer/style/rtl.less @@ -0,0 +1,69 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; +@import '../../checkbox/style/mixin'; + +@transfer-prefix-cls: ~'@{ant-prefix}-transfer'; + +.@{transfer-prefix-cls} { + &-rtl { + direction: rtl; + } + + &-list { + &-search { + .@{transfer-prefix-cls}-rtl & { + padding-right: @control-padding-horizontal-sm; + padding-left: 24px; + } + &-action { + .@{transfer-prefix-cls}-rtl & { + right: auto; + left: 12px; + } + } + } + + &-header { + > *:not(:last-child) { + .@{transfer-prefix-cls}-rtl & { + margin-right: 0; + margin-left: 4px; + } + } + + .@{transfer-prefix-cls}-rtl & { + right: 0; + left: auto; + } + &-title { + .@{transfer-prefix-cls}-rtl & { + text-align: left; + } + } + } + + &-content { + &-item { + > *:not(:last-child) { + .@{transfer-prefix-cls}-rtl & { + margin-right: 0; + margin-left: 8px; + } + } + } + } + + &-pagination { + .@{transfer-prefix-cls}-rtl & { + text-align: left; + } + } + + &-footer { + .@{transfer-prefix-cls}-rtl & { + right: 0; + left: auto; + } + } + } +} From 9ae2517679f4f53e86115f8490f89e753efbee7a Mon Sep 17 00:00:00 2001 From: ajuner <106791576@qq.com> Date: Wed, 30 Jun 2021 16:45:31 +0800 Subject: [PATCH 2/5] refactor(tooltip): use composition api (#4059) * refactor(tooltip): use composition api * chore: useConfigInject * fix: remove useless --- components/slider/__tests__/index.test.js | 4 +- components/tooltip/Tooltip.tsx | 223 +++++++++--------- components/tooltip/__tests__/tooltip.test.js | 14 +- components/tooltip/abstractTooltipProps.ts | 41 ++-- components/tooltip/placements.ts | 19 +- components/vc-tooltip/Content.jsx | 25 -- components/vc-tooltip/Tooltip.jsx | 103 -------- components/vc-tooltip/index.js | 4 - components/vc-tooltip/index.ts | 3 + components/vc-tooltip/src/Content.tsx | 35 +++ components/vc-tooltip/src/Tooltip.tsx | 106 +++++++++ .../{placements.js => src/placements.ts} | 0 12 files changed, 292 insertions(+), 285 deletions(-) delete mode 100644 components/vc-tooltip/Content.jsx delete mode 100644 components/vc-tooltip/Tooltip.jsx delete mode 100644 components/vc-tooltip/index.js create mode 100644 components/vc-tooltip/index.ts create mode 100644 components/vc-tooltip/src/Content.tsx create mode 100644 components/vc-tooltip/src/Tooltip.tsx rename components/vc-tooltip/{placements.js => src/placements.ts} (100%) diff --git a/components/slider/__tests__/index.test.js b/components/slider/__tests__/index.test.js index 71e5ad3cb5..f2a1af21c7 100644 --- a/components/slider/__tests__/index.test.js +++ b/components/slider/__tests__/index.test.js @@ -19,9 +19,9 @@ describe('Slider', () => { await asyncExpect(() => { expect(document.body.innerHTML).toMatchSnapshot(); wrapper.findAll('.ant-slider-handle')[0].trigger('mouseleave'); - }, 0); + }, 100); await asyncExpect(() => { expect(document.body.innerHTML).toMatchSnapshot(); - }, 0); + }, 100); }); }); diff --git a/components/tooltip/Tooltip.tsx b/components/tooltip/Tooltip.tsx index b2e6bc7025..63e770ead9 100644 --- a/components/tooltip/Tooltip.tsx +++ b/components/tooltip/Tooltip.tsx @@ -1,25 +1,19 @@ -import { defineComponent, ExtractPropTypes, inject, CSSProperties } from 'vue'; +import { defineComponent, ExtractPropTypes, CSSProperties, onMounted, ref } from 'vue'; import VcTooltip from '../vc-tooltip'; import classNames from '../_util/classNames'; import getPlacements from './placements'; import PropTypes from '../_util/vue-types'; import { PresetColorTypes } from '../_util/colors'; -import { - hasProp, - getComponent, - getStyle, - filterEmpty, - getSlot, - isValidElement, -} from '../_util/props-util'; +import warning from '../_util/warning'; +import { getPropsSlot, getStyle, filterEmpty, isValidElement } from '../_util/props-util'; import { cloneElement } from '../_util/vnode'; -import { defaultConfigProvider } from '../config-provider'; -import abstractTooltipProps from './abstractTooltipProps'; +import abstractTooltipProps, { triggerTypes, placementTypes } from './abstractTooltipProps'; +import useConfigInject from '../_util/hooks/useConfigInject'; const splitObject = (obj: any, keys: string[]) => { const picked = {}; const omitted = { ...obj }; - keys.forEach(key => { + keys.forEach((key) => { if (obj && key in obj) { picked[key] = obj[key]; delete omitted[key]; @@ -36,6 +30,10 @@ const tooltipProps = { title: PropTypes.VNodeChild, }; +export type TriggerTypes = typeof triggerTypes[number]; + +export type PlacementTypes = typeof placementTypes[number]; + export type TooltipProps = Partial>; export default defineComponent({ @@ -43,52 +41,56 @@ export default defineComponent({ inheritAttrs: false, props: tooltipProps, emits: ['update:visible', 'visibleChange'], - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), + setup(props, { slots, emit, attrs, expose }) { + const { prefixCls, getTargetContainer } = useConfigInject('tooltip', props); + + const visible = ref(props.visible); + + const tooltip = ref(); + + onMounted(() => { + warning( + !('default-visible' in attrs) || !('defaultVisible' in attrs), + 'Tooltip', + `'defaultVisible' is deprecated, please use 'v-model:visible'`, + ); + }); + + const handleVisibleChange = (bool: boolean) => { + visible.value = isNoTitle() ? false : bool; + if (!isNoTitle()) { + emit('update:visible', bool); + emit('visibleChange', bool); + } }; - }, - data() { - return { - sVisible: !!this.$props.visible || !!this.$props.defaultVisible, + + const isNoTitle = () => { + const title = getPropsSlot(slots, props, 'title'); + return !title && title !== 0; + }; + + const getPopupDomNode = () => { + return tooltip.value.getPopupDomNode(); + }; + + const getVisible = () => { + return !!visible.value; }; - }, - watch: { - visible(val) { - this.sVisible = val; - }, - }, - methods: { - handleVisibleChange(visible: boolean) { - if (!hasProp(this, 'visible')) { - this.sVisible = this.isNoTitle() ? false : visible; - } - if (!this.isNoTitle()) { - this.$emit('update:visible', visible); - this.$emit('visibleChange', visible); - } - }, - getPopupDomNode() { - return (this.$refs.tooltip as any).getPopupDomNode(); - }, + expose({ getPopupDomNode, getVisible }); - getPlacements() { - const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = this.$props; + const getTooltipPlacements = () => { + const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = props; return ( builtinPlacements || getPlacements({ arrowPointAtCenter, - verticalArrowShift: 8, autoAdjustOverflow, }) ); - }, + }; - // Fix Tooltip won't hide at disabled button - // mouse events don't trigger at disabled button in Chrome - // https://github.com/react-component/tooltip/issues/18 - getDisabledCompatibleChildren(ele: any) { + const getDisabledCompatibleChildren = (ele: any) => { if ( ((typeof ele.type === 'object' && (ele.type.__ANT_BUTTON === true || @@ -130,27 +132,21 @@ export default defineComponent({ return {child}; } return ele; - }, - - isNoTitle() { - const title = getComponent(this, 'title'); - return !title && title !== 0; - }, + }; - getOverlay() { - const title = getComponent(this, 'title'); + const getOverlay = () => { + const title = getPropsSlot(slots, props, 'title'); if (title === 0) { return title; } return title || ''; - }, + }; - // 动态设置动画点 - onPopupAlign(domNode: HTMLElement, align: any) { - const placements = this.getPlacements(); + const onPopupAlign = (domNode: HTMLElement, align: any) => { + const placements = getTooltipPlacements(); // 当前返回的位置 const placement = Object.keys(placements).filter( - key => + (key) => placements[key].points[0] === align.points[0] && placements[key].points[1] === align.points[1], )[0]; @@ -174,67 +170,58 @@ export default defineComponent({ transformOrigin.left = `${-align.offset[0]}px`; } domNode.style.transformOrigin = `${transformOrigin.left} ${transformOrigin.top}`; - }, - }, + }; - render() { - const { $props, $data, $attrs } = this; - const { - prefixCls: customizePrefixCls, - openClassName, - getPopupContainer, - color, - overlayClassName, - } = $props; - const { getPopupContainer: getContextPopupContainer } = this.configProvider; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('tooltip', customizePrefixCls); - let children = this.children || filterEmpty(getSlot(this)); - children = children.length === 1 ? children[0] : children; - let sVisible = $data.sVisible; - // Hide tooltip when there is no title - if (!hasProp(this, 'visible') && this.isNoTitle()) { - sVisible = false; - } - if (!children) { - return null; - } - const child = this.getDisabledCompatibleChildren( - isValidElement(children) ? children : {children}, - ); - const childCls = classNames({ - [openClassName || `${prefixCls}-open`]: sVisible, - [child.props && child.props.class]: child.props && child.props.class, - }); - const customOverlayClassName = classNames(overlayClassName, { - [`${prefixCls}-${color}`]: color && PresetColorRegex.test(color), - }); - let formattedOverlayInnerStyle: CSSProperties; - let arrowContentStyle: CSSProperties; - if (color && !PresetColorRegex.test(color)) { - formattedOverlayInnerStyle = { backgroundColor: color }; - arrowContentStyle = { backgroundColor: color }; - } + return () => { + const { openClassName, getPopupContainer, color, overlayClassName } = props; + let children = filterEmpty(slots.default?.()) ?? null; + children = children.length === 1 ? children[0] : children; + // Hide tooltip when there is no title + if (!('visible' in props) && isNoTitle()) { + visible.value = false; + } + if (!children) { + return null; + } + const child = getDisabledCompatibleChildren( + isValidElement(children) ? children : {children}, + ); + const childCls = classNames({ + [openClassName || `${prefixCls.value}-open`]: visible.value, + [child.props && child.props.class]: child.props && child.props.class, + }); + const customOverlayClassName = classNames(overlayClassName, { + [`${prefixCls.value}-${color}`]: color && PresetColorRegex.test(color), + }); + let formattedOverlayInnerStyle: CSSProperties; + let arrowContentStyle: CSSProperties; + if (color && !PresetColorRegex.test(color)) { + formattedOverlayInnerStyle = { backgroundColor: color }; + arrowContentStyle = { backgroundColor: color }; + } - const vcTooltipProps = { - ...$attrs, - ...$props, - prefixCls, - getTooltipContainer: getPopupContainer || getContextPopupContainer, - builtinPlacements: this.getPlacements(), - overlay: this.getOverlay(), - visible: sVisible, - ref: 'tooltip', - overlayClassName: customOverlayClassName, - overlayInnerStyle: formattedOverlayInnerStyle, - arrowContent: , - onVisibleChange: this.handleVisibleChange, - onPopupAlign: this.onPopupAlign, + const vcTooltipProps = { + ...attrs, + ...props, + prefixCls: prefixCls.value, + getTooltipContainer: getPopupContainer || getTargetContainer.value, + builtinPlacements: getTooltipPlacements(), + overlay: getOverlay(), + visible: visible.value, + ref: tooltip, + overlayClassName: customOverlayClassName, + overlayInnerStyle: formattedOverlayInnerStyle, + arrowContent: ( + + ), + onVisibleChange: handleVisibleChange, + onPopupAlign, + }; + return ( + + {visible.value ? cloneElement(child, { class: childCls }) : child} + + ); }; - return ( - - {sVisible ? cloneElement(child, { class: childCls }) : child} - - ); }, }); diff --git a/components/tooltip/__tests__/tooltip.test.js b/components/tooltip/__tests__/tooltip.test.js index f2fafbd154..2a6c60175c 100644 --- a/components/tooltip/__tests__/tooltip.test.js +++ b/components/tooltip/__tests__/tooltip.test.js @@ -44,14 +44,14 @@ describe('Tooltip', () => { }); await asyncExpect(() => { expect(onVisibleChange).not.toHaveBeenCalled(); - expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false); + expect(wrapper.vm.$refs.tooltip.getVisible()).toBe(false); }); await asyncExpect(() => { div.dispatchEvent(new MouseEvent('mouseleave')); }); await asyncExpect(() => { expect(onVisibleChange).not.toHaveBeenCalled(); - expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false); + expect(wrapper.vm.$refs.tooltip.getVisible()).toBe(false); }); await asyncExpect(() => { // update `title` value. @@ -62,14 +62,14 @@ describe('Tooltip', () => { }); await asyncExpect(() => { expect(onVisibleChange).toHaveBeenLastCalledWith(true); - expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(true); + expect(wrapper.vm.$refs.tooltip.getVisible()).toBe(true); }, 0); await asyncExpect(() => { wrapper.findAll('#hello')[0].element.dispatchEvent(new MouseEvent('mouseleave')); }); await asyncExpect(() => { expect(onVisibleChange).toHaveBeenLastCalledWith(false); - expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false); + expect(wrapper.vm.$refs.tooltip.getVisible()).toBe(false); }); await asyncExpect(() => { // add `visible` props. @@ -80,16 +80,16 @@ describe('Tooltip', () => { }); await asyncExpect(() => { expect(onVisibleChange).toHaveBeenLastCalledWith(true); - lastCount = onVisibleChange.mock.calls.length; - expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false); + expect(wrapper.vm.$refs.tooltip.getVisible()).toBe(true); }); await asyncExpect(() => { // always trigger onVisibleChange wrapper.findAll('#hello')[0].element.dispatchEvent(new MouseEvent('mouseleave')); + lastCount = onVisibleChange.mock.calls.length; }); await asyncExpect(() => { expect(onVisibleChange.mock.calls.length).toBe(lastCount); // no change with lastCount - expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false); + expect(wrapper.vm.$refs.tooltip.getVisible()).toBe(false); }); }); }); diff --git a/components/tooltip/abstractTooltipProps.ts b/components/tooltip/abstractTooltipProps.ts index be5e1067c7..bbaf225cce 100644 --- a/components/tooltip/abstractTooltipProps.ts +++ b/components/tooltip/abstractTooltipProps.ts @@ -1,27 +1,30 @@ import PropTypes from '../_util/vue-types'; import { tuple } from '../_util/type'; -const triggerType = PropTypes.oneOf(tuple('hover', 'focus', 'click', 'contextmenu')); +export const triggerTypes = tuple('hover', 'focus', 'click', 'contextmenu'); + +export const placementTypes = tuple( + 'top', + 'left', + 'right', + 'bottom', + 'topLeft', + 'topRight', + 'bottomLeft', + 'bottomRight', + 'leftTop', + 'leftBottom', + 'rightTop', + 'rightBottom', +); export default () => ({ - trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]).def('hover'), + trigger: PropTypes.oneOfType([ + PropTypes.oneOf(triggerTypes), + PropTypes.arrayOf(PropTypes.oneOf(triggerTypes)), + ]).def('hover'), visible: PropTypes.looseBool, - defaultVisible: PropTypes.looseBool, - placement: PropTypes.oneOf( - tuple( - 'top', - 'left', - 'right', - 'bottom', - 'topLeft', - 'topRight', - 'bottomLeft', - 'bottomRight', - 'leftTop', - 'leftBottom', - 'rightTop', - 'rightBottom', - ), - ).def('top'), + // defaultVisible: PropTypes.looseBool, + placement: PropTypes.oneOf(placementTypes).def('top'), color: PropTypes.string, transitionName: PropTypes.string.def('zoom-big-fast'), overlayStyle: PropTypes.object.def(() => ({})), diff --git a/components/tooltip/placements.ts b/components/tooltip/placements.ts index b529f1e758..188e92bc00 100644 --- a/components/tooltip/placements.ts +++ b/components/tooltip/placements.ts @@ -1,4 +1,4 @@ -import { placements as rcPlacements } from '../vc-tooltip/placements'; +import { placements as rcPlacements } from '../vc-tooltip/src/placements'; const autoAdjustOverflowEnabled = { adjustX: 1, @@ -12,15 +12,20 @@ const autoAdjustOverflowDisabled = { const targetOffset = [0, 0]; -interface PlacementsConfig { - arrowPointAtCenter: boolean; +export interface AdjustOverflow { + adjustX?: 0 | 1; + adjustY?: 0 | 1; +} + +export interface PlacementsConfig { + arrowPointAtCenter?: boolean; arrowWidth?: number; verticalArrowShift?: number; horizontalArrowShift?: number; - autoAdjustOverflow?: boolean | Object; + autoAdjustOverflow?: boolean | AdjustOverflow; } -export function getOverflowOptions(autoAdjustOverflow: boolean | Object) { +export function getOverflowOptions(autoAdjustOverflow: boolean | AdjustOverflow) { if (typeof autoAdjustOverflow === 'boolean') { return autoAdjustOverflow ? autoAdjustOverflowEnabled : autoAdjustOverflowDisabled; } @@ -34,7 +39,7 @@ export default function getPlacements(config: PlacementsConfig) { const { arrowWidth = 5, horizontalArrowShift = 16, - verticalArrowShift = 12, + verticalArrowShift = 8, autoAdjustOverflow = true, } = config; const placementMap = { @@ -87,7 +92,7 @@ export default function getPlacements(config: PlacementsConfig) { offset: [-4, verticalArrowShift + arrowWidth], }, }; - Object.keys(placementMap).forEach(key => { + Object.keys(placementMap).forEach((key) => { placementMap[key] = config.arrowPointAtCenter ? { ...placementMap[key], diff --git a/components/vc-tooltip/Content.jsx b/components/vc-tooltip/Content.jsx deleted file mode 100644 index f59d62673d..0000000000 --- a/components/vc-tooltip/Content.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from '../_util/vue-types'; - -export default { - name: 'Content', - props: { - prefixCls: PropTypes.string, - overlay: PropTypes.any, - trigger: PropTypes.any, - overlayInnerStyle: PropTypes.any, - }, - updated() { - const { trigger } = this; - if (trigger) { - trigger.forcePopupAlign(); - } - }, - render() { - const { overlay, prefixCls, overlayInnerStyle } = this; - return ( - - ); - }, -}; diff --git a/components/vc-tooltip/Tooltip.jsx b/components/vc-tooltip/Tooltip.jsx deleted file mode 100644 index df0e9307c6..0000000000 --- a/components/vc-tooltip/Tooltip.jsx +++ /dev/null @@ -1,103 +0,0 @@ -import PropTypes from '../_util/vue-types'; -import Trigger from '../vc-trigger'; -import { placements } from './placements'; -import Content from './Content'; -import { hasProp, getComponent, getOptionProps, getSlot } from '../_util/props-util'; -import { defineComponent } from 'vue'; -function noop() {} -export default defineComponent({ - name: 'Tooltip', - inheritAttrs: false, - props: { - trigger: PropTypes.any.def(['hover']), - defaultVisible: PropTypes.looseBool, - visible: PropTypes.looseBool, - placement: PropTypes.string.def('right'), - transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - animation: PropTypes.any, - afterVisibleChange: PropTypes.func.def(() => {}), - overlay: PropTypes.any, - overlayStyle: PropTypes.object, - overlayClassName: PropTypes.string, - prefixCls: PropTypes.string.def('rc-tooltip'), - mouseEnterDelay: PropTypes.number.def(0), - mouseLeaveDelay: PropTypes.number.def(0.1), - getTooltipContainer: PropTypes.func, - destroyTooltipOnHide: PropTypes.looseBool.def(false), - align: PropTypes.object.def(() => ({})), - arrowContent: PropTypes.any.def(null), - tipId: PropTypes.string, - builtinPlacements: PropTypes.object, - overlayInnerStyle: PropTypes.style, - }, - methods: { - getPopupElement() { - const { prefixCls, tipId, overlayInnerStyle } = this.$props; - return [ -
    - {getComponent(this, 'arrowContent')} -
    , - , - ]; - }, - - getPopupDomNode() { - return this.$refs.trigger.getPopupDomNode(); - }, - }, - render(h) { - const { - overlayClassName, - trigger, - mouseEnterDelay, - mouseLeaveDelay, - overlayStyle, - prefixCls, - afterVisibleChange, - transitionName, - animation, - placement, - align, - destroyTooltipOnHide, - defaultVisible, - getTooltipContainer, - ...restProps - } = getOptionProps(this); - const extraProps = { ...restProps }; - if (hasProp(this, 'visible')) { - extraProps.popupVisible = this.$props.visible; - } - const { $attrs } = this; - const triggerProps = { - popupClassName: overlayClassName, - prefixCls, - action: trigger, - builtinPlacements: placements, - popupPlacement: placement, - popupAlign: align, - getPopupContainer: getTooltipContainer, - afterPopupVisibleChange: afterVisibleChange, - popupTransitionName: transitionName, - popupAnimation: animation, - defaultPopupVisible: defaultVisible, - destroyPopupOnHide: destroyTooltipOnHide, - mouseLeaveDelay, - popupStyle: overlayStyle, - mouseEnterDelay, - ...extraProps, - ...$attrs, - onPopupVisibleChange: $attrs.onVisibleChange || noop, - onPopupAlign: $attrs.onPopupAlign || noop, - ref: 'trigger', - popup: this.getPopupElement(), - }; - return {getSlot(this)[0]}; - }, -}); diff --git a/components/vc-tooltip/index.js b/components/vc-tooltip/index.js deleted file mode 100644 index 142ad098db..0000000000 --- a/components/vc-tooltip/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// based on rc-tooltip 3.7.3 -import Tooltip from './Tooltip'; - -export default Tooltip; diff --git a/components/vc-tooltip/index.ts b/components/vc-tooltip/index.ts new file mode 100644 index 0000000000..d51cedb372 --- /dev/null +++ b/components/vc-tooltip/index.ts @@ -0,0 +1,3 @@ +import Tooltip from './src/Tooltip'; + +export default Tooltip; diff --git a/components/vc-tooltip/src/Content.tsx b/components/vc-tooltip/src/Content.tsx new file mode 100644 index 0000000000..0406ebe82d --- /dev/null +++ b/components/vc-tooltip/src/Content.tsx @@ -0,0 +1,35 @@ +import { onUpdated, ExtractPropTypes, defineComponent } from 'vue'; +import PropTypes from '../../_util/vue-types'; + +const tooltipContentProps = { + prefixCls: PropTypes.string, + overlay: PropTypes.any, + id: PropTypes.string, + trigger: PropTypes.any, + overlayInnerStyle: PropTypes.any, +}; + +export type TooltipContentProps = Partial>; + +export default defineComponent({ + name: 'Content', + props: tooltipContentProps, + setup(props: TooltipContentProps) { + onUpdated(() => { + const { trigger } = props; + if (trigger.value) { + trigger.value.forcePopupAlign(); + } + }); + return () => ( + + ); + }, +}); diff --git a/components/vc-tooltip/src/Tooltip.tsx b/components/vc-tooltip/src/Tooltip.tsx new file mode 100644 index 0000000000..3f00a2af6c --- /dev/null +++ b/components/vc-tooltip/src/Tooltip.tsx @@ -0,0 +1,106 @@ +import PropTypes from '../../_util/vue-types'; +import Trigger from '../../vc-trigger'; +import { placements } from './placements'; +import Content from './Content'; +import { getPropsSlot } from '../../_util/props-util'; +import { defineComponent, ref } from 'vue'; +function noop() {} +export default defineComponent({ + name: 'Tooltip', + inheritAttrs: false, + props: { + trigger: PropTypes.any.def(['hover']), + defaultVisible: PropTypes.looseBool, + visible: PropTypes.looseBool, + placement: PropTypes.string.def('right'), + transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + animation: PropTypes.any, + afterVisibleChange: PropTypes.func.def(() => {}), + overlay: PropTypes.any, + overlayStyle: PropTypes.object, + overlayClassName: PropTypes.string, + prefixCls: PropTypes.string.def('rc-tooltip'), + mouseEnterDelay: PropTypes.number.def(0.1), + mouseLeaveDelay: PropTypes.number.def(0.1), + getTooltipContainer: PropTypes.func, + destroyTooltipOnHide: PropTypes.looseBool.def(false), + align: PropTypes.object.def(() => ({})), + arrowContent: PropTypes.any.def(null), + tipId: PropTypes.string, + builtinPlacements: PropTypes.object, + overlayInnerStyle: PropTypes.style, + }, + setup(props, { slots, attrs, expose }) { + const triggerDOM = ref(); + + const getPopupElement = () => { + const { prefixCls, tipId, overlayInnerStyle } = props; + return [ +
    + {getPropsSlot(slots, props, 'arrowContent')} +
    , + , + ]; + }; + + const getPopupDomNode = () => { + return triggerDOM.value.getPopupDomNode(); + }; + + expose({ getPopupDomNode }); + + return () => { + const { + overlayClassName, + trigger, + mouseEnterDelay, + mouseLeaveDelay, + overlayStyle, + prefixCls, + afterVisibleChange, + transitionName, + animation, + placement, + align, + destroyTooltipOnHide, + defaultVisible, + getTooltipContainer, + ...restProps + } = props; + const extraProps = { ...restProps }; + + const triggerProps = { + popupClassName: overlayClassName, + prefixCls, + action: trigger, + builtinPlacements: placements, + popupPlacement: placement, + popupAlign: align, + getPopupContainer: getTooltipContainer, + afterPopupVisibleChange: afterVisibleChange, + popupTransitionName: transitionName, + popupAnimation: animation, + defaultPopupVisible: defaultVisible, + destroyPopupOnHide: destroyTooltipOnHide, + mouseLeaveDelay, + popupStyle: overlayStyle, + mouseEnterDelay, + popupVisible: props.visible, + ...extraProps, + ...attrs, + onPopupVisibleChange: (attrs.onVisibleChange as any) || noop, + onPopupAlign: attrs.onPopupAlign || noop, + ref: triggerDOM, + popup: getPopupElement(), + }; + return {slots.default?.()}; + }; + }, +}); diff --git a/components/vc-tooltip/placements.js b/components/vc-tooltip/src/placements.ts similarity index 100% rename from components/vc-tooltip/placements.js rename to components/vc-tooltip/src/placements.ts From c0987767c15ac6129e92e7e6c1443a1545b07dd9 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 30 Jun 2021 16:52:11 +0800 Subject: [PATCH 3/5] style: format code --- components/tooltip/Tooltip.tsx | 6 ++++-- components/tooltip/placements.ts | 2 +- components/transfer/ListItem.tsx | 5 +++-- components/transfer/index.tsx | 5 +++-- components/transfer/operation.tsx | 2 +- components/transfer/renderListBody.tsx | 16 ++++------------ components/transfer/search.tsx | 3 ++- components/vc-tooltip/src/Content.tsx | 3 ++- 8 files changed, 20 insertions(+), 22 deletions(-) diff --git a/components/tooltip/Tooltip.tsx b/components/tooltip/Tooltip.tsx index c63ae76a04..f6b9784c4a 100644 --- a/components/tooltip/Tooltip.tsx +++ b/components/tooltip/Tooltip.tsx @@ -1,4 +1,5 @@ -import { defineComponent, ExtractPropTypes, CSSProperties, onMounted, ref } from 'vue'; +import type { ExtractPropTypes, CSSProperties } from 'vue'; +import { defineComponent, onMounted, ref } from 'vue'; import VcTooltip from '../vc-tooltip'; import classNames from '../_util/classNames'; import getPlacements from './placements'; @@ -7,7 +8,8 @@ import { PresetColorTypes } from '../_util/colors'; import warning from '../_util/warning'; import { getPropsSlot, getStyle, filterEmpty, isValidElement } from '../_util/props-util'; import { cloneElement } from '../_util/vnode'; -import abstractTooltipProps, { triggerTypes, placementTypes } from './abstractTooltipProps'; +import type { triggerTypes, placementTypes } from './abstractTooltipProps'; +import abstractTooltipProps from './abstractTooltipProps'; import useConfigInject from '../_util/hooks/useConfigInject'; const splitObject = (obj: any, keys: string[]) => { diff --git a/components/tooltip/placements.ts b/components/tooltip/placements.ts index 188e92bc00..110553dac8 100644 --- a/components/tooltip/placements.ts +++ b/components/tooltip/placements.ts @@ -92,7 +92,7 @@ export default function getPlacements(config: PlacementsConfig) { offset: [-4, verticalArrowShift + arrowWidth], }, }; - Object.keys(placementMap).forEach((key) => { + Object.keys(placementMap).forEach(key => { placementMap[key] = config.arrowPointAtCenter ? { ...placementMap[key], diff --git a/components/transfer/ListItem.tsx b/components/transfer/ListItem.tsx index 6ef271f496..b0ae841fd5 100644 --- a/components/transfer/ListItem.tsx +++ b/components/transfer/ListItem.tsx @@ -1,13 +1,14 @@ import PropTypes, { withUndefined } from '../_util/vue-types'; import classNames from '../_util/classNames'; -import { TransferLocale } from '.'; +import type { TransferLocale } from '.'; import DeleteOutlined from '@ant-design/icons-vue/DeleteOutlined'; import defaultLocale from '../locale/default'; import Lazyload from '../vc-lazy-load'; import Checkbox from '../checkbox'; import TransButton from '../_util/transButton'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; -import { defineComponent, ExtractPropTypes } from 'vue'; +import type { ExtractPropTypes } from 'vue'; +import { defineComponent } from 'vue'; function noop() {} diff --git a/components/transfer/index.tsx b/components/transfer/index.tsx index 74c40d12d4..81d5996755 100644 --- a/components/transfer/index.tsx +++ b/components/transfer/index.tsx @@ -1,4 +1,5 @@ -import { defineComponent, ExtractPropTypes, ref, watch } from 'vue'; +import type { ExtractPropTypes } from 'vue'; +import { defineComponent, ref, watch } from 'vue'; import PropTypes from '../_util/vue-types'; import { getPropsSlot } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; @@ -7,7 +8,7 @@ import List from './list'; import Operation from './operation'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; import defaultLocale from '../locale-provider/default'; -import { RenderEmptyHandler } from '../config-provider'; +import type { RenderEmptyHandler } from '../config-provider'; import { withInstall } from '../_util/type'; import useConfigInject from '../_util/hooks/useConfigInject'; diff --git a/components/transfer/operation.tsx b/components/transfer/operation.tsx index 1014cb16f4..4d24806340 100644 --- a/components/transfer/operation.tsx +++ b/components/transfer/operation.tsx @@ -2,7 +2,7 @@ import type { CSSProperties, FunctionalComponent } from 'vue'; import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; import RightOutlined from '@ant-design/icons-vue/RightOutlined'; import Button from '../button'; -import { Direction } from '../config-provider'; +import type { Direction } from '../config-provider'; function noop() {} diff --git a/components/transfer/renderListBody.tsx b/components/transfer/renderListBody.tsx index acc036d5b6..f82aad80f5 100644 --- a/components/transfer/renderListBody.tsx +++ b/components/transfer/renderListBody.tsx @@ -1,19 +1,11 @@ -import { - defineComponent, - ExtractPropTypes, - nextTick, - computed, - ref, - watch, - onMounted, - onBeforeMount, -} from 'vue'; +import type { ExtractPropTypes } from 'vue'; +import { defineComponent, nextTick, computed, ref, watch, onMounted, onBeforeMount } from 'vue'; import classNames from '../_util/classNames'; import raf from '../_util/raf'; import ListItem from './ListItem'; import Pagination from '../pagination'; import PropTypes, { withUndefined } from '../_util/vue-types'; -import { DataSourceItem } from './list'; +import type { DataSourceItem } from './list'; export const transferListBodyProps = { prefixCls: PropTypes.string, @@ -121,7 +113,7 @@ const ListBody = defineComponent({ if (lazy !== false) { raf.cancel(lazyId); lazyId.value = raf(() => { - if (container) { + if (container.value) { const scrollEvent = new Event('scroll', { bubbles: true }); container.value.dispatchEvent(scrollEvent); } diff --git a/components/transfer/search.tsx b/components/transfer/search.tsx index 573fb8875f..fc804a4f4a 100644 --- a/components/transfer/search.tsx +++ b/components/transfer/search.tsx @@ -3,7 +3,8 @@ import initDefaultProps from '../_util/props-util/initDefaultProps'; import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; import Input from '../input'; -import { defineComponent, ExtractPropTypes } from 'vue'; +import type { ExtractPropTypes } from 'vue'; +import { defineComponent } from 'vue'; export const transferSearchProps = { prefixCls: PropTypes.string, diff --git a/components/vc-tooltip/src/Content.tsx b/components/vc-tooltip/src/Content.tsx index 0406ebe82d..c11691f0cd 100644 --- a/components/vc-tooltip/src/Content.tsx +++ b/components/vc-tooltip/src/Content.tsx @@ -1,4 +1,5 @@ -import { onUpdated, ExtractPropTypes, defineComponent } from 'vue'; +import type { ExtractPropTypes } from 'vue'; +import { onUpdated, defineComponent } from 'vue'; import PropTypes from '../../_util/vue-types'; const tooltipContentProps = { From 35269961a9585604cdd2a175aacb6f29a2c763a6 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 1 Jul 2021 14:55:45 +0800 Subject: [PATCH 4/5] refactor: transfer --- components/dropdown/dropdown.tsx | 13 +- .../__snapshots__/index.test.js.snap | 612 ++++++++++++------ .../{renderListBody.tsx => ListBody.tsx} | 95 +-- components/transfer/ListItem.tsx | 53 +- .../__snapshots__/index.test.js.snap | 40 +- .../__tests__/__snapshots__/list.test.js.snap | 13 +- components/transfer/__tests__/index.test.js | 153 +++-- components/transfer/index.tsx | 256 ++++---- components/transfer/interface.ts | 5 + components/transfer/list.tsx | 294 ++++----- components/transfer/operation.tsx | 2 +- components/transfer/search.tsx | 2 +- components/transfer/style/index.less | 2 +- 13 files changed, 867 insertions(+), 673 deletions(-) rename components/transfer/{renderListBody.tsx => ListBody.tsx} (59%) create mode 100644 components/transfer/interface.ts diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx index ffc160b8f3..8838592c9e 100644 --- a/components/dropdown/dropdown.tsx +++ b/components/dropdown/dropdown.tsx @@ -92,10 +92,15 @@ const Dropdown = defineComponent({ const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('dropdown', customizePrefixCls); const child = getSlot(this)[0]; - const dropdownTrigger = cloneElement(child, { - class: classNames(child?.props?.class, `${prefixCls}-trigger`), - disabled, - }); + const dropdownTrigger = cloneElement( + child, + Object.assign( + { + class: classNames(child?.props?.class, `${prefixCls}-trigger`), + }, + disabled ? { disabled } : {}, + ), + ); const triggerActions = disabled ? [] : typeof trigger === 'string' ? [trigger] : trigger; let alignPoint; if (triggerActions && triggerActions.indexOf('contextmenu') !== -1) { diff --git a/components/locale-provider/__tests__/__snapshots__/index.test.js.snap b/components/locale-provider/__tests__/__snapshots__/index.test.js.snap index e7de8ecd50..7c1beb76dc 100644 --- a/components/locale-provider/__tests__/__snapshots__/index.test.js.snap +++ b/components/locale-provider/__tests__/__snapshots__/index.test.js.snap @@ -616,9 +616,11 @@ exports[`Locale Provider should display the text as ar 1`] = ` Click to confirm
    -
    + 0 عنصر +