refactor: popconfirm

pull/4606/head
tangjinzhou 2021-08-31 10:51:11 +08:00
parent 0ade597b55
commit 783fb2c8a9
15 changed files with 233 additions and 166 deletions

View File

@ -7,3 +7,5 @@ date 相关组件: dayjs UI 变动
steps: add responsive、percent
collapse: add ghost、collapsible
popconfirm: add cancelButton、okButton、esc 隐藏

View File

@ -153,6 +153,13 @@ const collapseMotion = (style: Ref<CSSProperties>, className: Ref<string>): CSSM
};
};
export { Transition, TransitionGroup, collapseMotion };
const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => {
if (transitionName !== undefined) {
return transitionName;
}
return `${rootPrefixCls}-${motion}`;
};
export { Transition, TransitionGroup, collapseMotion, getTransitionName };
export default Transition;

View File

@ -5,7 +5,7 @@ exports[`Popconfirm should show overlay when trigger is clicked 1`] = `
<div class="ant-popover-arrow"><span class="ant-popover-arrow-content"></span></div>
<div class="ant-popover-inner" role="tooltip">
<div class="ant-popover-inner-content">
<div class="ant-popover-message"><span role="img" aria-label="exclamation-circle" class="anticon anticon-exclamation-circle"><svg class="" data-icon="exclamation-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg></span>
<div class="ant-popover-message"><span role="img" aria-label="exclamation-circle" class="anticon anticon-exclamation-circle"><svg focusable="false" class="" data-icon="exclamation-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg></span>
<div class="ant-popover-message-title">code</div>
</div>
<div class="ant-popover-buttons"><button class="ant-btn ant-btn-sm" type="button">

View File

@ -68,12 +68,11 @@ describe('Popconfirm', () => {
await asyncExpect(() => {
const popup = popconfirm.vm.$refs.popconfirm.getPopupDomNode();
expect(popup).not.toBe(null);
expect(popup.className).toContain('ant-popover-placement-top');
expect(popup.innerHTML).toMatchSnapshot();
}, 1000);
});
fit('should not open in disabled', async () => {
it('should not open in disabled', async () => {
const popconfirm = mount(
{
render() {
@ -87,7 +86,6 @@ describe('Popconfirm', () => {
{ sync: false },
);
popconfirm.find('span').trigger('click');
popconfirm.vm.$refs.popconfirm.$forceUpdate();
const popup = popconfirm.vm.$refs.popconfirm.getPopupDomNode();
expect(popup).toBeFalsy();
});

View File

@ -1,159 +1,201 @@
import omit from 'omit.js';
import type { PropType } from 'vue';
import { defineComponent, inject } from 'vue';
import { computed, ExtractPropTypes, onMounted, PropType, ref, toRef } from 'vue';
import { defineComponent } from 'vue';
import Tooltip from '../tooltip';
import abstractTooltipProps from '../tooltip/abstractTooltipProps';
import PropTypes from '../_util/vue-types';
import { getOptionProps, hasProp, getComponent, mergeProps } from '../_util/props-util';
import { initDefaultProps } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import type { LegacyButtonType } from '../button/buttonTypes';
import type { ButtonProps, LegacyButtonType } from '../button/buttonTypes';
import { convertLegacyProps } from '../button/buttonTypes';
import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled';
import Button from '../button';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale-provider/default';
import { defaultConfigProvider } from '../config-provider';
import { withInstall } from '../_util/type';
import useMergedState from '../_util/hooks/useMergedState';
import devWarning from '../vc-util/devWarning';
import KeyCode from '../_util/KeyCode';
import useConfigInject from '../_util/hooks/useConfigInject';
import classNames from '../_util/classNames';
import { getTransitionName } from '../_util/transition';
import { cloneVNodes } from '../_util/vnode';
const tooltipProps = abstractTooltipProps();
export const popconfirmProps = () => ({
...abstractTooltipProps(),
prefixCls: PropTypes.string,
content: PropTypes.any,
title: PropTypes.any,
okType: {
type: String as PropType<LegacyButtonType>,
default: 'primary',
},
disabled: PropTypes.looseBool.def(false),
okText: PropTypes.any,
cancelText: PropTypes.any,
icon: PropTypes.any,
okButtonProps: PropTypes.object,
cancelButtonProps: PropTypes.object,
});
export type PopconfirmProps = Partial<ExtractPropTypes<ReturnType<typeof popconfirmProps>>>;
export interface PopconfirmLocale {
okText: string;
cancelText: string;
}
const Popconfirm = defineComponent({
name: 'APopconfirm',
mixins: [BaseMixin],
props: {
...tooltipProps,
prefixCls: PropTypes.string,
transitionName: PropTypes.string.def('zoom-big'),
content: PropTypes.any,
title: PropTypes.any,
trigger: tooltipProps.trigger.def('click'),
okType: {
type: String as PropType<LegacyButtonType>,
default: 'primary',
},
disabled: PropTypes.looseBool.def(false),
okText: PropTypes.any,
cancelText: PropTypes.any,
icon: PropTypes.any,
okButtonProps: PropTypes.object,
cancelButtonProps: PropTypes.object,
onConfirm: PropTypes.func,
onCancel: PropTypes.func,
onVisibleChange: PropTypes.func,
},
props: initDefaultProps(popconfirmProps(), {
trigger: 'click',
transitionName: 'zoom-big',
align: () => ({}),
placement: 'top',
mouseEnterDelay: 0.1,
mouseLeaveDelay: 0.1,
arrowPointAtCenter: false,
autoAdjustOverflow: true,
okType: 'primary',
disabled: false,
}),
slots: ['title', 'content', 'okText', 'icon', 'cancelText', 'cancelButton', 'okButton'],
emits: ['update:visible', 'confirm', 'cancel', 'visibleChange'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
setup(props: PopconfirmProps, { slots, emit, expose }) {
onMounted(() => {
devWarning(
props.defaultVisible === undefined,
'Popconfirm',
`'defaultVisible' is deprecated, please use 'v-model:visible'`,
);
});
const tooltipRef = ref();
expose({
getPopupDomNode: () => {
return tooltipRef.value?.getPopupDomNode?.();
},
});
const [visible, setVisible] = useMergedState(false, {
value: toRef(props, 'visible'),
defaultValue: props.defaultVisible,
});
const settingVisible = (value: boolean, e?: MouseEvent | KeyboardEvent) => {
if (props.visible === undefined) {
setVisible(value);
}
emit('update:visible', value);
emit('visibleChange', value, e);
};
},
data() {
const props = getOptionProps(this) as any;
const state = { sVisible: false };
if ('visible' in props) {
state.sVisible = props.visible;
}
if ('defaultVisible' in props) {
state.sVisible = props.defaultVisible;
}
return state;
},
watch: {
visible(val) {
this.sVisible = val;
},
},
methods: {
onConfirmHandle(e: Event) {
this.setVisible(false, e);
this.$emit('confirm', e);
},
onCancelHandle(e: Event) {
this.setVisible(false, e);
this.$emit('cancel', e);
},
const onConfirm = (e: MouseEvent) => {
settingVisible(false, e);
emit('confirm', e);
};
onVisibleChangeHandle(sVisible: boolean) {
const { disabled } = this.$props;
const onCancel = (e: MouseEvent) => {
settingVisible(false, e);
emit('cancel', e);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.keyCode === KeyCode.ESC && visible) {
settingVisible(false, e);
}
};
const onVisibleChange = (value: boolean) => {
const { disabled } = props;
if (disabled) {
return;
}
this.setVisible(sVisible);
},
setVisible(sVisible: boolean, e?: Event) {
if (!hasProp(this, 'visible')) {
this.setState({ sVisible });
}
this.$emit('update:visible', sVisible);
this.$emit('visibleChange', sVisible, e);
},
getPopupDomNode() {
return (this.$refs.tooltip as any).getPopupDomNode();
},
renderOverlay(prefixCls: string, popconfirmLocale) {
const { okType, okButtonProps, cancelButtonProps } = this;
const icon = getComponent(this, 'icon') || <ExclamationCircleFilled />;
const cancelBtnProps = mergeProps({
settingVisible(value);
};
const { prefixCls: prefixClsConfirm, configProvider } = useConfigInject('popconfirm', props);
const rootPrefixCls = computed(() => configProvider.getPrefixCls());
const popoverPrefixCls = computed(() => configProvider.getPrefixCls('popover'));
const [popconfirmLocale] = useLocaleReceiver('Popconfirm', defaultLocale.Popconfirm);
const renderOverlay = () => {
const {
okButtonProps,
cancelButtonProps,
title = slots.title?.(),
cancelText = slots.cancel?.(),
okText = slots.okText?.(),
okType,
icon = slots.icon?.(),
} = props;
const { cancelButton, okButton } = slots;
const cancelProps: ButtonProps = {
onClick: onCancel,
size: 'small',
onClick: this.onCancelHandle,
...cancelButtonProps,
});
const okBtnProps = mergeProps({
};
const okProps: ButtonProps = {
onClick: onConfirm,
...convertLegacyProps(okType),
size: 'small',
onClick: this.onConfirmHandle,
...okButtonProps,
});
};
return (
<div class={`${prefixCls}-inner-content`}>
<div class={`${prefixCls}-message`}>
{icon}
<div class={`${prefixCls}-message-title`}>{getComponent(this, 'title')}</div>
<div class={`${popoverPrefixCls.value}-inner-content`}>
<div class={`${popoverPrefixCls.value}-message`}>
{icon || <ExclamationCircleFilled />}
<div class={`${popoverPrefixCls.value}-message-title`}>{title}</div>
</div>
<div class={`${prefixCls}-buttons`}>
<Button {...cancelBtnProps}>
{getComponent(this, 'cancelText') || popconfirmLocale.cancelText}
</Button>
<Button {...okBtnProps}>
{getComponent(this, 'okText') || popconfirmLocale.okText}
</Button>
<div class={`${popoverPrefixCls.value}-buttons`}>
{cancelButton ? (
cancelButton(cancelProps)
) : (
<Button {...cancelProps}>{cancelText || popconfirmLocale.value.cancelText}</Button>
)}
{okButton ? (
okButton(okProps)
) : (
<Button {...okProps}>{okText || popconfirmLocale.value.okText}</Button>
)}
</div>
</div>
);
},
},
render() {
const props = getOptionProps(this);
const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('popover', customizePrefixCls);
const otherProps = omit(props, [
'title',
'content',
'cancelText',
'okText',
'onUpdate:visible',
]);
const overlay = (
<LocaleReceiver
componentName="Popconfirm"
defaultLocale={defaultLocale.Popconfirm}
children={popconfirmLocale => this.renderOverlay(prefixCls, popconfirmLocale)}
/>
);
const tooltipProps = {
...otherProps,
title: overlay,
prefixCls,
visible: this.sVisible,
ref: 'tooltip',
onVisibleChange: this.onVisibleChangeHandle,
};
return <Tooltip {...tooltipProps}>{this.$slots?.default()}</Tooltip>;
return () => {
const { placement, overlayClassName, ...restProps } = props;
const otherProps = omit(restProps, [
'title',
'content',
'cancelText',
'okText',
'onUpdate:visible',
]);
const overlayClassNames = classNames(prefixClsConfirm.value, overlayClassName);
return (
<Tooltip
{...otherProps}
prefixCls={popoverPrefixCls.value}
placement={placement}
onVisibleChange={onVisibleChange}
visible={visible.value}
overlayClassName={overlayClassNames}
transitionName={getTransitionName(rootPrefixCls.value, 'zoom-big', props.transitionName)}
v-slots={{ title: renderOverlay }}
ref={tooltipRef}
>
{cloneVNodes(
slots.default?.() || [],
{
onKeydown: (e: KeyboardEvent) => {
onKeyDown(e);
},
},
false,
)}
</Tooltip>
);
};
},
});
export default withInstall(Popconfirm);

View File

@ -0,0 +1,8 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@popconfirm-prefix-cls: ~'@{ant-prefix}-popconfirm';
.@{popconfirm-prefix-cls} {
z-index: @zindex-popoconfirm;
}

View File

@ -4,3 +4,5 @@ import '../../style/index.less';
// deps-lint-skip: tooltip, popover
import '../../popover/style';
import '../../button/style';
import './index.less';

View File

@ -323,8 +323,8 @@
@layout-trigger-color-light: @text-color;
// z-index list, order by `z-index`
@zindex-badge: 1;
@zindex-table-fixed: 1;
@zindex-badge: auto;
@zindex-table-fixed: 2;
@zindex-affix: 10;
@zindex-back-top: 10;
@zindex-picker-panel: 10;
@ -336,7 +336,8 @@
@zindex-popover: 1030;
@zindex-dropdown: 1050;
@zindex-picker: 1050;
@zindex-tooltip: 1060;
@zindex-popoconfirm: 1060;
@zindex-tooltip: 1070;
@zindex-image: 1080;
// Animation

View File

@ -6,12 +6,13 @@ import classNames from '../_util/classNames';
import PropTypes from '../_util/vue-types';
import { PresetColorTypes } from '../_util/colors';
import warning from '../_util/warning';
import { getPropsSlot, getStyle, filterEmpty, isValidElement } from '../_util/props-util';
import { getStyle, filterEmpty, isValidElement, initDefaultProps } from '../_util/props-util';
import { cloneElement } from '../_util/vnode';
import type { triggerTypes, placementTypes } from './abstractTooltipProps';
import abstractTooltipProps from './abstractTooltipProps';
import useConfigInject from '../_util/hooks/useConfigInject';
import getPlacements, { AdjustOverflow, PlacementsConfig } from './placements';
import firstNotUndefined from '../_util/firstNotUndefined';
export { AdjustOverflow, PlacementsConfig };
@ -40,36 +41,45 @@ const splitObject = (obj: any, keys: string[]) => {
});
return { picked, omitted };
};
const props = abstractTooltipProps();
const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?$`);
export const tooltipProps = {
...props,
title: PropTypes.VNodeChild,
};
export const tooltipProps = () => ({
...abstractTooltipProps(),
title: PropTypes.any,
});
export type TriggerTypes = typeof triggerTypes[number];
export type PlacementTypes = typeof placementTypes[number];
export type TooltipProps = Partial<ExtractPropTypes<typeof tooltipProps>>;
export type TooltipProps = Partial<ExtractPropTypes<ReturnType<typeof tooltipProps>>>;
export default defineComponent({
name: 'ATooltip',
inheritAttrs: false,
props: tooltipProps,
props: initDefaultProps(tooltipProps(), {
trigger: 'hover',
transitionName: 'zoom-big-fast',
align: () => ({}),
placement: 'top',
mouseEnterDelay: 0.1,
mouseLeaveDelay: 0.1,
arrowPointAtCenter: false,
autoAdjustOverflow: true,
}),
slots: ['title'],
emits: ['update:visible', 'visibleChange'],
setup(props, { slots, emit, attrs, expose }) {
const { prefixCls, getTargetContainer } = useConfigInject('tooltip', props);
const visible = ref(false);
const visible = ref(firstNotUndefined([props.visible, props.defaultVisible]));
const tooltip = ref();
onMounted(() => {
warning(
!('default-visible' in attrs) || !('defaultVisible' in attrs),
props.defaultVisible === undefined,
'Tooltip',
`'defaultVisible' is deprecated, please use 'v-model:visible'`,
);
@ -79,11 +89,10 @@ export default defineComponent({
val => {
visible.value = !!val;
},
{ immediate: true },
);
const isNoTitle = () => {
const title = getPropsSlot(slots, props, 'title');
const title = props.title ?? slots.title;
return !title && title !== 0;
};
@ -164,8 +173,7 @@ export default defineComponent({
};
const getOverlay = () => {
const title = getPropsSlot(slots, props, 'title');
return title ?? '';
return props.title ?? slots.title?.();
};
const onPopupAlign = (domNode: HTMLElement, align: any) => {
@ -234,7 +242,6 @@ export default defineComponent({
prefixCls: prefixCls.value,
getTooltipContainer: getPopupContainer || getTargetContainer.value,
builtinPlacements: tooltipPlacements.value,
overlay: getOverlay(),
visible: tempVisible,
ref: tooltip,
overlayClassName: customOverlayClassName,
@ -249,6 +256,7 @@ export default defineComponent({
arrowContent: () => (
<span class={`${prefixCls.value}-arrow-content`} style={arrowContentStyle}></span>
),
overlay: getOverlay,
}}
>
{visible.value ? cloneElement(child, { class: childCls }) : child}

View File

@ -21,23 +21,23 @@ export default () => ({
trigger: PropTypes.oneOfType([
PropTypes.oneOf(triggerTypes),
PropTypes.arrayOf(PropTypes.oneOf(triggerTypes)),
]).def('hover'),
]),
visible: PropTypes.looseBool,
// defaultVisible: PropTypes.looseBool,
placement: PropTypes.oneOf(placementTypes).def('top'),
defaultVisible: PropTypes.looseBool,
placement: PropTypes.oneOf(placementTypes),
color: PropTypes.string,
transitionName: PropTypes.string.def('zoom-big-fast'),
overlayStyle: PropTypes.object.def(() => ({})),
transitionName: PropTypes.string,
overlayStyle: PropTypes.style,
overlayClassName: PropTypes.string,
openClassName: PropTypes.string,
prefixCls: PropTypes.string,
mouseEnterDelay: PropTypes.number.def(0.1),
mouseLeaveDelay: PropTypes.number.def(0.1),
mouseEnterDelay: PropTypes.number,
mouseLeaveDelay: PropTypes.number,
getPopupContainer: PropTypes.func,
arrowPointAtCenter: PropTypes.looseBool.def(false),
autoAdjustOverflow: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]).def(true),
destroyTooltipOnHide: PropTypes.looseBool.def(false),
align: PropTypes.object.def(() => ({})),
arrowPointAtCenter: PropTypes.looseBool,
autoAdjustOverflow: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
destroyTooltipOnHide: PropTypes.looseBool,
align: PropTypes.object,
builtinPlacements: PropTypes.object,
children: PropTypes.array,
onVisibleChange: PropTypes.func,

View File

@ -22,7 +22,7 @@ export default defineComponent({
openClassName: PropTypes.string,
animation: PropTypes.any,
align: PropTypes.object,
overlayStyle: PropTypes.object.def(() => ({})),
overlayStyle: PropTypes.style,
placement: PropTypes.string.def('bottomLeft'),
overlay: PropTypes.any,
trigger: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).def(

View File

@ -4,7 +4,6 @@ import PropTypes from '../../_util/vue-types';
const tooltipContentProps = {
prefixCls: PropTypes.string,
overlay: PropTypes.any,
id: PropTypes.string,
overlayInnerStyle: PropTypes.any,
};
@ -14,7 +13,8 @@ export type TooltipContentProps = Partial<ExtractPropTypes<typeof tooltipContent
export default defineComponent({
name: 'Content',
props: tooltipContentProps,
setup(props: TooltipContentProps) {
slots: ['overlay'],
setup(props: TooltipContentProps, { slots }) {
return () => (
<div
class={`${props.prefixCls}-inner`}
@ -22,7 +22,7 @@ export default defineComponent({
role="tooltip"
style={props.overlayInnerStyle}
>
{typeof props.overlay === 'function' ? props.overlay() : props.overlay}
{slots.overlay?.()}
</div>
);
},

View File

@ -16,8 +16,7 @@ export default defineComponent({
transitionName: PropTypes.string,
animation: PropTypes.any,
afterVisibleChange: PropTypes.func.def(() => {}),
overlay: PropTypes.any,
overlayStyle: PropTypes.object,
overlayStyle: PropTypes.style,
overlayClassName: PropTypes.string,
prefixCls: PropTypes.string.def('rc-tooltip'),
mouseEnterDelay: PropTypes.number.def(0.1),
@ -47,8 +46,8 @@ export default defineComponent({
key="content"
prefixCls={prefixCls}
id={tipId}
overlay={getPropsSlot(slots, props, 'overlay')}
overlayInnerStyle={overlayInnerStyle}
v-slots={{ overlay: slots.overlay }}
/>,
];
};

View File

@ -55,7 +55,7 @@ export default defineComponent({
onPopupVisibleChange: PropTypes.func.def(noop),
afterPopupVisibleChange: PropTypes.func.def(noop),
popup: PropTypes.any,
popupStyle: PropTypes.object.def(() => ({})),
popupStyle: PropTypes.style,
prefixCls: PropTypes.string.def('rc-trigger-popup'),
popupClassName: PropTypes.string.def(''),
popupPlacement: PropTypes.string,

View File

@ -5,7 +5,7 @@
</template>
<script>
import { defineComponent } from 'vue';
import demo from '../v2-doc/src/docs/collapse/demo/index.vue';
import demo from '../v2-doc/src/docs/popconfirm/demo/index.vue';
// import Affix from '../components/affix';
export default defineComponent({
components: {