Skip to content

refactor: tooltip #1678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ExtractPropTypes, PropType, Ref } from 'vue';
import type { ExtractPropTypes, PropType, Ref,ComponentInternalInstance } from 'vue';

export type Placement =
| 'top'
Expand All @@ -15,15 +15,12 @@ export type Placement =
| 'left-end';

export type Alignment = 'start' | 'end';
export type AppendToBodyScrollStrategy = 'close' | 'repostion'
export type PlaceStrategy = 'most-space' | 'no-space'
export type OffsetOptions = { mainAxis?: number; crossAxis?: number };

export type Point = { x?: number; y?: number };

export type UseOverlayFn = {
arrowRef: Ref<HTMLElement | undefined>;
overlayRef: Ref<HTMLElement | undefined>;
updatePosition: () => void;
};

export type EmitEventFn = (event: 'positionChange' | 'update:modelValue', result?: unknown) => void;

Expand All @@ -40,7 +37,7 @@ export const flexibleOverlayProps = {
default: false,
},
origin: {
type: Object as PropType<HTMLElement>,
type: Object as PropType<HTMLElement> | ComponentInternalInstance,
require: true,
},
position: {
Expand Down Expand Up @@ -70,6 +67,28 @@ export const flexibleOverlayProps = {
type: Boolean,
default: false,
},
appendToBodyScrollStrategy:{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

格式化看一下

type:String as PropType<AppendToBodyScrollStrategy>,
default:'reposition'
},
// 保持和宿主元素的宽度一致
fitOriginWidth:{
type: Boolean,
default: false,
},
// 宽高变化时,是否自动调整位置
autoUpdatePosition:{
type: Boolean,
default: false,
},
// 弹出层位置的放置策略
placeStrategy:{
type:String as PropType<PlaceStrategy>,
default:'most-space'
},
scrollElement:{
type:[Object,String] as PropType<HTMLElement | 'auto'>
}
};

export type FlexibleOverlayProps = ExtractPropTypes<typeof flexibleOverlayProps>;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import '../../../styles-var/devui-var.scss';
@import '@devui/theme/styles-var/devui-var.scss';

.#{$devui-prefix}-flexible-overlay {
position: fixed;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineComponent, toRefs, withModifiers } from 'vue';
import { flexibleOverlayProps, FlexibleOverlayProps } from './flexible-overlay-types';
import { useOverlay } from './use-flexible-overlay';
import { useNamespace } from '../../../shared/hooks/use-namespace';
import { useNamespace } from '@devui/shared/utils';
import './flexible-overlay.scss';

