Skip to content

Commit

Permalink
feat(Dropdown): 优化展开方向识别逻辑 (#1835) (#1836)
Browse files Browse the repository at this point in the history
  • Loading branch information
xingyan95 authored Apr 19, 2024
1 parent bdbc24d commit 06ff164
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 46 deletions.
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.

0 comments on commit 06ff164

Please sign in to comment.