Skip to content
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

feat(Dropdown): 优化展开方向识别逻辑 #1835

Merged
merged 10 commits into from
Apr 19, 2024
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ref, unref, watch, nextTick, onUnmounted } from 'vue';
import { arrow, autoPlacement, computePosition, offset, shift } from '@floating-ui/dom';
import { ref, unref, watch, nextTick, onUnmounted, toRefs } from 'vue';
import { arrow, computePosition, offset, flip } from '@floating-ui/dom';
import { FlexibleOverlayProps, Placement, Point, UseOverlayFn, EmitEventFn, Rect } from './flexible-overlay-types';
import { getScrollParent } from './utils';

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

export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseOverlayFn {
const { position, showArrow } = toRefs(props);
const overlayRef = ref<HTMLElement | undefined>();
const arrowRef = ref<HTMLElement | undefined>();
let originParent = null;

const updateArrowPosition = (arrowEl: HTMLElement, placement: Placement, point: Point, overlayEl: HTMLElement) => {
const { x, y } = adjustArrowPosition(props.isArrowCenter, point, placement, overlayEl.getBoundingClientRect());
const staticSide = {
Expand All @@ -48,54 +48,46 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseO
const hostEl = <HTMLElement>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());
if (!overlayEl) {
return;

const [mainPosition, ...fallbackPosition] = position.value;
const middleware = [offset(props.offset)];
middleware.push(fallbackPosition.length ? flip({ fallbackPlacements: fallbackPosition }) : flip());
if (showArrow.value) {
middleware.push(arrow({ element: arrowRef.value! }));
}
const { x, y, placement, middlewareData } = await computePosition(hostEl, overlayEl, {
strategy: 'fixed',
placement: mainPosition,
middleware,
});
let applyX = x;
let applyY = y;
if (props.shiftOffset !== undefined) {
const { x: shiftX, y: shiftY } = middlewareData.shift;
shiftX < 0 && (applyX -= props.shiftOffset);
shiftX > 0 && (applyX += props.shiftOffset);
shiftY < 0 && (applyY -= props.shiftOffset);
shiftY > 0 && (applyY += props.shiftOffset);
}
emit('positionChange', placement);
Object.assign(overlayEl.style, { top: `${applyY}px`, left: `${applyX}px` });
props.showArrow && updateArrowPosition(arrowEl, placement, middlewareData.arrow, overlayEl);
};

const scrollCallback = (e: Event) => {
const scrollElement = e.target as HTMLElement;
if (scrollElement?.contains(props.origin?.$el ?? props.origin)) {
updatePosition();
}
};
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('scroll', scrollCallback, true);
window.addEventListener('resize', updatePosition);
} else {
originParent?.removeEventListener('scroll', updatePosition);
originParent !== window && window.removeEventListener('scroll', updatePosition);
window.removeEventListener('scroll', scrollCallback, true);
window.removeEventListener('resize', updatePosition);
}
}
);
onUnmounted(() => {
originParent?.removeEventListener('scroll', updatePosition);
originParent !== window && window.removeEventListener('scroll', updatePosition);
window.removeEventListener('scroll', scrollCallback, true);
window.removeEventListener('resize', updatePosition);
});

Expand Down
15 changes: 7 additions & 8 deletions packages/devui-vue/docs/components/dropdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

```vue
<template>
<d-dropdown style="width: 100px;" :position="position" align="start">
<d-dropdown style="width: 100px;" :position="position">
<d-button>Click Me</d-button>
<template #menu>
<ul class="list-menu">
Expand All @@ -31,7 +31,7 @@ import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
return {
position: ref(['bottom-start', 'top-start']),
position: ref(['bottom-start', 'right','top-end']),
};
},
});
Expand Down Expand Up @@ -74,7 +74,7 @@ export default defineComponent({
</d-radio-group>
</div>
</div>
<d-dropdown :visible="isOpen" :trigger="trigger" :position="position" align="start" @toggle="handleToggle">
<d-dropdown :visible="isOpen" :trigger="trigger" :position="position" @toggle="handleToggle">
<d-button class="mt-1">More</d-button>
<template #menu>
<ul class="list-menu">
Expand Down Expand Up @@ -125,7 +125,7 @@ export default defineComponent({
</d-radio-group>
</div>
</div>
<d-dropdown :close-scope="closeScope" :position="position" align="start" style="width: 100px;">
<d-dropdown :close-scope="closeScope" :position="position" style="width: 100px;">
<d-button class="mt-1">More</d-button>
<template #menu>
<ul class="list-menu">
Expand Down Expand Up @@ -165,14 +165,14 @@ export default defineComponent({
<d-button>Click Me</d-button>
<template #menu>
<ul class="list-menu">
<d-dropdown :position="position" :offset="0" align="start">
<d-dropdown :position="position" :offset="0">
<li class="menu-item">
Item 1
<i class="icon icon-chevron-right"></i>
</li>
<template #menu>
<ul class="list-menu">
<d-dropdown :position="position" :offset="0" align="start">
<d-dropdown :position="position" :offset="0" >
<li class="menu-item">
Item 1-1
<i class="icon icon-chevron-right"></i>
Expand Down Expand Up @@ -261,8 +261,7 @@ export default defineComponent({
| visible | `boolean` | false | 可选,可以显式指定 dropdown 是否打开 | [触发方式](#触发方式) |
| trigger | [TriggerType](#triggertype) | click | 可选,dropdown 触发方式, click 为点、hover 为悬停、manually 为完全手动控制 | [触发方式](#触发方式) |
| close-scope | [CloseScopeArea](#closescopearea) | all | 可选,点击关闭区域,blank 点击非菜单空白关闭, all 点击菜单内外关闭,none 仅触发元素关闭 | [可关闭区域](#可关闭区域) |
| position | [Placement[]](#placement) | ['bottom'] | 可选,展开位置,若位置包含`start`或`end`,需通过`align`参数设置对齐方式 | [基本用法](#基本用法) |
| align | `start \| end \| null` | null | 可选,对齐方式,默认居中对齐。若指定`start`对齐,当`start`位置放不下时,会自动调整为`end`对齐 | [基本用法](#基本用法) |
| position | [Placement[]](#placement) | ['bottom'] | 可选,展开位置,按照顺序自动选择位置 | [基本用法](#基本用法) |
| offset | `number` \| [OffsetOptions](#offsetoptions) | 4 | 可选,指定与触发元素的间距 | [多级菜单](#多级菜单) |
| shift-offset | `number` | -- | 可选,当设置该参数时,表示启用贴边功能,当指定的 position 放不下时,选择最近的视图边界对齐,此参数可设置相对视图边界的偏移量 |
| close-on-mouse-leave-menu | `boolean` | false | 可选,是否进入菜单后离开菜单的时候关闭菜单 |
Expand Down
4 changes: 2 additions & 2 deletions packages/devui-vue/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vue-devui",
"version": "1.6.14",
"version": "1.6.15",
"license": "MIT",
"description": "DevUI components based on Vite and Vue3",
"keywords": [
Expand Down Expand Up @@ -47,7 +47,7 @@
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
"@devui-design/icons": "^1.3.0",
"@floating-ui/dom": "^0.4.4",
"@floating-ui/dom": "1.2.5",
"@iktakahiro/markdown-it-katex": "^4.0.1",
"@types/codemirror": "0.0.97",
"@types/lodash-es": "^4.17.4",
Expand Down
20 changes: 13 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading