refactor: tour #6332

pull/6577/head
tangjinzhou 2023-05-17 19:02:49 +08:00
parent 698c0ff3b4
commit e5787c2ed2
26 changed files with 209 additions and 325 deletions

View File

@ -1,6 +1,4 @@
import PropTypes from './vue-types'; import PropTypes from './vue-types';
import switchScrollingEffect from './switchScrollingEffect';
import setStyle from './setStyle';
import Portal from './Portal'; import Portal from './Portal';
import { import {
defineComponent, defineComponent,
@ -11,10 +9,12 @@ import {
onUpdated, onUpdated,
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
computed,
} from 'vue'; } from 'vue';
import canUseDom from './canUseDom'; import canUseDom from './canUseDom';
import ScrollLocker from '../vc-util/Dom/scrollLocker';
import raf from './raf'; import raf from './raf';
import { booleanType } from './type';
import useScrollLocker from './hooks/useScrollLocker';
let openCount = 0; let openCount = 0;
const supportDom = canUseDom(); const supportDom = canUseDom();
@ -24,10 +24,6 @@ export function getOpenCount() {
return process.env.NODE_ENV === 'test' ? openCount : 0; return process.env.NODE_ENV === 'test' ? openCount : 0;
} }
// https://github.com/ant-design/ant-design/issues/19340
// https://github.com/ant-design/ant-design/issues/19332
let cacheOverflow = {};
const getParent = (getContainer: GetContainer) => { const getParent = (getContainer: GetContainer) => {
if (!supportDom) { if (!supportDom) {
return null; return null;
@ -57,20 +53,20 @@ export default defineComponent({
forceRender: { type: Boolean, default: undefined }, forceRender: { type: Boolean, default: undefined },
getContainer: PropTypes.any, getContainer: PropTypes.any,
visible: { type: Boolean, default: undefined }, visible: { type: Boolean, default: undefined },
autoLock: booleanType(),
didUpdate: Function,
}, },
setup(props, { slots }) { setup(props, { slots }) {
const container = shallowRef<HTMLElement>(); const container = shallowRef<HTMLElement>();
const componentRef = shallowRef(); const componentRef = shallowRef();
const rafId = shallowRef<number>(); const rafId = shallowRef<number>();
const scrollLocker = new ScrollLocker({
container: getParent(props.getContainer) as HTMLElement,
});
const removeCurrentContainer = () => { const removeCurrentContainer = () => {
// Portal will remove from `parentNode`. // Portal will remove from `parentNode`.
// Let's handle this again to avoid refactor issue. // Let's handle this again to avoid refactor issue.
container.value?.parentNode?.removeChild(container.value); container.value?.parentNode?.removeChild(container.value);
container.value = null;
}; };
const attachToParent = (force = false) => { const attachToParent = (force = false) => {
if (force || (container.value && !container.value.parentNode)) { if (force || (container.value && !container.value.parentNode)) {
@ -86,13 +82,13 @@ export default defineComponent({
return true; return true;
}; };
// attachToParent(); // attachToParent();
const defaultContainer = document.createElement('div');
const getContainer = () => { const getContainer = () => {
if (!supportDom) { if (!supportDom) {
return null; return null;
} }
if (!container.value) { if (!container.value) {
container.value = document.createElement('div'); container.value = defaultContainer;
attachToParent(true); attachToParent(true);
} }
setWrapperClassName(); setWrapperClassName();
@ -108,30 +104,19 @@ export default defineComponent({
setWrapperClassName(); setWrapperClassName();
attachToParent(); attachToParent();
}); });
/**
* Enhance ./switchScrollingEffect
* 1. Simulate document body scroll bar with
* 2. Record body has overflow style and recover when all of PortalWrapper invisible
* 3. Disable body scroll when PortalWrapper has open
*
* @memberof PortalWrapper
*/
const switchScrolling = () => {
if (openCount === 1 && !Object.keys(cacheOverflow).length) {
switchScrollingEffect();
// Must be set after switchScrollingEffect
cacheOverflow = setStyle({
overflow: 'hidden',
overflowX: 'hidden',
overflowY: 'hidden',
});
} else if (!openCount) {
setStyle(cacheOverflow);
cacheOverflow = {};
switchScrollingEffect(true);
}
};
const instance = getCurrentInstance(); const instance = getCurrentInstance();
useScrollLocker(
computed(() => {
return (
props.autoLock &&
props.visible &&
canUseDom() &&
(container.value === document.body || container.value === defaultContainer)
);
}),
);
onMounted(() => { onMounted(() => {
let init = false; let init = false;
watch( watch(
@ -157,17 +142,6 @@ export default defineComponent({
) { ) {
removeCurrentContainer(); removeCurrentContainer();
} }
// updateScrollLocker
if (
visible &&
visible !== prevVisible &&
supportDom &&
getParent(getContainer) !== scrollLocker.getContainer()
) {
scrollLocker.reLock({
container: getParent(getContainer) as HTMLElement,
});
}
} }
init = true; init = true;
}, },
@ -192,22 +166,30 @@ export default defineComponent({
removeCurrentContainer(); removeCurrentContainer();
raf.cancel(rafId.value); raf.cancel(rafId.value);
}); });
watch(
[() => props.visible, () => props.forceRender],
() => {
const { forceRender, visible } = props;
if (visible === false && !forceRender) {
removeCurrentContainer();
}
},
{ flush: 'post' },
);
return () => { return () => {
const { forceRender, visible } = props; const { forceRender, visible } = props;
let portal = null; let portal = null;
const childProps = { const childProps = {
getOpenCount: () => openCount, getOpenCount: () => openCount,
getContainer, getContainer,
switchScrollingEffect: switchScrolling,
scrollLocker,
}; };
if (visible === false && !forceRender) return null;
if (forceRender || visible || componentRef.value) { if (forceRender || visible || componentRef.value) {
portal = ( portal = (
<Portal <Portal
getContainer={getContainer} getContainer={getContainer}
ref={componentRef} ref={componentRef}
didUpdate={props.didUpdate}
v-slots={{ default: () => slots.default?.(childProps) }} v-slots={{ default: () => slots.default?.(childProps) }}
></Portal> ></Portal>
); );

View File

@ -19,26 +19,30 @@ export function isBodyOverflowing() {
export default function useScrollLocker(lock?: Ref<boolean>) { export default function useScrollLocker(lock?: Ref<boolean>) {
const mergedLock = computed(() => !!lock && !!lock.value); const mergedLock = computed(() => !!lock && !!lock.value);
const id = computed(() => { uuid += 1;
uuid += 1; const id = `${UNIQUE_ID}_${uuid}`;
return `${UNIQUE_ID}_${uuid}`;
});
watchEffect(() => { watchEffect(
if (mergedLock.value) { onClear => {
const scrollbarSize = getScrollBarSize(); if (mergedLock.value) {
const isOverflow = isBodyOverflowing(); const scrollbarSize = getScrollBarSize();
const isOverflow = isBodyOverflowing();
updateCSS( updateCSS(
` `
html body { html body {
overflow-y: hidden; overflow-y: hidden;
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''} ${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''}
}`, }`,
id.value, id,
); );
} else { } else {
removeCSS(id.value); removeCSS(id);
} }
}); onClear(() => {
removeCSS(id);
});
},
{ flush: 'post' },
);
} }

View File

@ -1,42 +0,0 @@
import getScrollBarSize from './getScrollBarSize';
import setStyle from './setStyle';
function isBodyOverflowing() {
return (
document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) &&
window.innerWidth > document.body.offsetWidth
);
}
let cacheStyle = {};
export default (close?: boolean) => {
if (!isBodyOverflowing() && !close) {
return;
}
// https://github.com/ant-design/ant-design/issues/19729
const scrollingEffectClassName = 'ant-scrolling-effect';
const scrollingEffectClassNameReg = new RegExp(`${scrollingEffectClassName}`, 'g');
const bodyClassName = document.body.className;
if (close) {
if (!scrollingEffectClassNameReg.test(bodyClassName)) return;
setStyle(cacheStyle);
cacheStyle = {};
document.body.className = bodyClassName.replace(scrollingEffectClassNameReg, '').trim();
return;
}
const scrollBarSize = getScrollBarSize();
if (scrollBarSize) {
cacheStyle = setStyle({
position: 'relative',
width: `calc(100% - ${scrollBarSize}px)`,
});
if (!scrollingEffectClassNameReg.test(bodyClassName)) {
const addClassName = `${bodyClassName} ${scrollingEffectClassName}`;
document.body.className = addClassName.trim();
}
}
};

View File

@ -28,7 +28,7 @@ The most basic usage.
<a-button ref="ref3"><EllipsisOutlined /></a-button> <a-button ref="ref3"><EllipsisOutlined /></a-button>
</a-space> </a-space>
<a-tour :open="open" :steps="steps" @close="handleOpen(false)" /> <a-tour v-model:current="current" :open="open" :steps="steps" @close="handleOpen(false)" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -41,7 +41,7 @@ const open = ref<boolean>(false);
const ref1 = ref(null); const ref1 = ref(null);
const ref2 = ref(null); const ref2 = ref(null);
const ref3 = ref(null); const ref3 = ref(null);
const current = ref(0);
const steps: TourProps['steps'] = [ const steps: TourProps['steps'] = [
{ {
title: 'Upload File', title: 'Upload File',

View File

@ -23,7 +23,7 @@ Use when you want to guide users through a product.
| mask | Whether to enable masking, change mask style and fill color by pass custom props | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | | | mask | Whether to enable masking, change mask style and fill color by pass custom props | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | |
| type | Type, affects the background color and text color | `default` `primary` | `default` | | | type | Type, affects the background color and text color | `default` `primary` | `default` | |
| open | Open tour | `boolean` | - | | | open | Open tour | `boolean` | - | |
| current | What is the current step | `number` | - | | | current(v-model) | What is the current step | `number` | - | |
| scrollIntoViewOptions | support pass custom scrollIntoView options | `boolean` \| `ScrollIntoViewOptions` | `true` | | | scrollIntoViewOptions | support pass custom scrollIntoView options | `boolean` \| `ScrollIntoViewOptions` | `true` | |
| indicatorsRender | custom indicator | `v-slot:indicatorsRender="{current, total}"` | - | | | indicatorsRender | custom indicator | `v-slot:indicatorsRender="{current, total}"` | - | |
| zIndex | Tour's zIndex | `number` | `1001` | | | zIndex | Tour's zIndex | `number` | `1001` | |

View File

@ -1,4 +1,4 @@
import { defineComponent, toRefs } from 'vue'; import { computed, defineComponent, toRefs } from 'vue';
import VCTour from '../vc-tour'; import VCTour from '../vc-tour';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import TourPanel from './panelRender'; import TourPanel from './panelRender';
@ -11,26 +11,27 @@ import useMergedType from './useMergedType';
// CSSINJS // CSSINJS
import useStyle from './style'; import useStyle from './style';
import getPlacements from '../_util/placements';
export { TourProps, TourStepProps }; export { TourProps, TourStepProps };
const Tour = defineComponent({ const Tour = defineComponent({
name: 'ATour', name: 'ATour',
inheritAttrs: false,
props: tourProps(), props: tourProps(),
setup(props, { attrs, emit, slots }) { setup(props, { attrs, emit, slots }) {
const { current } = toRefs(props); const { current, type, steps, defaultCurrent } = toRefs(props);
const { prefixCls, direction } = useConfigInject('tour', props); const { prefixCls, direction } = useConfigInject('tour', props);
// style // style
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId] = useStyle(prefixCls);
const { currentMergedType, updateInnerCurrent } = useMergedType({ const { currentMergedType, updateInnerCurrent } = useMergedType({
defaultType: props.type, defaultType: type,
steps: props.steps, steps,
current, current,
defaultCurrent: props.defaultCurrent, defaultCurrent,
}); });
return () => { return () => {
const { steps, current, type, rootClassName, ...restProps } = props; const { steps, current, type, rootClassName, ...restProps } = props;
@ -58,9 +59,17 @@ const Tour = defineComponent({
const onStepChange = (stepCurrent: number) => { const onStepChange = (stepCurrent: number) => {
updateInnerCurrent(stepCurrent); updateInnerCurrent(stepCurrent);
emit('update:current', stepCurrent);
emit('change', stepCurrent); emit('change', stepCurrent);
}; };
const builtinPlacements = computed(() =>
getPlacements({
arrowPointAtCenter: true,
autoAdjustOverflow: true,
}),
);
return wrapSSR( return wrapSSR(
<VCTour <VCTour
{...attrs} {...attrs}
@ -73,6 +82,7 @@ const Tour = defineComponent({
renderPanel={mergedRenderPanel} renderPanel={mergedRenderPanel}
onChange={onStepChange} onChange={onStepChange}
steps={steps} steps={steps}
builtinPlacements={builtinPlacements.value as any}
/>, />,
); );
}; };

View File

@ -24,7 +24,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*nF6hQpM0XtEAAA
| mask | 是否启用蒙层,也可传入配置改变蒙层样式和填充色 | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | | | mask | 是否启用蒙层,也可传入配置改变蒙层样式和填充色 | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` | |
| type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` | | | type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` | |
| open | 打开引导 | `boolean` | - | | | open | 打开引导 | `boolean` | - | |
| current | 当前处于哪一步 | `number` | - | | | current(v-model) | 当前处于哪一步 | `number` | - | |
| scrollIntoViewOptions | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数 | `boolean` \| `ScrollIntoViewOptions` | `true` | | | scrollIntoViewOptions | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数 | `boolean` \| `ScrollIntoViewOptions` | `true` | |
| indicatorsRender | 自定义指示器 | `v-slot:indicatorsRender="{current, total}"` | - | | | indicatorsRender | 自定义指示器 | `v-slot:indicatorsRender="{current, total}"` | - | |
| zIndex | Tour 的层级 | `number` | `1001` | | | zIndex | Tour 的层级 | `number` | `1001` | |

View File

@ -8,6 +8,7 @@ export const tourProps = () => ({
prefixCls: { type: String }, prefixCls: { type: String },
current: { type: Number }, current: { type: Number },
type: { type: String as PropType<'default' | 'primary'> }, // default 类型,影响底色与文字颜色 type: { type: String as PropType<'default' | 'primary'> }, // default 类型,影响底色与文字颜色
'onUpdate:current': Function as PropType<(val: number) => void>,
}); });
export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>; export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>;

View File

@ -12,16 +12,16 @@ import defaultLocale from '../locale/en_US';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
const panelRender = defineComponent({ const panelRender = defineComponent({
name: 'ATourPanel',
inheritAttrs: false,
props: tourStepProps(), props: tourStepProps(),
setup(props, { attrs, slots }) { setup(props, { attrs, slots }) {
const { current, total } = toRefs(props); const { current, total } = toRefs(props);
const isLastStep = computed(() => current.value === total.value - 1); const isLastStep = computed(() => current.value === total.value - 1);
const prevButtonProps = props.prevButtonProps as TourBtnProps;
const nextButtonProps = props.nextButtonProps as TourBtnProps;
const prevBtnClick = e => { const prevBtnClick = e => {
const prevButtonProps = props.prevButtonProps as TourBtnProps;
props.onPrev?.(e); props.onPrev?.(e);
if (typeof prevButtonProps?.onClick === 'function') { if (typeof prevButtonProps?.onClick === 'function') {
prevButtonProps?.onClick(); prevButtonProps?.onClick();
@ -29,6 +29,7 @@ const panelRender = defineComponent({
}; };
const nextBtnClick = e => { const nextBtnClick = e => {
const nextButtonProps = props.nextButtonProps as TourBtnProps;
if (isLastStep.value) { if (isLastStep.value) {
props.onFinish?.(e); props.onFinish?.(e);
} else { } else {
@ -40,16 +41,7 @@ const panelRender = defineComponent({
}; };
return () => { return () => {
const { const { prefixCls, title, onClose, cover, description, type: stepType, arrow } = props;
prefixCls,
title,
onClose,
cover,
description,
type: stepType,
arrow,
} = props;
const prevButtonProps = props.prevButtonProps as TourBtnProps; const prevButtonProps = props.prevButtonProps as TourBtnProps;
const nextButtonProps = props.nextButtonProps as TourBtnProps; const nextButtonProps = props.nextButtonProps as TourBtnProps;

View File

@ -1,33 +1,36 @@
import useMergedState from '../_util/hooks/useMergedState';
import type { TourProps } from './interface'; import type { TourProps } from './interface';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
interface Props { interface Props {
defaultType?: string; defaultType?: Ref<string>;
steps?: TourProps['steps']; steps?: Ref<TourProps['steps']>;
current?: Ref<number>; current?: Ref<number>;
defaultCurrent?: number; defaultCurrent?: Ref<number>;
} }
/** /**
* returns the merged type of a step or the default type. * returns the merged type of a step or the default type.
*/ */
const useMergedType = ({ defaultType, steps = [], current, defaultCurrent }: Props) => { const useMergedType = ({ defaultType, steps, current, defaultCurrent }: Props) => {
const [innerCurrent, updateInnerCurrent] = useMergedState<number | undefined>(defaultCurrent, { const innerCurrent = ref(defaultCurrent?.value);
value: current, const mergedCurrent = computed(() => current?.value);
}); watch(
mergedCurrent,
watch(current, val => { val => {
if (val === undefined) return; innerCurrent.value = val ?? defaultCurrent?.value;
updateInnerCurrent(val); },
}); { immediate: true },
);
const updateInnerCurrent = (val: number) => {
innerCurrent.value = val;
};
const innerType = computed(() => { const innerType = computed(() => {
return typeof innerCurrent.value === 'number' ? steps[innerCurrent.value]?.type : defaultType; return typeof innerCurrent.value === 'number'
? steps && steps.value?.[innerCurrent.value]?.type
: defaultType?.value;
}); });
const currentMergedType = computed(() => innerType.value ?? defaultType); const currentMergedType = computed(() => innerType.value ?? defaultType?.value);
return { currentMergedType, updateInnerCurrent }; return { currentMergedType, updateInnerCurrent };
}; };

View File

@ -209,7 +209,7 @@ const DrawerChild = defineComponent({
const motionProps = typeof motion === 'function' ? motion(placement) : motion; const motionProps = typeof motion === 'function' ? motion(placement) : motion;
return ( return (
<div <div
{...omit(otherProps, ['switchScrollingEffect', 'autofocus'])} {...omit(otherProps, ['autofocus'])}
tabindex={-1} tabindex={-1}
class={wrapperClassName} class={wrapperClassName}
style={rootStyle} style={rootStyle}

View File

@ -54,7 +54,6 @@ const drawerChildProps = () => ({
getContainer: Function, getContainer: Function,
getOpenCount: Function as PropType<() => number>, getOpenCount: Function as PropType<() => number>,
scrollLocker: PropTypes.any, scrollLocker: PropTypes.any,
switchScrollingEffect: Function,
inline: Boolean, inline: Boolean,
}); });
export { drawerProps, drawerChildProps }; export { drawerProps, drawerChildProps };

View File

@ -39,7 +39,6 @@ import useMergedState from '../_util/hooks/useMergedState';
import { warning } from '../vc-util/warning'; import { warning } from '../vc-util/warning';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import type { SharedTimeProps } from './panels/TimePanel'; import type { SharedTimeProps } from './panels/TimePanel';
import { useProviderTrigger } from '../vc-trigger/context';
import { legacyPropsWarning } from './utils/warnUtil'; import { legacyPropsWarning } from './utils/warnUtil';
export type PickerRefConfig = { export type PickerRefConfig = {
@ -435,8 +434,6 @@ function Picker<DateType>() {
}, },
}); });
const getPortal = useProviderTrigger();
return () => { return () => {
const { const {
prefixCls = 'rc-picker', prefixCls = 'rc-picker',
@ -631,7 +628,6 @@ function Picker<DateType>() {
{suffixNode} {suffixNode}
{clearNode} {clearNode}
</div> </div>
{getPortal()}
</div> </div>
</PickerTrigger> </PickerTrigger>
); );

View File

@ -96,7 +96,6 @@ function PickerTrigger(props: PickerTriggerProps, { slots }) {
default: slots.default, default: slots.default,
popup: slots.popupElement, popup: slots.popupElement,
}} }}
tryPopPortal
></Trigger> ></Trigger>
); );
} }

View File

@ -44,7 +44,6 @@ import useMergedState from '../_util/hooks/useMergedState';
import { warning } from '../vc-util/warning'; import { warning } from '../vc-util/warning';
import useState from '../_util/hooks/useState'; import useState from '../_util/hooks/useState';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { useProviderTrigger } from '../vc-trigger/context';
import { legacyPropsWarning } from './utils/warnUtil'; import { legacyPropsWarning } from './utils/warnUtil';
import { useElementSize } from '../_util/hooks/_vueuse/useElementSize'; import { useElementSize } from '../_util/hooks/_vueuse/useElementSize';
@ -256,7 +255,6 @@ function RangerPicker<DateType>() {
const needConfirmButton = computed( const needConfirmButton = computed(
() => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time',
); );
const getPortal = useProviderTrigger();
const presets = computed(() => props.presets); const presets = computed(() => props.presets);
const ranges = computed(() => props.ranges); const ranges = computed(() => props.ranges);
const presetList = usePresets(presets, ranges); const presetList = usePresets(presets, ranges);
@ -1298,7 +1296,6 @@ function RangerPicker<DateType>() {
/> />
{suffixNode} {suffixNode}
{clearNode} {clearNode}
{getPortal()}
</div> </div>
</PickerTrigger> </PickerTrigger>
); );

View File

@ -35,17 +35,16 @@ const Mask = defineComponent({
zIndex: { type: Number }, zIndex: { type: Number },
}, },
setup(props, { attrs }) { setup(props, { attrs }) {
const id = useId();
return () => { return () => {
const { prefixCls, open, rootClassName, pos, showMask, fill, animated, zIndex } = props; const { prefixCls, open, rootClassName, pos, showMask, fill, animated, zIndex } = props;
const id = useId();
const maskId = `${prefixCls}-mask-${id}`; const maskId = `${prefixCls}-mask-${id}`;
const mergedAnimated = typeof animated === 'object' ? animated?.placeholder : animated; const mergedAnimated = typeof animated === 'object' ? animated?.placeholder : animated;
console.log(open);
return ( return (
<Portal <Portal
visible={open} visible={open}
autoLock
v-slots={{ v-slots={{
default: () => default: () =>
open && ( open && (
@ -60,7 +59,9 @@ const Mask = defineComponent({
top: 0, top: 0,
bottom: 0, bottom: 0,
zIndex, zIndex,
pointerEvents: 'none',
}, },
attrs.style as CSSProperties,
]} ]}
> >
{showMask ? ( {showMask ? (

View File

@ -12,8 +12,7 @@ import Mask from './Mask';
import { getPlacements } from './placements'; import { getPlacements } from './placements';
import type { PlacementType } from './placements'; import type { PlacementType } from './placements';
import { initDefaultProps } from '../_util/props-util'; import { initDefaultProps } from '../_util/props-util';
import useScrollLocker from './hooks/useScrollLocker';
import canUseDom from '../_util/canUseDom';
import { import {
someType, someType,
stringType, stringType,
@ -22,18 +21,20 @@ import {
functionType, functionType,
booleanType, booleanType,
} from '../_util/type'; } from '../_util/type';
import Portal from '../_util/PortalWrapper';
const CENTER_PLACEHOLDER: CSSProperties = { const CENTER_PLACEHOLDER: CSSProperties = {
left: '50%', left: '50%',
top: '50%', top: '50%',
width: 1, width: '1px',
height: 1, height: '1px',
}; };
export const tourProps = () => { export const tourProps = () => {
const { builtinPlacements, ...pickedTriggerProps } = triggerProps(); const { builtinPlacements, popupAlign } = triggerProps();
return { return {
...pickedTriggerProps, builtinPlacements,
popupAlign,
steps: arrayType<TourStepInfo[]>(), steps: arrayType<TourStepInfo[]>(),
open: booleanType(), open: booleanType(),
defaultCurrent: { type: Number }, defaultCurrent: { type: Number },
@ -58,6 +59,7 @@ export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>;
const Tour = defineComponent({ const Tour = defineComponent({
name: 'Tour', name: 'Tour',
inheritAttrs: false,
props: initDefaultProps(tourProps(), {}), props: initDefaultProps(tourProps(), {}),
setup(props) { setup(props) {
const { defaultCurrent, placement, mask, scrollIntoViewOptions, open, gap, arrow } = const { defaultCurrent, placement, mask, scrollIntoViewOptions, open, gap, arrow } =
@ -125,11 +127,6 @@ const Tour = defineComponent({
props.onChange?.(nextCurrent); props.onChange?.(nextCurrent);
}; };
// ========================= lock scroll =========================
const lockScroll = computed(() => mergedOpen.value && canUseDom());
useScrollLocker(lockScroll);
return () => { return () => {
const { const {
prefixCls, prefixCls,
@ -159,8 +156,8 @@ const Tour = defineComponent({
const mergedMaskStyle = typeof mergedMask.value === 'boolean' ? undefined : mergedMask.value; const mergedMaskStyle = typeof mergedMask.value === 'boolean' ? undefined : mergedMask.value;
// when targetElement is not exist, use body as triggerDOMNode // when targetElement is not exist, use body as triggerDOMNode
const getTriggerDOMNode = () => { const getTriggerDOMNode = node => {
return targetElement.value || document.body; return node || targetElement.value || document.body;
}; };
const getPopupElement = () => ( const getPopupElement = () => (
@ -185,7 +182,19 @@ const Tour = defineComponent({
{...curStep.value} {...curStep.value}
/> />
); );
const posInfoStyle = computed(() => {
const info = posInfo.value || CENTER_PLACEHOLDER;
// info[key] number px
const style: CSSProperties = {};
Object.keys(info).forEach(key => {
if (typeof info[key] === 'number') {
style[key] = `${info[key]}px`;
} else {
style[key] = info[key];
}
});
return style;
});
return ( return (
<> <>
<Mask <Mask
@ -203,18 +212,8 @@ const Tour = defineComponent({
builtinPlacements={getPlacements(arrowPointAtCenter.value)} builtinPlacements={getPlacements(arrowPointAtCenter.value)}
{...restProps} {...restProps}
ref={triggerRef} ref={triggerRef}
popupStyle={ popupStyle={curStep.value.style}
!curStep.value.target popupPlacement={mergedPlacement.value}
? {
...curStep.value.style,
position: 'fixed',
left: CENTER_PLACEHOLDER.left,
top: CENTER_PLACEHOLDER.top,
transform: 'translate(-50%, -50%)',
}
: curStep.value.style
}
popupPlacement={!curStep.value.target ? 'center' : mergedPlacement.value}
popupVisible={mergedOpen.value} popupVisible={mergedOpen.value}
popupClassName={classNames(rootClassName, curStep.value.className)} popupClassName={classNames(rootClassName, curStep.value.className)}
prefixCls={prefixCls} prefixCls={prefixCls}
@ -225,14 +224,16 @@ const Tour = defineComponent({
mask={false} mask={false}
getTriggerDOMNode={getTriggerDOMNode} getTriggerDOMNode={getTriggerDOMNode}
> >
<div <Portal visible={mergedOpen.value} autoLock>
class={classNames(rootClassName, `${prefixCls}-target-placeholder`)} <div
style={{ class={classNames(rootClassName, `${prefixCls}-target-placeholder`)}
...(posInfo.value || CENTER_PLACEHOLDER), style={{
position: 'fixed', ...posInfoStyle.value,
pointerEvents: 'none', position: 'fixed',
}} pointerEvents: 'none',
/> }}
/>
</Portal>
</Trigger> </Trigger>
</> </>
); );

View File

@ -5,6 +5,7 @@ import type { TourStepProps } from '../interface';
const DefaultPanel = defineComponent({ const DefaultPanel = defineComponent({
name: 'DefaultPanel', name: 'DefaultPanel',
inheritAttrs: false,
props: tourStepProps(), props: tourStepProps(),
setup(props, { attrs }) { setup(props, { attrs }) {
return () => { return () => {

View File

@ -4,6 +4,7 @@ import { tourStepProps } from '../interface';
const TourStep = defineComponent({ const TourStep = defineComponent({
name: 'TourStep', name: 'TourStep',
inheritAttrs: false,
props: tourStepProps(), props: tourStepProps(),
setup(props, { attrs }) { setup(props, { attrs }) {
return () => { return () => {

View File

@ -1,4 +1,4 @@
import { computed, watchEffect, watch } from 'vue'; import { computed, watchEffect, onMounted, watch, onBeforeUnmount } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { isInViewPort } from '../util'; import { isInViewPort } from '../util';
import type { TourStepInfo } from '..'; import type { TourStepInfo } from '..';
@ -29,11 +29,15 @@ export default function useTarget(
// `null` as empty target. // `null` as empty target.
const [targetElement, setTargetElement] = useState<null | HTMLElement | undefined>(undefined); const [targetElement, setTargetElement] = useState<null | HTMLElement | undefined>(undefined);
watchEffect(() => { watchEffect(
const nextElement = typeof target.value === 'function' ? (target.value as any)() : target.value; () => {
const nextElement =
typeof target.value === 'function' ? (target.value as any)() : target.value;
setTargetElement(nextElement || null); setTargetElement(nextElement || null);
}); },
{ flush: 'post' },
);
// ========================= Align ========================== // ========================= Align ==========================
const [posInfo, setPosInfo] = useState<PosInfo>(null); const [posInfo, setPosInfo] = useState<PosInfo>(null);
@ -47,36 +51,29 @@ export default function useTarget(
const { left, top, width, height } = targetElement.value.getBoundingClientRect(); const { left, top, width, height } = targetElement.value.getBoundingClientRect();
const nextPosInfo: PosInfo = { left, top, width, height, radius: 0 }; const nextPosInfo: PosInfo = { left, top, width, height, radius: 0 };
if (JSON.stringify(posInfo.value) !== JSON.stringify(nextPosInfo)) {
setPosInfo(nextPosInfo); setPosInfo(nextPosInfo);
}
} else { } else {
// Not exist target which means we just show in center // Not exist target which means we just show in center
setPosInfo(null); setPosInfo(null);
} }
}; };
watchEffect(() => { onMounted(() => {
updatePos(); watch(
[open, targetElement],
() => {
updatePos();
},
{ flush: 'post', immediate: true },
);
// update when window resize // update when window resize
window.addEventListener('resize', updatePos); window.addEventListener('resize', updatePos);
return () => {
window.removeEventListener('resize', updatePos);
};
}); });
onBeforeUnmount(() => {
watch( window.removeEventListener('resize', updatePos);
open, });
val => {
updatePos();
// update when window resize
if (val) {
window.addEventListener('resize', updatePos);
} else {
window.removeEventListener('resize', updatePos);
}
},
{ immediate: true },
);
// ======================== PosInfo ========================= // ======================== PosInfo =========================
const mergedPosInfo = computed(() => { const mergedPosInfo = computed(() => {

View File

@ -1,3 +1,5 @@
import type { BuildInPlacements } from '../vc-trigger/interface';
export type PlacementType = export type PlacementType =
| 'left' | 'left'
| 'leftTop' | 'leftTop'
@ -15,64 +17,6 @@ export type PlacementType =
const targetOffset = [0, 0]; const targetOffset = [0, 0];
export type AlignPointTopBottom = 't' | 'b' | 'c';
export type AlignPointLeftRight = 'l' | 'r' | 'c';
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
export type AlignPoint = `${AlignPointTopBottom}${AlignPointLeftRight}`;
export interface AlignType {
/**
* move point of source node to align with point of target node.
* Such as ['tr','cc'], align top right point of source node with center point of target node.
* Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
points?: (string | AlignPoint)[];
/**
* offset source node by offset[0] in x and offset[1] in y.
* If offset contains percentage string value, it is relative to sourceNode region.
*/
offset?: number[];
/**
* offset target node by offset[0] in x and offset[1] in y.
* If targetOffset contains percentage string value, it is relative to targetNode region.
*/
targetOffset?: number[];
/**
* If adjustX field is true, will adjust source node in x direction if source node is invisible.
* If adjustY field is true, will adjust source node in y direction if source node is invisible.
*/
overflow?: {
adjustX?: boolean | number;
adjustY?: boolean | number;
shiftX?: boolean | number;
shiftY?: boolean | number;
};
/** Auto adjust arrow position */
autoArrow?: boolean;
/**
* Config visible region check of html node. Default `visible`:
* - `visible`: The visible region of user browser window. Use `clientHeight` for check.
* - `scroll`: The whole region of the html scroll area. Use `scrollHeight` for check.
*/
htmlRegion?: 'visible' | 'scroll';
/**
* Whether use css right instead of left to position
*/
useCssRight?: boolean;
/**
* Whether use css bottom instead of top to position
*/
useCssBottom?: boolean;
/**
* Whether use css transform instead of left/top/right/bottom to position if browser supports.
* Defaults to false.
*/
useCssTransform?: boolean;
ignoreShake?: boolean;
}
export type BuildInPlacements = Record<string, AlignType>;
const basePlacements: BuildInPlacements = { const basePlacements: BuildInPlacements = {
left: { left: {
points: ['cr', 'cl'], points: ['cr', 'cl'],

View File

@ -15,11 +15,11 @@ import addEventListener from '../vc-util/Dom/addEventListener';
import Popup from './Popup'; import Popup from './Popup';
import { getAlignFromPlacement, getAlignPopupClassName } from './utils/alignUtil'; import { getAlignFromPlacement, getAlignPopupClassName } from './utils/alignUtil';
import BaseMixin from '../_util/BaseMixin'; import BaseMixin from '../_util/BaseMixin';
import Portal from '../_util/Portal'; import Portal from '../_util/PortalWrapper';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { cloneElement } from '../_util/vnode'; import { cloneElement } from '../_util/vnode';
import supportsPassive from '../_util/supportsPassive'; import supportsPassive from '../_util/supportsPassive';
import { useInjectTrigger, useProvidePortal } from './context'; import { useProvidePortal } from './context';
const ALL_HANDLERS = [ const ALL_HANDLERS = [
'onClick', 'onClick',
@ -45,14 +45,11 @@ export default defineComponent({
} }
return popupAlign; return popupAlign;
}); });
const { setPortal, popPortal } = useInjectTrigger(props.tryPopPortal);
const popupRef = shallowRef(null); const popupRef = shallowRef(null);
const setPopupRef = val => { const setPopupRef = val => {
popupRef.value = val; popupRef.value = val;
}; };
return { return {
popPortal,
setPortal,
vcTriggerContext: inject( vcTriggerContext: inject(
'vcTriggerContext', 'vcTriggerContext',
{} as { {} as {
@ -92,14 +89,6 @@ export default defineComponent({
(this as any).fireEvents(h, e); (this as any).fireEvents(h, e);
}; };
}); });
(this as any).setPortal?.(
<Portal
key="portal"
v-slots={{ default: this.getComponent }}
getContainer={this.getContainer}
didUpdate={this.handlePortalUpdate}
></Portal>,
);
return { return {
prevPopupVisible: popupVisible, prevPopupVisible: popupVisible,
sPopupVisible: popupVisible, sPopupVisible: popupVisible,
@ -406,7 +395,7 @@ export default defineComponent({
} }
mouseProps.onMousedown = this.onPopupMouseDown; mouseProps.onMousedown = this.onPopupMouseDown;
mouseProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onPopupMouseDown; mouseProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onPopupMouseDown;
const { handleGetPopupClassFromAlign, getRootDomNode, getContainer, $attrs } = this; const { handleGetPopupClassFromAlign, getRootDomNode, $attrs } = this;
const { const {
prefixCls, prefixCls,
destroyPopupOnHide, destroyPopupOnHide,
@ -439,7 +428,6 @@ export default defineComponent({
transitionName: popupTransitionName, transitionName: popupTransitionName,
maskAnimation, maskAnimation,
maskTransitionName, maskTransitionName,
getContainer,
class: popupClassName, class: popupClassName,
style: popupStyle, style: popupStyle,
onAlign: $attrs.onPopupAlign || noop, onAlign: $attrs.onPopupAlign || noop,
@ -644,7 +632,7 @@ export default defineComponent({
render() { render() {
const { $attrs } = this; const { $attrs } = this;
const children = filterEmpty(getSlot(this)); const children = filterEmpty(getSlot(this));
const { alignPoint } = this.$props; const { alignPoint, getPopupContainer } = this.$props;
const child = children[0]; const child = children[0];
this.childOriginEvents = getEvents(child); this.childOriginEvents = getEvents(child);
@ -701,23 +689,21 @@ export default defineComponent({
newChildProps.class = childrenClassName; newChildProps.class = childrenClassName;
} }
const trigger = cloneElement(child, { ...newChildProps, ref: 'triggerRef' }, true, true); const trigger = cloneElement(child, { ...newChildProps, ref: 'triggerRef' }, true, true);
if (this.popPortal) {
return trigger; const portal = (
} else { <Portal
const portal = ( key="portal"
<Portal v-slots={{ default: this.getComponent }}
key="portal" getContainer={getPopupContainer && (() => getPopupContainer(this.getRootDomNode()))}
v-slots={{ default: this.getComponent }} didUpdate={this.handlePortalUpdate}
getContainer={this.getContainer} visible={this.$data.sPopupVisible}
didUpdate={this.handlePortalUpdate} ></Portal>
></Portal> );
); return (
return ( <>
<> {portal}
{portal} {trigger}
{trigger} </>
</> );
);
}
}, },
}); });

View File

@ -18,12 +18,6 @@ export const useProviderTrigger = () => {
}; };
}; };
export const useInjectTrigger = (tryPopPortal?: boolean) => {
return tryPopPortal
? inject(TriggerContextKey, { setPortal: () => {}, popPortal: false })
: { setPortal: () => {}, popPortal: false };
};
export interface PortalContextProps { export interface PortalContextProps {
shouldRender: Ref<boolean>; shouldRender: Ref<boolean>;
inTriggerContext: boolean; // 仅处理 trigger 上下文的 portal inTriggerContext: boolean; // 仅处理 trigger 上下文的 portal

View File

@ -5,22 +5,23 @@ import PropTypes from '../_util/vue-types';
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */ /** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
export type AlignPoint = string; export type AlignPoint = string;
export type OffsetType = number | `${number}%`;
export interface AlignType { export interface AlignType {
/** /**
* move point of source node to align with point of target node. * move point of source node to align with point of target node.
* Such as ['tr','cc'], align top right point of source node with center point of target node. * Such as ['tr','cc'], align top right point of source node with center point of target node.
* Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */ * Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
points?: AlignPoint[]; points?: (string | AlignPoint)[];
/** /**
* offset source node by offset[0] in x and offset[1] in y. * offset source node by offset[0] in x and offset[1] in y.
* If offset contains percentage string value, it is relative to sourceNode region. * If offset contains percentage string value, it is relative to sourceNode region.
*/ */
offset?: number[]; offset?: OffsetType[];
/** /**
* offset target node by offset[0] in x and offset[1] in y. * offset target node by offset[0] in x and offset[1] in y.
* If targetOffset contains percentage string value, it is relative to targetNode region. * If targetOffset contains percentage string value, it is relative to targetNode region.
*/ */
targetOffset?: number[]; targetOffset?: OffsetType[];
/** /**
* If adjustX field is true, will adjust source node in x direction if source node is invisible. * If adjustX field is true, will adjust source node in x direction if source node is invisible.
* If adjustY field is true, will adjust source node in y direction if source node is invisible. * If adjustY field is true, will adjust source node in y direction if source node is invisible.
@ -28,7 +29,24 @@ export interface AlignType {
overflow?: { overflow?: {
adjustX?: boolean | number; adjustX?: boolean | number;
adjustY?: boolean | number; adjustY?: boolean | number;
shiftX?: boolean | number;
shiftY?: boolean | number;
}; };
/** Auto adjust arrow position */
autoArrow?: boolean;
/**
* Config visible region check of html node. Default `visible`:
* - `visible`:
* The visible region of user browser window.
* Use `clientHeight` for check.
* If `visible` region not satisfy, fallback to `scroll`.
* - `scroll`:
* The whole region of the html scroll area.
* Use `scrollHeight` for check.
* - `visibleFirst`:
* Similar to `visible`, but if `visible` region not satisfy, fallback to `scroll`.
*/
htmlRegion?: 'visible' | 'scroll' | 'visibleFirst';
/** /**
* Whether use css right instead of left to position * Whether use css right instead of left to position
*/ */
@ -122,8 +140,6 @@ export const triggerProps = () => ({
autoDestroy: { type: Boolean, default: false }, autoDestroy: { type: Boolean, default: false },
mobile: Object, mobile: Object,
getTriggerDOMNode: Function as PropType<(d?: HTMLElement) => HTMLElement>, getTriggerDOMNode: Function as PropType<(d?: HTMLElement) => HTMLElement>,
// portal context will change
tryPopPortal: Boolean, // no need reactive
}); });
export type TriggerProps = Partial<ExtractPropTypes<ReturnType<typeof triggerProps>>>; export type TriggerProps = Partial<ExtractPropTypes<ReturnType<typeof triggerProps>>>;

2
typings/global.d.ts vendored
View File

@ -256,6 +256,8 @@ declare module 'vue' {
AWeekPicker: typeof import('ant-design-vue')['WeekPicker']; AWeekPicker: typeof import('ant-design-vue')['WeekPicker'];
AQRCode: typeof import('ant-design-vue')['QRCode']; AQRCode: typeof import('ant-design-vue')['QRCode'];
ATour: typeof import('ant-design-vue')['Tour'];
} }
} }
export {}; export {};