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 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>
);

View File

@ -19,12 +19,11 @@ 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}`;
});
const id = `${UNIQUE_ID}_${uuid}`;
watchEffect(() => {
watchEffect(
onClear => {
if (mergedLock.value) {
const scrollbarSize = getScrollBarSize();
const isOverflow = isBodyOverflowing();
@ -35,10 +34,15 @@ html body {
overflow-y: hidden;
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''}
}`,
id.value,
id,
);
} 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-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',

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` | |
| 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` | |

View File

@ -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}
/>,
);
};

View File

@ -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` | |

View File

@ -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>>>;

View File

@ -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;

View File

@ -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 };
};

View File

@ -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}

View File

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

View File

@ -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>
);

View File

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

View File

@ -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>
);

View File

@ -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 ? (

View File

@ -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}
>
<Portal visible={mergedOpen.value} autoLock>
<div
class={classNames(rootClassName, `${prefixCls}-target-placeholder`)}
style={{
...(posInfo.value || CENTER_PLACEHOLDER),
...posInfoStyle.value,
position: 'fixed',
pointerEvents: 'none',
}}
/>
</Portal>
</Trigger>
</>
);

View File

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

View File

@ -4,6 +4,7 @@ import { tourStepProps } from '../interface';
const TourStep = defineComponent({
name: 'TourStep',
inheritAttrs: false,
props: tourStepProps(),
setup(props, { attrs }) {
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 { 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);
});
},
{ 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 };
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();
// update when window resize
window.addEventListener('resize', updatePos);
return () => {
window.removeEventListener('resize', updatePos);
};
});
onMounted(() => {
watch(
open,
val => {
[open, targetElement],
() => {
updatePos();
// update when window resize
if (val) {
window.addEventListener('resize', updatePos);
} else {
window.removeEventListener('resize', updatePos);
}
},
{ immediate: true },
{ flush: 'post', immediate: true },
);
// update when window resize
window.addEventListener('resize', updatePos);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updatePos);
});
// ======================== PosInfo =========================
const mergedPosInfo = computed(() => {

View File

@ -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'],

View File

@ -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,15 +689,14 @@ 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}
getContainer={getPopupContainer && (() => getPopupContainer(this.getRootDomNode()))}
didUpdate={this.handlePortalUpdate}
visible={this.$data.sPopupVisible}
></Portal>
);
return (
@ -718,6 +705,5 @@ export default defineComponent({
{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 {
shouldRender: Ref<boolean>;
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' */
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>>>;

2
typings/global.d.ts vendored
View File

@ -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 {};