export const FlexibleOverlay = defineComponent({
Expand All @@ -12,14 +12,15 @@ export const FlexibleOverlay = defineComponent({
setup(props: FlexibleOverlayProps, { slots, attrs, emit, expose }) {
const ns = useNamespace('flexible-overlay');
const { clickEventBubble } = toRefs(props);
const { arrowRef, overlayRef, updatePosition } = useOverlay(props, emit);
const { arrowRef, overlayRef, styles,showOverlay, updatePosition } = useOverlay(props, emit);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

格式化看一下

expose({ updatePosition });

return () =>
props.modelValue && (
showOverlay.value && (
<div
ref={overlayRef}
class={ns.b()}
style={styles.value}
{...attrs}
onClick={withModifiers(() => ({}), [clickEventBubble.value ? '' : 'stop'])}
onPointerup={withModifiers(() => ({}), ['stop'])}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ref, unref, watch, nextTick, onUnmounted } from 'vue';
import { arrow, autoPlacement, computePosition, offset, shift } from '@floating-ui/dom';
import { FlexibleOverlayProps, Placement, Point, UseOverlayFn, EmitEventFn, Rect } from './flexible-overlay-types';
import { ref, unref, watch, nextTick, onUnmounted, computed, toRefs } from 'vue';
import { arrow, autoPlacement, computePosition, offset, shift, flip } from '@floating-ui/dom';
import { FlexibleOverlayProps, Placement, Point, EmitEventFn, Rect } from './flexible-overlay-types';
import { getScrollParent } from './utils';

function adjustArrowPosition(isArrowCenter: boolean, point: Point, placement: Placement, originRect: Rect): Point {
Expand All @@ -24,10 +24,58 @@ function adjustArrowPosition(isArrowCenter: boolean, point: Point, placement: Pl
return { x, y };
}

export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseOverlayFn {
export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn) {
const { fitOriginWidth, autoUpdatePosition, align, position, showArrow, shiftOffset, placeStrategy } = toRefs(props);
const overlayRef = ref<HTMLElement | undefined>();
const arrowRef = ref<HTMLElement | undefined>();
let originParent = null;
const showOverlay = ref(false);
const overlayWidth = ref(0)
const baseOption = { strategy: 'fixed' }
const baseMiddleware = [offset(props.offset)]
let originParent: HTMLElement;
let rect: DOMRect
let originObserver: ResizeObserver
let overlayObserver: ResizeObserver
const styles = computed(() => {
if (fitOriginWidth.value) {
return { width: overlayWidth.value + 'px' }
} else {
return {}
}
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

格式化看一下,没有分号


const generateMostSpaceOptions = () => {
const middleware = [
...baseMiddleware,
autoPlacement({
alignment: align.value,
allowedPlacements: position.value,
})
];
if (showArrow.value) {
middleware.push(arrow({ element: arrowRef.value! }))
}
if (shiftOffset?.value !== undefined) {
middleware.push(shift())
}
return { ...baseOption, middleware }
};

const generateNoSpaceOptions = () => {
const [mainPostion, ...fallbackPostion] = position.value
const middleware = [...baseMiddleware];
if (showArrow.value) {
middleware.push(arrow({ element: arrowRef.value! }))
}
middleware.push(fallbackPostion.length ? flip({ fallbackPlacements: fallbackPostion }) : flip());
return { ...baseOption, placement: mainPostion, middleware }
}

const optionMap = {
'most-space': generateMostSpaceOptions,
'no-space': generateNoSpaceOptions,
}

const updateArrowPosition = (arrowEl: HTMLElement, placement: Placement, point: Point, overlayEl: HTMLElement) => {
const { x, y } = adjustArrowPosition(props.isArrowCenter, point, placement, overlayEl.getBoundingClientRect());
const staticSide = {
Expand All @@ -45,25 +93,16 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseO
});
};
const updatePosition = async () => {
const hostEl = <HTMLElement>props.origin;
const hostEl = <HTMLElement>(props.origin?.$el ?? props.origin);
const overlayEl = <HTMLElement>unref(overlayRef.value);
const arrowEl = <HTMLElement>unref(arrowRef.value);
const middleware = [
offset(props.offset),
autoPlacement({
alignment: props.align,
allowedPlacements: props.position,
}),
];
props.showArrow && middleware.push(arrow({ element: arrowEl }));
props.shiftOffset !== undefined && middleware.push(shift());
const { x, y, placement, middlewareData } = await computePosition(hostEl, overlayEl, {
strategy: 'fixed',
middleware,
});
if (!hostEl || !overlayEl) {
return;
}
const { x, y, placement, middlewareData } = await computePosition(hostEl, overlayEl, optionMap[placeStrategy.value]);
let applyX = x;
let applyY = y;
if (props.shiftOffset !== undefined) {
if (placeStrategy.value === 'most-space' && props.shiftOffset !== undefined) {
const { x: shiftX, y: shiftY } = middlewareData.shift;
shiftX < 0 && (applyX -= props.shiftOffset);
shiftX > 0 && (applyX += props.shiftOffset);
Expand All @@ -74,27 +113,107 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseO
Object.assign(overlayEl.style, { top: `${applyY}px`, left: `${applyX}px` });
props.showArrow && updateArrowPosition(arrowEl, placement, middlewareData.arrow, overlayEl);
};
watch(
() => props.modelValue,
() => {
if (props.modelValue && props.origin) {
originParent = getScrollParent(props.origin);
nextTick(updatePosition);
originParent?.addEventListener('scroll', updatePosition);
originParent !== window && window.addEventListener('scroll', updatePosition);
window.addEventListener('resize', updatePosition);
} else {
originParent?.removeEventListener('scroll', updatePosition);
originParent !== window && window.removeEventListener('scroll', updatePosition);
window.removeEventListener('resize', updatePosition);

const scrollCallBack = (e: Event) => {
const scrollElement = e.target as HTMLElement;
if (scrollElement?.contains(props.origin?.$el ?? props.origin)) {
if (props.appendToBodyScrollStrategy === 'repostion') {
updatePosition()
}
if (props.appendToBodyScrollStrategy === 'close') {
showOverlay.value = false;
emit('update:modelValue', false)
}
}
}

const updateWidth = (originEl: Element) => {
overlayWidth.value = originEl.getBoundingClientRect().width;
updatePosition()
};

const observeOrigin = () => {
if (fitOriginWidth.value && typeof window !== 'undefined') {
const originEl = props.origin?.$el ?? props.origin
if (originEl) {
originObserver = new window.ResizeObserver(() => updateWidth(originEl))
originObserver.observe(originEl)
}
}
}

const unobserveOrigin = () => {
const originEl = props.origin?.$el ?? props.origin
originEl && originObserver?.unobserve(originEl)
}

const observeOverlay = () => {
if (autoUpdatePosition.value && typeof window !== 'undefined') {
overlayObserver = new window.ResizeObserver(updatePosition)
originObserver.observe(overlayRef.value)
}
}
const unobserveOverlay = () => {
overlayRef.value && overlayObserver?.unobserve(overlayRef.value)
}


const checkBounds = (rect: DOMRect, scrollElement: HTMLElement) => {
if (!scrollElement.getBoundingClientRect) {
return false;
}
if (props.scrollElement) {
const containerRect = scrollElement.getBoundingClientRect()
const positionFixArr = [rect.height, 0, 0, 0];
const bounds = [
Math.round(rect.top + positionFixArr[0]) >= Math.round(containerRect.top),
Math.round(rect.right + positionFixArr[1]) <= Math.round(containerRect.right),
Math.round(rect.bottom + positionFixArr[2]) <= Math.round(containerRect.bottom),
Math.round(rect.left + positionFixArr[3]) >= Math.round(containerRect.left),
]
if (bounds.includes(false)) {
return true;
}
}
return false;
};
watch([() => props.modelValue, () => props.origin], () => {
if (props.modelValue && props.origin) {
originParent =
!props.scrollElement || props.scrollElement === 'auto'
? (getScrollParent((props.origin.$el ?? props.origin) as HTMLElement) as HTMLElement)
: props.scrollElement;
rect = ((props.origin.$el ?? props.origin) as HTMLElement).getBoundingClientRect()
if (checkBounds(rect, originParent)) {
showOverlay.value = false;
nextTick(() => {
emit('update:modelValue', false)
})
return
}
showOverlay.value = true
nextTick(updatePosition);
window.addEventListener('scroll', scrollCallBack, true);
window.addEventListener('resize', updatePosition);
observeOrigin()
nextTick(observeOverlay)
} else {
showOverlay.value = false;
originParent?.removeEventListener('scroll', scrollCallBack, true);
originParent?.removeEventListener('resize', updatePosition);
unobserveOrigin()
unobserveOverlay()
}
}
);
onUnmounted(() => {
originParent?.removeEventListener('scroll', updatePosition);
originParent !== window && window.removeEventListener('scroll', updatePosition);
window.removeEventListener('resize', updatePosition);
showOverlay.value = false;
originParent?.removeEventListener('scroll', scrollCallBack, true);
originParent?.removeEventListener('resize', updatePosition);
unobserveOrigin()
unobserveOverlay()

});

return { arrowRef, overlayRef, updatePosition };
return { arrowRef, overlayRef, styles, showOverlay, updatePosition };
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,20 @@
import { cloneVNode, defineComponent, withDirectives, inject } from 'vue';
import type { SetupContext, Ref } from 'vue';
import { POPPER_TRIGGER_TOKEN } from './popper-trigger-types';
import { getFirstValidChild } from './use-popper-trigger';
import { defineComponent } from 'vue';
import type { SetupContext } from 'vue';
import { usePopperTrigger } from './use-popper-trigger';

export default defineComponent({
name: 'DPopperTrigger',
setup(_, ctx: SetupContext) {
const { slots, attrs } = ctx;
const { slots } = ctx;
return () => {
const defaultSlot = slots.default?.(attrs);
const triggerRef = inject(POPPER_TRIGGER_TOKEN) as Ref<HTMLElement | null>;

const defaultSlot = slots.default?.();
if (!defaultSlot) {
return null;
}
const {addDirectiveToFirstValidChild} = usePopperTrigger();
addDirectiveToFirstValidChild(defaultSlot)

const firstValidChild = getFirstValidChild(defaultSlot);

if (!firstValidChild) {
return null;
}

return withDirectives(cloneVNode(firstValidChild, attrs), [
[
{
mounted(el) {
triggerRef.value = el;
},
updated(el) {
triggerRef.value = el;
},
unmounted() {
triggerRef.value = null;
},
},
],
]);
return defaultSlot;
};
},
});
Loading