refactor: tour #6332
							parent
							
								
									698c0ff3b4
								
							
						
					
					
						commit
						e5787c2ed2
					
				|  | @ -1,6 +1,4 @@ | |||
| import PropTypes from './vue-types'; | ||||
| import switchScrollingEffect from './switchScrollingEffect'; | ||||
| import setStyle from './setStyle'; | ||||
| import Portal from './Portal'; | ||||
| import { | ||||
|   defineComponent, | ||||
|  | @ -11,10 +9,12 @@ import { | |||
|   onUpdated, | ||||
|   getCurrentInstance, | ||||
|   nextTick, | ||||
|   computed, | ||||
| } from 'vue'; | ||||
| import canUseDom from './canUseDom'; | ||||
| import ScrollLocker from '../vc-util/Dom/scrollLocker'; | ||||
| import raf from './raf'; | ||||
| import { booleanType } from './type'; | ||||
| import useScrollLocker from './hooks/useScrollLocker'; | ||||
| 
 | ||||
| let openCount = 0; | ||||
| const supportDom = canUseDom(); | ||||
|  | @ -24,10 +24,6 @@ export function getOpenCount() { | |||
|   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) => { | ||||
|   if (!supportDom) { | ||||
|     return null; | ||||
|  | @ -57,20 +53,20 @@ export default defineComponent({ | |||
|     forceRender: { type: Boolean, default: undefined }, | ||||
|     getContainer: PropTypes.any, | ||||
|     visible: { type: Boolean, default: undefined }, | ||||
|     autoLock: booleanType(), | ||||
|     didUpdate: Function, | ||||
|   }, | ||||
| 
 | ||||
|   setup(props, { slots }) { | ||||
|     const container = shallowRef<HTMLElement>(); | ||||
|     const componentRef = shallowRef(); | ||||
|     const rafId = shallowRef<number>(); | ||||
|     const scrollLocker = new ScrollLocker({ | ||||
|       container: getParent(props.getContainer) as HTMLElement, | ||||
|     }); | ||||
| 
 | ||||
|     const removeCurrentContainer = () => { | ||||
|       // Portal will remove from `parentNode`. | ||||
|       // Let's handle this again to avoid refactor issue. | ||||
|       container.value?.parentNode?.removeChild(container.value); | ||||
|       container.value = null; | ||||
|     }; | ||||
|     const attachToParent = (force = false) => { | ||||
|       if (force || (container.value && !container.value.parentNode)) { | ||||
|  | @ -86,13 +82,13 @@ export default defineComponent({ | |||
|       return true; | ||||
|     }; | ||||
|     // attachToParent(); | ||||
| 
 | ||||
|     const defaultContainer = document.createElement('div'); | ||||
|     const getContainer = () => { | ||||
|       if (!supportDom) { | ||||
|         return null; | ||||
|       } | ||||
|       if (!container.value) { | ||||
|         container.value = document.createElement('div'); | ||||
|         container.value = defaultContainer; | ||||
|         attachToParent(true); | ||||
|       } | ||||
|       setWrapperClassName(); | ||||
|  | @ -108,30 +104,19 @@ export default defineComponent({ | |||
|       setWrapperClassName(); | ||||
|       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(); | ||||
| 
 | ||||
|     useScrollLocker( | ||||
|       computed(() => { | ||||
|         return ( | ||||
|           props.autoLock && | ||||
|           props.visible && | ||||
|           canUseDom() && | ||||
|           (container.value === document.body || container.value === defaultContainer) | ||||
|         ); | ||||
|       }), | ||||
|     ); | ||||
|     onMounted(() => { | ||||
|       let init = false; | ||||
|       watch( | ||||
|  | @ -157,17 +142,6 @@ export default defineComponent({ | |||
|             ) { | ||||
|               removeCurrentContainer(); | ||||
|             } | ||||
|             // updateScrollLocker | ||||
|             if ( | ||||
|               visible && | ||||
|               visible !== prevVisible && | ||||
|               supportDom && | ||||
|               getParent(getContainer) !== scrollLocker.getContainer() | ||||
|             ) { | ||||
|               scrollLocker.reLock({ | ||||
|                 container: getParent(getContainer) as HTMLElement, | ||||
|               }); | ||||
|             } | ||||
|           } | ||||
|           init = true; | ||||
|         }, | ||||
|  | @ -192,22 +166,30 @@ export default defineComponent({ | |||
|       removeCurrentContainer(); | ||||
|       raf.cancel(rafId.value); | ||||
|     }); | ||||
| 
 | ||||
|     watch( | ||||
|       [() => props.visible, () => props.forceRender], | ||||
|       () => { | ||||
|         const { forceRender, visible } = props; | ||||
|         if (visible === false && !forceRender) { | ||||
|           removeCurrentContainer(); | ||||
|         } | ||||
|       }, | ||||
|       { flush: 'post' }, | ||||
|     ); | ||||
|     return () => { | ||||
|       const { forceRender, visible } = props; | ||||
|       let portal = null; | ||||
|       const childProps = { | ||||
|         getOpenCount: () => openCount, | ||||
|         getContainer, | ||||
|         switchScrollingEffect: switchScrolling, | ||||
|         scrollLocker, | ||||
|       }; | ||||
| 
 | ||||
|       if (visible === false && !forceRender) return null; | ||||
|       if (forceRender || visible || componentRef.value) { | ||||
|         portal = ( | ||||
|           <Portal | ||||
|             getContainer={getContainer} | ||||
|             ref={componentRef} | ||||
|             didUpdate={props.didUpdate} | ||||
|             v-slots={{ default: () => slots.default?.(childProps) }} | ||||
|           ></Portal> | ||||
|         ); | ||||
|  |  | |||
|  | @ -19,26 +19,30 @@ export function isBodyOverflowing() { | |||
| 
 | ||||
| export default function useScrollLocker(lock?: Ref<boolean>) { | ||||
|   const mergedLock = computed(() => !!lock && !!lock.value); | ||||
|   const id = computed(() => { | ||||
|     uuid += 1; | ||||
|     return `${UNIQUE_ID}_${uuid}`; | ||||
|   }); | ||||
|   uuid += 1; | ||||
|   const id = `${UNIQUE_ID}_${uuid}`; | ||||
| 
 | ||||
|   watchEffect(() => { | ||||
|     if (mergedLock.value) { | ||||
|       const scrollbarSize = getScrollBarSize(); | ||||
|       const isOverflow = isBodyOverflowing(); | ||||
|   watchEffect( | ||||
|     onClear => { | ||||
|       if (mergedLock.value) { | ||||
|         const scrollbarSize = getScrollBarSize(); | ||||
|         const isOverflow = isBodyOverflowing(); | ||||
| 
 | ||||
|       updateCSS( | ||||
|         ` | ||||
|         updateCSS( | ||||
|           ` | ||||
| html body { | ||||
|   overflow-y: hidden; | ||||
|   ${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''} | ||||
| }`,
 | ||||
|         id.value, | ||||
|       ); | ||||
|     } else { | ||||
|       removeCSS(id.value); | ||||
|     } | ||||
|   }); | ||||
|           id, | ||||
|         ); | ||||
|       } else { | ||||
|         removeCSS(id); | ||||
|       } | ||||
|       onClear(() => { | ||||
|         removeCSS(id); | ||||
|       }); | ||||
|     }, | ||||
|     { flush: 'post' }, | ||||
|   ); | ||||
| } | ||||
|  | @ -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(); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | @ -28,7 +28,7 @@ The most basic usage. | |||
|     <a-button ref="ref3"><EllipsisOutlined /></a-button> | ||||
|   </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> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -41,7 +41,7 @@ const open = ref<boolean>(false); | |||
| const ref1 = ref(null); | ||||
| const ref2 = ref(null); | ||||
| const ref3 = ref(null); | ||||
| 
 | ||||
| const current = ref(0); | ||||
| const steps: TourProps['steps'] = [ | ||||
|   { | ||||
|     title: 'Upload 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` |  | | ||||
| | type | Type, affects the background color and text color | `default` `primary` | `default` |  | | ||||
| | 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` |  | | ||||
| | indicatorsRender | custom indicator | `v-slot:indicatorsRender="{current, total}"` | - |  | | ||||
| | zIndex | Tour's zIndex | `number` | `1001` |  | | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { defineComponent, toRefs } from 'vue'; | ||||
| import { computed, defineComponent, toRefs } from 'vue'; | ||||
| import VCTour from '../vc-tour'; | ||||
| import classNames from '../_util/classNames'; | ||||
| import TourPanel from './panelRender'; | ||||
|  | @ -11,26 +11,27 @@ import useMergedType from './useMergedType'; | |||
| 
 | ||||
| // CSSINJS | ||||
| import useStyle from './style'; | ||||
| import getPlacements from '../_util/placements'; | ||||
| 
 | ||||
| export { TourProps, TourStepProps }; | ||||
| 
 | ||||
| const Tour = defineComponent({ | ||||
|   name: 'ATour', | ||||
|   inheritAttrs: false, | ||||
|   props: tourProps(), | ||||
|   setup(props, { attrs, emit, slots }) { | ||||
|     const { current } = toRefs(props); | ||||
|     const { current, type, steps, defaultCurrent } = toRefs(props); | ||||
|     const { prefixCls, direction } = useConfigInject('tour', props); | ||||
| 
 | ||||
|     // style | ||||
|     const [wrapSSR, hashId] = useStyle(prefixCls); | ||||
| 
 | ||||
|     const { currentMergedType, updateInnerCurrent } = useMergedType({ | ||||
|       defaultType: props.type, | ||||
|       steps: props.steps, | ||||
|       defaultType: type, | ||||
|       steps, | ||||
|       current, | ||||
|       defaultCurrent: props.defaultCurrent, | ||||
|       defaultCurrent, | ||||
|     }); | ||||
| 
 | ||||
|     return () => { | ||||
|       const { steps, current, type, rootClassName, ...restProps } = props; | ||||
| 
 | ||||
|  | @ -58,9 +59,17 @@ const Tour = defineComponent({ | |||
| 
 | ||||
|       const onStepChange = (stepCurrent: number) => { | ||||
|         updateInnerCurrent(stepCurrent); | ||||
|         emit('update:current', stepCurrent); | ||||
|         emit('change', stepCurrent); | ||||
|       }; | ||||
| 
 | ||||
|       const builtinPlacements = computed(() => | ||||
|         getPlacements({ | ||||
|           arrowPointAtCenter: true, | ||||
|           autoAdjustOverflow: true, | ||||
|         }), | ||||
|       ); | ||||
| 
 | ||||
|       return wrapSSR( | ||||
|         <VCTour | ||||
|           {...attrs} | ||||
|  | @ -73,6 +82,7 @@ const Tour = defineComponent({ | |||
|           renderPanel={mergedRenderPanel} | ||||
|           onChange={onStepChange} | ||||
|           steps={steps} | ||||
|           builtinPlacements={builtinPlacements.value as any} | ||||
|         />, | ||||
|       ); | ||||
|     }; | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*nF6hQpM0XtEAAA | |||
| | mask | 是否启用蒙层,也可传入配置改变蒙层样式和填充色 | `boolean` \| `{ style?: CSSProperties; color?: string; }` | `true` |  | | ||||
| | type | 类型,影响底色与文字颜色 | `default` \| `primary` | `default` |  | | ||||
| | open | 打开引导 | `boolean` | - |  | | ||||
| | current | 当前处于哪一步 | `number` | - |  | | ||||
| | current(v-model) | 当前处于哪一步 | `number` | - |  | | ||||
| | scrollIntoViewOptions | 是否支持当前元素滚动到视窗内,也可传入配置指定滚动视窗的相关参数 | `boolean` \| `ScrollIntoViewOptions` | `true` |  | | ||||
| | indicatorsRender | 自定义指示器 | `v-slot:indicatorsRender="{current, total}"` | - |  | | ||||
| | zIndex | Tour 的层级 | `number` | `1001` |  | | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ export const tourProps = () => ({ | |||
|   prefixCls: { type: String }, | ||||
|   current: { type: Number }, | ||||
|   type: { type: String as PropType<'default' | 'primary'> }, //	default	类型,影响底色与文字颜色
 | ||||
|   'onUpdate:current': Function as PropType<(val: number) => void>, | ||||
| }); | ||||
| 
 | ||||
| export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>; | ||||
|  |  | |||
|  | @ -12,16 +12,16 @@ import defaultLocale from '../locale/en_US'; | |||
| import type { VueNode } from '../_util/type'; | ||||
| 
 | ||||
| const panelRender = defineComponent({ | ||||
|   name: 'ATourPanel', | ||||
|   inheritAttrs: false, | ||||
|   props: tourStepProps(), | ||||
|   setup(props, { attrs, slots }) { | ||||
|     const { current, total } = toRefs(props); | ||||
| 
 | ||||
|     const isLastStep = computed(() => current.value === total.value - 1); | ||||
| 
 | ||||
|     const prevButtonProps = props.prevButtonProps as TourBtnProps; | ||||
|     const nextButtonProps = props.nextButtonProps as TourBtnProps; | ||||
| 
 | ||||
|     const prevBtnClick = e => { | ||||
|       const prevButtonProps = props.prevButtonProps as TourBtnProps; | ||||
|       props.onPrev?.(e); | ||||
|       if (typeof prevButtonProps?.onClick === 'function') { | ||||
|         prevButtonProps?.onClick(); | ||||
|  | @ -29,6 +29,7 @@ const panelRender = defineComponent({ | |||
|     }; | ||||
| 
 | ||||
|     const nextBtnClick = e => { | ||||
|       const nextButtonProps = props.nextButtonProps as TourBtnProps; | ||||
|       if (isLastStep.value) { | ||||
|         props.onFinish?.(e); | ||||
|       } else { | ||||
|  | @ -40,16 +41,7 @@ const panelRender = defineComponent({ | |||
|     }; | ||||
| 
 | ||||
|     return () => { | ||||
|       const { | ||||
|         prefixCls, | ||||
|         title, | ||||
|         onClose, | ||||
| 
 | ||||
|         cover, | ||||
|         description, | ||||
|         type: stepType, | ||||
|         arrow, | ||||
|       } = props; | ||||
|       const { prefixCls, title, onClose, cover, description, type: stepType, arrow } = props; | ||||
| 
 | ||||
|       const prevButtonProps = props.prevButtonProps as TourBtnProps; | ||||
|       const nextButtonProps = props.nextButtonProps as TourBtnProps; | ||||
|  |  | |||
|  | @ -1,33 +1,36 @@ | |||
| import useMergedState from '../_util/hooks/useMergedState'; | ||||
| import type { TourProps } from './interface'; | ||||
| import type { Ref } from 'vue'; | ||||
| import { computed, watch } from 'vue'; | ||||
| import { ref, computed, watch } from 'vue'; | ||||
| 
 | ||||
| interface Props { | ||||
|   defaultType?: string; | ||||
|   steps?: TourProps['steps']; | ||||
|   defaultType?: Ref<string>; | ||||
|   steps?: Ref<TourProps['steps']>; | ||||
|   current?: Ref<number>; | ||||
|   defaultCurrent?: number; | ||||
|   defaultCurrent?: Ref<number>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * returns the merged type of a step or the default type. | ||||
|  */ | ||||
| const useMergedType = ({ defaultType, steps = [], current, defaultCurrent }: Props) => { | ||||
|   const [innerCurrent, updateInnerCurrent] = useMergedState<number | undefined>(defaultCurrent, { | ||||
|     value: current, | ||||
|   }); | ||||
| 
 | ||||
|   watch(current, val => { | ||||
|     if (val === undefined) return; | ||||
|     updateInnerCurrent(val); | ||||
|   }); | ||||
| 
 | ||||
| const useMergedType = ({ defaultType, steps, current, defaultCurrent }: Props) => { | ||||
|   const innerCurrent = ref(defaultCurrent?.value); | ||||
|   const mergedCurrent = computed(() => current?.value); | ||||
|   watch( | ||||
|     mergedCurrent, | ||||
|     val => { | ||||
|       innerCurrent.value = val ?? defaultCurrent?.value; | ||||
|     }, | ||||
|     { immediate: true }, | ||||
|   ); | ||||
|   const updateInnerCurrent = (val: number) => { | ||||
|     innerCurrent.value = val; | ||||
|   }; | ||||
|   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 }; | ||||
| }; | ||||
|  |  | |||
|  | @ -209,7 +209,7 @@ const DrawerChild = defineComponent({ | |||
|       const motionProps = typeof motion === 'function' ? motion(placement) : motion; | ||||
|       return ( | ||||
|         <div | ||||
|           {...omit(otherProps, ['switchScrollingEffect', 'autofocus'])} | ||||
|           {...omit(otherProps, ['autofocus'])} | ||||
|           tabindex={-1} | ||||
|           class={wrapperClassName} | ||||
|           style={rootStyle} | ||||
|  |  | |||
|  | @ -54,7 +54,6 @@ const drawerChildProps = () => ({ | |||
|   getContainer: Function, | ||||
|   getOpenCount: Function as PropType<() => number>, | ||||
|   scrollLocker: PropTypes.any, | ||||
|   switchScrollingEffect: Function, | ||||
|   inline: Boolean, | ||||
| }); | ||||
| export { drawerProps, drawerChildProps }; | ||||
|  |  | |||
|  | @ -39,7 +39,6 @@ import useMergedState from '../_util/hooks/useMergedState'; | |||
| import { warning } from '../vc-util/warning'; | ||||
| import classNames from '../_util/classNames'; | ||||
| import type { SharedTimeProps } from './panels/TimePanel'; | ||||
| import { useProviderTrigger } from '../vc-trigger/context'; | ||||
| import { legacyPropsWarning } from './utils/warnUtil'; | ||||
| 
 | ||||
| export type PickerRefConfig = { | ||||
|  | @ -435,8 +434,6 @@ function Picker<DateType>() { | |||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       const getPortal = useProviderTrigger(); | ||||
| 
 | ||||
|       return () => { | ||||
|         const { | ||||
|           prefixCls = 'rc-picker', | ||||
|  | @ -631,7 +628,6 @@ function Picker<DateType>() { | |||
|                 {suffixNode} | ||||
|                 {clearNode} | ||||
|               </div> | ||||
|               {getPortal()} | ||||
|             </div> | ||||
|           </PickerTrigger> | ||||
|         ); | ||||
|  |  | |||
|  | @ -96,7 +96,6 @@ function PickerTrigger(props: PickerTriggerProps, { slots }) { | |||
|         default: slots.default, | ||||
|         popup: slots.popupElement, | ||||
|       }} | ||||
|       tryPopPortal | ||||
|     ></Trigger> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -44,7 +44,6 @@ import useMergedState from '../_util/hooks/useMergedState'; | |||
| import { warning } from '../vc-util/warning'; | ||||
| import useState from '../_util/hooks/useState'; | ||||
| import classNames from '../_util/classNames'; | ||||
| import { useProviderTrigger } from '../vc-trigger/context'; | ||||
| import { legacyPropsWarning } from './utils/warnUtil'; | ||||
| import { useElementSize } from '../_util/hooks/_vueuse/useElementSize'; | ||||
| 
 | ||||
|  | @ -256,7 +255,6 @@ function RangerPicker<DateType>() { | |||
|       const needConfirmButton = computed( | ||||
|         () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', | ||||
|       ); | ||||
|       const getPortal = useProviderTrigger(); | ||||
|       const presets = computed(() => props.presets); | ||||
|       const ranges = computed(() => props.ranges); | ||||
|       const presetList = usePresets(presets, ranges); | ||||
|  | @ -1298,7 +1296,6 @@ function RangerPicker<DateType>() { | |||
|               /> | ||||
|               {suffixNode} | ||||
|               {clearNode} | ||||
|               {getPortal()} | ||||
|             </div> | ||||
|           </PickerTrigger> | ||||
|         ); | ||||
|  |  | |||
|  | @ -35,17 +35,16 @@ const Mask = defineComponent({ | |||
|     zIndex: { type: Number }, | ||||
|   }, | ||||
|   setup(props, { attrs }) { | ||||
|     const id = useId(); | ||||
|     return () => { | ||||
|       const { prefixCls, open, rootClassName, pos, showMask, fill, animated, zIndex } = props; | ||||
| 
 | ||||
|       const id = useId(); | ||||
|       const maskId = `${prefixCls}-mask-${id}`; | ||||
|       const mergedAnimated = typeof animated === 'object' ? animated?.placeholder : animated; | ||||
| 
 | ||||
|       console.log(open); | ||||
|       return ( | ||||
|         <Portal | ||||
|           visible={open} | ||||
|           autoLock | ||||
|           v-slots={{ | ||||
|             default: () => | ||||
|               open && ( | ||||
|  | @ -60,7 +59,9 @@ const Mask = defineComponent({ | |||
|                       top: 0, | ||||
|                       bottom: 0, | ||||
|                       zIndex, | ||||
|                       pointerEvents: 'none', | ||||
|                     }, | ||||
|                     attrs.style as CSSProperties, | ||||
|                   ]} | ||||
|                 > | ||||
|                   {showMask ? ( | ||||
|  |  | |||
|  | @ -12,8 +12,7 @@ import Mask from './Mask'; | |||
| import { getPlacements } from './placements'; | ||||
| import type { PlacementType } from './placements'; | ||||
| import { initDefaultProps } from '../_util/props-util'; | ||||
| import useScrollLocker from './hooks/useScrollLocker'; | ||||
| import canUseDom from '../_util/canUseDom'; | ||||
| 
 | ||||
| import { | ||||
|   someType, | ||||
|   stringType, | ||||
|  | @ -22,18 +21,20 @@ import { | |||
|   functionType, | ||||
|   booleanType, | ||||
| } from '../_util/type'; | ||||
| import Portal from '../_util/PortalWrapper'; | ||||
| 
 | ||||
| const CENTER_PLACEHOLDER: CSSProperties = { | ||||
|   left: '50%', | ||||
|   top: '50%', | ||||
|   width: 1, | ||||
|   height: 1, | ||||
|   width: '1px', | ||||
|   height: '1px', | ||||
| }; | ||||
| 
 | ||||
| export const tourProps = () => { | ||||
|   const { builtinPlacements, ...pickedTriggerProps } = triggerProps(); | ||||
|   const { builtinPlacements, popupAlign } = triggerProps(); | ||||
|   return { | ||||
|     ...pickedTriggerProps, | ||||
|     builtinPlacements, | ||||
|     popupAlign, | ||||
|     steps: arrayType<TourStepInfo[]>(), | ||||
|     open: booleanType(), | ||||
|     defaultCurrent: { type: Number }, | ||||
|  | @ -58,6 +59,7 @@ export type TourProps = Partial<ExtractPropTypes<ReturnType<typeof tourProps>>>; | |||
| 
 | ||||
| const Tour = defineComponent({ | ||||
|   name: 'Tour', | ||||
|   inheritAttrs: false, | ||||
|   props: initDefaultProps(tourProps(), {}), | ||||
|   setup(props) { | ||||
|     const { defaultCurrent, placement, mask, scrollIntoViewOptions, open, gap, arrow } = | ||||
|  | @ -125,11 +127,6 @@ const Tour = defineComponent({ | |||
|       props.onChange?.(nextCurrent); | ||||
|     }; | ||||
| 
 | ||||
|     // ========================= lock scroll ========================= | ||||
|     const lockScroll = computed(() => mergedOpen.value && canUseDom()); | ||||
| 
 | ||||
|     useScrollLocker(lockScroll); | ||||
| 
 | ||||
|     return () => { | ||||
|       const { | ||||
|         prefixCls, | ||||
|  | @ -159,8 +156,8 @@ const Tour = defineComponent({ | |||
|       const mergedMaskStyle = typeof mergedMask.value === 'boolean' ? undefined : mergedMask.value; | ||||
| 
 | ||||
|       // when targetElement is not exist, use body as triggerDOMNode | ||||
|       const getTriggerDOMNode = () => { | ||||
|         return targetElement.value || document.body; | ||||
|       const getTriggerDOMNode = node => { | ||||
|         return node || targetElement.value || document.body; | ||||
|       }; | ||||
| 
 | ||||
|       const getPopupElement = () => ( | ||||
|  | @ -185,7 +182,19 @@ const Tour = defineComponent({ | |||
|           {...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 ( | ||||
|         <> | ||||
|           <Mask | ||||
|  | @ -203,18 +212,8 @@ const Tour = defineComponent({ | |||
|             builtinPlacements={getPlacements(arrowPointAtCenter.value)} | ||||
|             {...restProps} | ||||
|             ref={triggerRef} | ||||
|             popupStyle={ | ||||
|               !curStep.value.target | ||||
|                 ? { | ||||
|                     ...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} | ||||
|             popupStyle={curStep.value.style} | ||||
|             popupPlacement={mergedPlacement.value} | ||||
|             popupVisible={mergedOpen.value} | ||||
|             popupClassName={classNames(rootClassName, curStep.value.className)} | ||||
|             prefixCls={prefixCls} | ||||
|  | @ -225,14 +224,16 @@ const Tour = defineComponent({ | |||
|             mask={false} | ||||
|             getTriggerDOMNode={getTriggerDOMNode} | ||||
|           > | ||||
|             <div | ||||
|               class={classNames(rootClassName, `${prefixCls}-target-placeholder`)} | ||||
|               style={{ | ||||
|                 ...(posInfo.value || CENTER_PLACEHOLDER), | ||||
|                 position: 'fixed', | ||||
|                 pointerEvents: 'none', | ||||
|               }} | ||||
|             /> | ||||
|             <Portal visible={mergedOpen.value} autoLock> | ||||
|               <div | ||||
|                 class={classNames(rootClassName, `${prefixCls}-target-placeholder`)} | ||||
|                 style={{ | ||||
|                   ...posInfoStyle.value, | ||||
|                   position: 'fixed', | ||||
|                   pointerEvents: 'none', | ||||
|                 }} | ||||
|               /> | ||||
|             </Portal> | ||||
|           </Trigger> | ||||
|         </> | ||||
|       ); | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import type { TourStepProps } from '../interface'; | |||
| 
 | ||||
| const DefaultPanel = defineComponent({ | ||||
|   name: 'DefaultPanel', | ||||
|   inheritAttrs: false, | ||||
|   props: tourStepProps(), | ||||
|   setup(props, { attrs }) { | ||||
|     return () => { | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { tourStepProps } from '../interface'; | |||
| 
 | ||||
| const TourStep = defineComponent({ | ||||
|   name: 'TourStep', | ||||
|   inheritAttrs: false, | ||||
|   props: tourStepProps(), | ||||
|   setup(props, { attrs }) { | ||||
|     return () => { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { computed, watchEffect, watch } from 'vue'; | ||||
| import { computed, watchEffect, onMounted, watch, onBeforeUnmount } from 'vue'; | ||||
| import type { Ref } from 'vue'; | ||||
| import { isInViewPort } from '../util'; | ||||
| import type { TourStepInfo } from '..'; | ||||
|  | @ -29,11 +29,15 @@ export default function useTarget( | |||
|   // `null` as empty target.
 | ||||
|   const [targetElement, setTargetElement] = useState<null | HTMLElement | undefined>(undefined); | ||||
| 
 | ||||
|   watchEffect(() => { | ||||
|     const nextElement = typeof target.value === 'function' ? (target.value as any)() : target.value; | ||||
|   watchEffect( | ||||
|     () => { | ||||
|       const nextElement = | ||||
|         typeof target.value === 'function' ? (target.value as any)() : target.value; | ||||
| 
 | ||||
|     setTargetElement(nextElement || null); | ||||
|   }); | ||||
|       setTargetElement(nextElement || null); | ||||
|     }, | ||||
|     { flush: 'post' }, | ||||
|   ); | ||||
| 
 | ||||
|   // ========================= Align ==========================
 | ||||
|   const [posInfo, setPosInfo] = useState<PosInfo>(null); | ||||
|  | @ -47,36 +51,29 @@ export default function useTarget( | |||
| 
 | ||||
|       const { left, top, width, height } = targetElement.value.getBoundingClientRect(); | ||||
|       const nextPosInfo: PosInfo = { left, top, width, height, radius: 0 }; | ||||
| 
 | ||||
|       setPosInfo(nextPosInfo); | ||||
|       if (JSON.stringify(posInfo.value) !== JSON.stringify(nextPosInfo)) { | ||||
|         setPosInfo(nextPosInfo); | ||||
|       } | ||||
|     } else { | ||||
|       // Not exist target which means we just show in center
 | ||||
|       setPosInfo(null); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   watchEffect(() => { | ||||
|     updatePos(); | ||||
|   onMounted(() => { | ||||
|     watch( | ||||
|       [open, targetElement], | ||||
|       () => { | ||||
|         updatePos(); | ||||
|       }, | ||||
|       { flush: 'post', immediate: true }, | ||||
|     ); | ||||
|     // update when window resize
 | ||||
|     window.addEventListener('resize', updatePos); | ||||
|     return () => { | ||||
|       window.removeEventListener('resize', updatePos); | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   watch( | ||||
|     open, | ||||
|     val => { | ||||
|       updatePos(); | ||||
|       // update when window resize
 | ||||
|       if (val) { | ||||
|         window.addEventListener('resize', updatePos); | ||||
|       } else { | ||||
|         window.removeEventListener('resize', updatePos); | ||||
|       } | ||||
|     }, | ||||
|     { immediate: true }, | ||||
|   ); | ||||
|   onBeforeUnmount(() => { | ||||
|     window.removeEventListener('resize', updatePos); | ||||
|   }); | ||||
| 
 | ||||
|   // ======================== PosInfo =========================
 | ||||
|   const mergedPosInfo = computed(() => { | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import type { BuildInPlacements } from '../vc-trigger/interface'; | ||||
| 
 | ||||
| export type PlacementType = | ||||
|   | 'left' | ||||
|   | 'leftTop' | ||||
|  | @ -15,64 +17,6 @@ export type PlacementType = | |||
| 
 | ||||
| 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 = { | ||||
|   left: { | ||||
|     points: ['cr', 'cl'], | ||||
|  |  | |||
|  | @ -15,11 +15,11 @@ import addEventListener from '../vc-util/Dom/addEventListener'; | |||
| import Popup from './Popup'; | ||||
| import { getAlignFromPlacement, getAlignPopupClassName } from './utils/alignUtil'; | ||||
| import BaseMixin from '../_util/BaseMixin'; | ||||
| import Portal from '../_util/Portal'; | ||||
| import Portal from '../_util/PortalWrapper'; | ||||
| import classNames from '../_util/classNames'; | ||||
| import { cloneElement } from '../_util/vnode'; | ||||
| import supportsPassive from '../_util/supportsPassive'; | ||||
| import { useInjectTrigger, useProvidePortal } from './context'; | ||||
| import { useProvidePortal } from './context'; | ||||
| 
 | ||||
| const ALL_HANDLERS = [ | ||||
|   'onClick', | ||||
|  | @ -45,14 +45,11 @@ export default defineComponent({ | |||
|       } | ||||
|       return popupAlign; | ||||
|     }); | ||||
|     const { setPortal, popPortal } = useInjectTrigger(props.tryPopPortal); | ||||
|     const popupRef = shallowRef(null); | ||||
|     const setPopupRef = val => { | ||||
|       popupRef.value = val; | ||||
|     }; | ||||
|     return { | ||||
|       popPortal, | ||||
|       setPortal, | ||||
|       vcTriggerContext: inject( | ||||
|         'vcTriggerContext', | ||||
|         {} as { | ||||
|  | @ -92,14 +89,6 @@ export default defineComponent({ | |||
|         (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 { | ||||
|       prevPopupVisible: popupVisible, | ||||
|       sPopupVisible: popupVisible, | ||||
|  | @ -406,7 +395,7 @@ export default defineComponent({ | |||
|       } | ||||
|       mouseProps.onMousedown = this.onPopupMouseDown; | ||||
|       mouseProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onPopupMouseDown; | ||||
|       const { handleGetPopupClassFromAlign, getRootDomNode, getContainer, $attrs } = this; | ||||
|       const { handleGetPopupClassFromAlign, getRootDomNode, $attrs } = this; | ||||
|       const { | ||||
|         prefixCls, | ||||
|         destroyPopupOnHide, | ||||
|  | @ -439,7 +428,6 @@ export default defineComponent({ | |||
|         transitionName: popupTransitionName, | ||||
|         maskAnimation, | ||||
|         maskTransitionName, | ||||
|         getContainer, | ||||
|         class: popupClassName, | ||||
|         style: popupStyle, | ||||
|         onAlign: $attrs.onPopupAlign || noop, | ||||
|  | @ -644,7 +632,7 @@ export default defineComponent({ | |||
|   render() { | ||||
|     const { $attrs } = this; | ||||
|     const children = filterEmpty(getSlot(this)); | ||||
|     const { alignPoint } = this.$props; | ||||
|     const { alignPoint, getPopupContainer } = this.$props; | ||||
| 
 | ||||
|     const child = children[0]; | ||||
|     this.childOriginEvents = getEvents(child); | ||||
|  | @ -701,23 +689,21 @@ export default defineComponent({ | |||
|       newChildProps.class = childrenClassName; | ||||
|     } | ||||
|     const trigger = cloneElement(child, { ...newChildProps, ref: 'triggerRef' }, true, true); | ||||
|     if (this.popPortal) { | ||||
|       return trigger; | ||||
|     } else { | ||||
|       const portal = ( | ||||
|         <Portal | ||||
|           key="portal" | ||||
|           v-slots={{ default: this.getComponent }} | ||||
|           getContainer={this.getContainer} | ||||
|           didUpdate={this.handlePortalUpdate} | ||||
|         ></Portal> | ||||
|       ); | ||||
|       return ( | ||||
|         <> | ||||
|           {portal} | ||||
|           {trigger} | ||||
|         </> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const portal = ( | ||||
|       <Portal | ||||
|         key="portal" | ||||
|         v-slots={{ default: this.getComponent }} | ||||
|         getContainer={getPopupContainer && (() => getPopupContainer(this.getRootDomNode()))} | ||||
|         didUpdate={this.handlePortalUpdate} | ||||
|         visible={this.$data.sPopupVisible} | ||||
|       ></Portal> | ||||
|     ); | ||||
|     return ( | ||||
|       <> | ||||
|         {portal} | ||||
|         {trigger} | ||||
|       </> | ||||
|     ); | ||||
|   }, | ||||
| }); | ||||
|  |  | |||
|  | @ -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 { | ||||
|   shouldRender: Ref<boolean>; | ||||
|   inTriggerContext: boolean; // 仅处理 trigger 上下文的 portal
 | ||||
|  |  | |||
|  | @ -5,22 +5,23 @@ import PropTypes from '../_util/vue-types'; | |||
| /** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */ | ||||
| export type AlignPoint = string; | ||||
| 
 | ||||
| export type OffsetType = number | `${number}%`; | ||||
| 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?: AlignPoint[]; | ||||
|   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?: OffsetType[]; | ||||
|   /** | ||||
|    * 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[]; | ||||
|   targetOffset?: OffsetType[]; | ||||
|   /** | ||||
|    * 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. | ||||
|  | @ -28,7 +29,24 @@ export interface AlignType { | |||
|   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. | ||||
|    *    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 | ||||
|    */ | ||||
|  | @ -122,8 +140,6 @@ export const triggerProps = () => ({ | |||
|   autoDestroy: { type: Boolean, default: false }, | ||||
|   mobile: Object, | ||||
|   getTriggerDOMNode: Function as PropType<(d?: HTMLElement) => HTMLElement>, | ||||
|   // portal context will change
 | ||||
|   tryPopPortal: Boolean, // no need reactive
 | ||||
| }); | ||||
| 
 | ||||
| export type TriggerProps = Partial<ExtractPropTypes<ReturnType<typeof triggerProps>>>; | ||||
|  |  | |||
|  | @ -256,6 +256,8 @@ declare module 'vue' { | |||
|     AWeekPicker: typeof import('ant-design-vue')['WeekPicker']; | ||||
| 
 | ||||
|     AQRCode: typeof import('ant-design-vue')['QRCode']; | ||||
| 
 | ||||
|     ATour: typeof import('ant-design-vue')['Tour']; | ||||
|   } | ||||
| } | ||||
| export {}; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 tangjinzhou
						tangjinzhou