Skip to content
Merged
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
8 changes: 8 additions & 0 deletions packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export const dropdownMenuProps = {
type: Function as PropType<() => boolean>,
default: (): boolean => true,
},
showAnimation: {
type: Boolean,
default: true,
},
overlayClass: {
type: String,
default: '',
},
};

export type DropdownMenuProps = ExtractPropTypes<typeof dropdownMenuProps>;
9 changes: 5 additions & 4 deletions packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ export default defineComponent({
props: dropdownMenuProps,
emits: ['update:modelValue'],
setup(props: DropdownMenuProps, { slots, attrs, emit }) {
const { modelValue, origin, position, align, offset, clickOutside } = toRefs(props);
const { modelValue, origin, position, align, offset, clickOutside, showAnimation, overlayClass } = toRefs(props);
const dropdownMenuRef = ref(null);

onClickOutside(dropdownMenuRef, (value) => {
if (clickOutside.value?.() && !origin.value.contains(value.target)) {
if (clickOutside.value?.() && !origin?.value?.contains(value.target)) {
emit('update:modelValue', false);
}
});

const currentPosition = ref('bottom');
const handlePositionChange = (pos) => {
const handlePositionChange = (pos: string) => {
currentPosition.value = pos.split('-')[0] === 'top' ? 'top' : 'bottom';
};
const styles = computed(() => ({
Expand All @@ -28,14 +28,15 @@ export default defineComponent({

return () => (
<Teleport to='body'>
<Transition name={`devui-dropdown-fade-${currentPosition.value}`}>
<Transition name={showAnimation.value ? `devui-dropdown-fade-${currentPosition.value}` : ''}>
<FlexibleOverlay
v-model={modelValue.value}
origin={origin?.value}
position={position.value}
align={align.value}
offset={offset.value}
onPositionChange={handlePositionChange}
class={overlayClass.value}
style={styles.value}>
<div ref={dropdownMenuRef} class='devui-dropdown-menu-wrap' {...attrs}>
{slots.default?.()}
Expand Down
22 changes: 21 additions & 1 deletion packages/devui-vue/devui/dropdown/src/dropdown-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropType, ExtractPropTypes, Ref } from 'vue';
import type { PropType, ExtractPropTypes, Ref, ComputedRef } from 'vue';

export type TriggerType = 'click' | 'hover' | 'manually';
export type CloseScopeArea = 'all' | 'blank' | 'none';
Expand Down Expand Up @@ -51,6 +51,18 @@ export const dropdownProps = {
type: Boolean,
default: false,
},
showAnimation: {
type: Boolean,
default: true,
},
overlayClass: {
type: String,
default: '',
},
destroyOnHide: {
type: Boolean,
default: true,
},
};

export type DropdownProps = ExtractPropTypes<typeof dropdownProps>;
Expand All @@ -63,3 +75,11 @@ export interface UseDropdownProps {
props: DropdownProps;
emit: EmitEvent;
}

export interface UseOverlayFn {
overlayModelValue: Ref<boolean>;
overlayShowValue: Ref<boolean>;
styles: ComputedRef<Record<string, string>>;
classes: ComputedRef<Record<string, boolean>>;
handlePositionChange: (pos: string) => void;
}
30 changes: 14 additions & 16 deletions packages/devui-vue/devui/dropdown/src/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineComponent, ref, toRefs, Transition, Teleport, computed } from 'vue';
import { defineComponent, ref, toRefs, Transition, Teleport } from 'vue';
import { dropdownProps, DropdownProps } from './dropdown-types';
import { useDropdown, useDropdownEvent } from './use-dropdown';
import { useDropdown, useDropdownEvent, useOverlayProps } from './use-dropdown';
import { FlexibleOverlay } from '../../overlay';
import './dropdown.scss';

Expand All @@ -11,23 +11,15 @@ export default defineComponent({
inheritAttrs: false,
props: dropdownProps,
emits: ['toggle'],
setup(props: DropdownProps, { slots, attrs, emit }) {
const { visible, position, align, offset } = toRefs(props);
setup(props: DropdownProps, { slots, attrs, emit, expose }) {
const { visible, position, align, offset, showAnimation } = toRefs(props);
const origin = ref<HTMLElement>();
const dropdownRef = ref<HTMLElement>();
const overlayRef = ref();
const id = `dropdown_${dropdownId++}`;
const isOpen = ref<boolean>(false);
const currentPosition = ref('bottom');
const handlePositionChange = (pos) => {
currentPosition.value = pos.includes('top') || pos.includes('end') ? 'top' : 'bottom';
};
const styles = computed(() => ({
transformOrigin: currentPosition.value === 'top' ? '0% 100%' : '0% 0%',
}));
const classes = computed(() => ({
'fade-in-bottom': isOpen.value && currentPosition.value === 'bottom',
'fade-in-top': isOpen.value && currentPosition.value === 'top',
}));

useDropdownEvent({
id,
isOpen,
Expand All @@ -37,15 +29,21 @@ export default defineComponent({
emit,
});
useDropdown(id, visible, isOpen, origin, dropdownRef, currentPosition, emit);
const { overlayModelValue, overlayShowValue, styles, classes, handlePositionChange } = useOverlayProps(props, currentPosition, isOpen);
expose({
updatePosition: () => overlayRef.value.updatePosition(),
});
return () => (
<>
<div ref={origin} class='devui-dropdown-toggle'>
{slots.default?.()}
</div>
<Teleport to='body'>
<Transition name={`devui-dropdown-fade-${currentPosition.value}`}>
<Transition name={showAnimation.value ? `devui-dropdown-fade-${currentPosition.value}` : ''}>
<FlexibleOverlay
v-model={isOpen.value}
v-model={overlayModelValue.value}
v-show={overlayShowValue.value}
ref={overlayRef}
origin={origin.value}
position={position.value}
align={align.value}
Expand Down
36 changes: 30 additions & 6 deletions packages/devui-vue/devui/dropdown/src/use-dropdown.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { watch, onMounted, onUnmounted, toRefs } from 'vue';
import { watch, onMounted, onUnmounted, toRefs, computed, ref } from 'vue';
import type { Ref } from 'vue';
import { getElement } from '../../shared/util/dom';
import { UseDropdownProps, EmitEvent } from './dropdown-types';
import { UseDropdownProps, EmitEvent, DropdownProps, UseOverlayFn } from './dropdown-types';

const dropdownMap = new Map();

Expand All @@ -20,12 +20,12 @@ export const useDropdownEvent = ({ id, isOpen, origin, dropdownRef, props, emit
isOpen.value = status;
emit('toggle', isOpen.value);
};
const handleLeave = async (elementType: 'origin' | 'dropdown', e?) => {
const handleLeave = async (elementType: 'origin' | 'dropdown', closeAll?: boolean) => {
await new Promise((resolve) => setTimeout(resolve, 50));
if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) {
return;
}
if (e) {
if (closeAll) {
[...dropdownMap.values()].reverse().forEach((item) => {
setTimeout(() => {
item.toggle?.();
Expand Down Expand Up @@ -66,7 +66,7 @@ export const useDropdownEvent = ({ id, isOpen, origin, dropdownRef, props, emit
subscribeEvent(originEl, 'click', () => toggle(!isOpen.value)),
subscribeEvent(dropdownEl, 'mouseleave', (e: MouseEvent) => {
if (closeOnMouseLeaveMenu.value && !dropdownMap.get(id).child?.contains(e.relatedTarget)) {
handleLeave('dropdown', e);
handleLeave('dropdown', true);
}
})
);
Expand All @@ -89,7 +89,7 @@ export const useDropdownEvent = ({ id, isOpen, origin, dropdownRef, props, emit
if (e.relatedTarget && (originEl?.contains(e.relatedTarget) || dropdownMap.get(id).child?.contains(e.relatedTarget))) {
return;
}
handleLeave('dropdown', e);
handleLeave('dropdown', true);
})
);
}
Expand Down Expand Up @@ -155,3 +155,27 @@ export function useDropdown(
dropdownMap.delete(id);
});
}

export function useOverlayProps(props: DropdownProps, currentPosition: Ref<string>, isOpen: Ref<boolean>): UseOverlayFn {
const { showAnimation, overlayClass, destroyOnHide } = toRefs(props);
const overlayModelValue = ref<boolean>(false);
const overlayShowValue = ref<boolean>(false);
const styles = computed(() => ({
transformOrigin: currentPosition.value === 'top' ? '0% 100%' : '0% 0%',
}));
const classes = computed(() => ({
'fade-in-bottom': showAnimation.value && isOpen.value && currentPosition.value === 'bottom',
'fade-in-top': showAnimation.value && isOpen.value && currentPosition.value === 'top',
[`${overlayClass.value}`]: true,
}));
const handlePositionChange = (pos: string) => {
currentPosition.value = pos.includes('top') || pos.includes('end') ? 'top' : 'bottom';
};

watch(isOpen, (isOpenVal) => {
overlayModelValue.value = destroyOnHide.value ? isOpenVal : true;
overlayShowValue.value = isOpenVal;
});

return { overlayModelValue, overlayShowValue, styles, classes, handlePositionChange };
}
5 changes: 3 additions & 2 deletions packages/devui-vue/devui/overlay/src/flexible-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ export const FlexibleOverlay = defineComponent({
inheritAttrs: false,
props: flexibleOverlayProps,
emits: ['update:modelValue', 'positionChange'],
setup(props: FlexibleOverlayProps, { slots, attrs, emit }) {
const { arrowRef, overlayRef } = useOverlay(props, emit);
setup(props: FlexibleOverlayProps, { slots, attrs, emit, expose }) {
const { arrowRef, overlayRef, updatePosition } = useOverlay(props, emit);
expose({ updatePosition });

return () =>
props.modelValue && (
Expand Down
2 changes: 1 addition & 1 deletion packages/devui-vue/devui/overlay/src/overlay-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const flexibleOverlayProps = {

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

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

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

Expand Down
5 changes: 3 additions & 2 deletions packages/devui-vue/devui/overlay/src/use-flexible-overlay.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ref, unref, watch, nextTick, onUnmounted } from 'vue';
import { FlexibleOverlayProps, Placement, Point, UseOverlayFn, EmitEventFn } from './overlay-types';
import { arrow, autoPlacement, computePosition, offset } from '@floating-ui/dom';
import { arrow, autoPlacement, computePosition, offset, shift } from '@floating-ui/dom';
import { getScrollParent } from './flexible-utils';

function adjustArrowPosition(isArrowCenter: boolean, point: Point, placement: Placement, originRect: any): Point {
Expand Down Expand Up @@ -48,6 +48,7 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseO
const overlayEl = <HTMLElement>unref(overlayRef.value);
const arrowEl = <HTMLElement>unref(arrowRef.value);
const middleware = [
shift(),
offset(props.offset),
autoPlacement({
alignment: props.align,
Expand Down Expand Up @@ -86,5 +87,5 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseO
window.removeEventListener('resize', updatePosition);
});

return { arrowRef, overlayRef };
return { arrowRef, overlayRef, updatePosition };
}
27 changes: 19 additions & 8 deletions packages/devui-vue/docs/components/dropdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,22 @@ export default defineComponent({
| align | `start \| end \| null` | `null` | 可选,对齐方式,默认居中对齐。若指定`start`对齐,当`start`位置放不下时,会自动调整为`end`对齐 |
| offset | `number \| OffsetOptions` | `4` | 可选,指定与触发元素的间距 |
| close-on-mouse-leave-menu | `boolean` | `false` | 可选,是否进入菜单后离开菜单的时候关闭菜单 |
| show-animation | `boolean` | `true` | 可选,控制是否显示动画 |
| overlay-class | `string` | `''` | 可选,自定义 overlay 的样式 |
| destroy-on-hide | `boolean` | `true` | 可选,是否在关闭 dropdown 时将其销毁 |

### d-dropdown 事件

| 事件名 | 说明 | 参数 |
| ------ | ------------------------------------------------------------- | ----------------------- |
| toggle | 组件收起和展开的布尔值,true 表示将要展开,false 表示将要关闭 | `EventEmitter<boolean>` |

### d-dropdown 方法

| 方法名 | 说明 | 类型 |
| -------------- | -------------------------------------------- | ------------ |
| updatePosition | 更新 dropdown 的位置,使其跟随在触发元素旁边 | `() => void` |

### d-dropdown 插槽

| 名称 | 说明 |
Expand All @@ -281,14 +290,16 @@ export default defineComponent({

### d-dropdown-menu 参数

| 参数 | 类型 | 默认 | 说明 |
| ------------- | ------------------------- | ------------ | --------------------------------------------------------------------------------------------- |
| origin | `HTMLElement` | `-` | 必选,必须指定 DropdownMenu 的关联元素 |
| v-model | `boolean` | `false` | 必选,指定 DropdownMenu 是否打开 |
| position | `Placement[]` | `['bottom']` | 可选,展开位置,若位置包含`start`或`end`,需通过`align`参数设置对齐方式 |
| align | `start \| end \| null` | `null` | 可选,对齐方式,默认居中对齐。若指定`start`对齐,当`start`位置放不下时,会自动调整为`end`对齐 |
| offset | `number \| OffsetOptions` | `4` | 可选,指定与触发元素的间距 |
| close-outside | `() => boolean` | `() => true` | 可选,点击外部区域的回调函数,默认返回 true,点击外部区域会关闭 DropdownMenu |
| 参数 | 类型 | 默认 | 说明 |
| -------------- | ------------------------- | ------------ | --------------------------------------------------------------------------------------------- |
| origin | `HTMLElement` | `-` | 必选,必须指定 DropdownMenu 的关联元素 |
| v-model | `boolean` | `false` | 必选,指定 DropdownMenu 是否打开 |
| position | `Placement[]` | `['bottom']` | 可选,展开位置,若位置包含`start`或`end`,需通过`align`参数设置对齐方式 |
| align | `start \| end \| null` | `null` | 可选,对齐方式,默认居中对齐。若指定`start`对齐,当`start`位置放不下时,会自动调整为`end`对齐 |
| offset | `number \| OffsetOptions` | `4` | 可选,指定与触发元素的间距 |
| close-outside | `() => boolean` | `() => true` | 可选,点击外部区域的回调函数,默认返回 true,点击外部区域会关闭 DropdownMenu |
| show-animation | `boolean` | `true` | 可选,控制是否显示动画 |
| overlay-class | `string` | `''` | 可选,自定义 overlay 的样式 |

### TriggerType 类型

Expand Down