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