diff --git a/packages/devui-vue/devui/dropdown/index.ts b/packages/devui-vue/devui/dropdown/index.ts index 0075b1cbba..a99d4509c3 100644 --- a/packages/devui-vue/devui/dropdown/index.ts +++ b/packages/devui-vue/devui/dropdown/index.ts @@ -1,18 +1,16 @@ -import type { App } from 'vue' +import type { App } from 'vue'; +import Dropdown from './src/dropdown'; +import DropdownMenu from './src/dropdown-menu'; +export * from './src/dropdown-menu-types'; -import Dropdown from './src/dropdown' - -Dropdown.install = function (app: App): void { - app.component(Dropdown.name, Dropdown) -} - -export { Dropdown } +export { Dropdown, DropdownMenu }; export default { title: 'Dropdown 下拉菜单', category: '导航', - status: '10%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + status: '10%', install(app: App): void { - app.use(Dropdown as any) - } -} + app.component(Dropdown.name, Dropdown); + app.component(DropdownMenu.name, DropdownMenu); + }, +}; diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-directive.ts b/packages/devui-vue/devui/dropdown/src/dropdown-directive.ts deleted file mode 100644 index 38d67100b7..0000000000 --- a/packages/devui-vue/devui/dropdown/src/dropdown-directive.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const dDropdownDirective = { - -}; - -export const dDropdownMenuDirective = { - -}; diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts new file mode 100644 index 0000000000..ac89d8711a --- /dev/null +++ b/packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts @@ -0,0 +1,47 @@ +import type { PropType, ExtractPropTypes } from 'vue'; + +export type CloseScopeArea = 'all' | 'blank' | 'none'; +export type Placement = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'top-start' + | 'top-end' + | 'right-start' + | 'right-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end'; +export type Alignment = 'start' | 'end'; +export type OffsetOptions = { mainAxis?: number; crossAxis?: number }; + +export const dropdownMenuProps = { + modelValue: { + type: Boolean, + default: false, + }, + origin: { + type: Object as PropType, + require: true, + }, + position: { + type: Array as PropType>, + default: ['bottom'], + }, + align: { + type: String as PropType | null, + default: null, + }, + offset: { + type: [Number, Object] as PropType, + default: 4, + }, + clickOutside: { + type: Function as PropType<() => boolean>, + default: (): boolean => true, + }, +}; + +export type DropdownMenuProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx b/packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx new file mode 100644 index 0000000000..34c6de7756 --- /dev/null +++ b/packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx @@ -0,0 +1,48 @@ +import { defineComponent, ref, toRefs, Transition, Teleport, computed } from 'vue'; +import { onClickOutside } from '@vueuse/core'; +import { FlexibleOverlay } from '../../overlay'; +import { dropdownMenuProps, DropdownMenuProps } from './dropdown-menu-types'; + +export default defineComponent({ + name: 'DDropdownMenu', + inheritAttrs: false, + props: dropdownMenuProps, + emits: ['update:modelValue'], + setup(props: DropdownMenuProps, { slots, attrs, emit }) { + const { modelValue, origin, position, align, offset, clickOutside } = toRefs(props); + const dropdownMenuRef = ref(null); + + onClickOutside(dropdownMenuRef, (value) => { + if (clickOutside.value?.() && !origin.value.contains(value.target)) { + emit('update:modelValue', false); + } + }); + + const currentPosition = ref('bottom'); + const handlePositionChange = (pos) => { + currentPosition.value = pos.split('-')[0] === 'top' ? 'top' : 'bottom'; + }; + const styles = computed(() => ({ + transformOrigin: currentPosition.value === 'top' ? '0% 100%' : '0% 0%', + })); + + return () => ( + + + +
+ {slots.default?.()} +
+
+
+
+ ); + }, +}); diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts index 6666c69c13..b0eca132ad 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts +++ b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts @@ -1,46 +1,65 @@ -import type { PropType, ExtractPropTypes, ComponentPublicInstance } from 'vue' +import type { PropType, ExtractPropTypes, Ref } from 'vue'; export type TriggerType = 'click' | 'hover' | 'manually'; export type CloseScopeArea = 'all' | 'blank' | 'none'; +export type Placement = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'top-start' + | 'top-end' + | 'right-start' + | 'right-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end'; +export type Alignment = 'start' | 'end'; +export type OffsetOptions = { mainAxis?: number; crossAxis?: number }; -export const dropdownProps = { - origin: { - type: Object as PropType - }, +type ReadonlyRef = Readonly>; - isOpen: { - type: Boolean, - default: false - }, +export type EmitEvent = (event: 'toggle', result: boolean) => void; - disabled: { +export const dropdownProps = { + visible: { type: Boolean, - default: false + default: false, }, - trigger: { type: String as PropType, - default: 'click' + default: 'click', }, - closeScope: { type: String as PropType, - default: 'all' + default: 'all', + }, + position: { + type: Array as PropType>, + default: ['bottom'], + }, + align: { + type: String as PropType | null, + default: null, + }, + offset: { + type: [Number, Object] as PropType, + default: 4, }, - closeOnMouseLeaveMenu: { type: Boolean, - default: false + default: false, }, +}; - showAnimation: { - type: Boolean, - default: true - }, - width: { - type: [Number, String], - default: '102px' - } -} as const +export type DropdownProps = ExtractPropTypes; -export type DropdownProps = ExtractPropTypes +export interface UseDropdownProps { + id: string; + isOpen: Ref; + origin: ReadonlyRef; + dropdownRef: ReadonlyRef; + props: DropdownProps; + emit: EmitEvent; +} diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.scss b/packages/devui-vue/devui/dropdown/src/dropdown.scss index 5b1a269bc7..78c93a6141 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown.scss +++ b/packages/devui-vue/devui/dropdown/src/dropdown.scss @@ -1,4 +1,8 @@ -@import '../../styles-var/devui-var'; +@import '../../styles-var/devui-var.scss'; + +.devui-dropdown-toggle { + display: inline-block; +} .devui-dropdown span { &.icon-chevron-down, @@ -26,38 +30,80 @@ } } -.devui-dropdown-fade { - @mixin d-dropdown-fade-animation { - animation-name: d-dropdown-fade; - animation-duration: 0.3s; - } - @keyframes d-dropdown-fade { - 0% { - opacity: 0; - transform: scaleY(0.9999) scaleY(0); - } +.fade-in-top { + animation: fadeInTop 0.2s cubic-bezier(0.16, 0.75, 0.5, 1); +} - 100% { - opacity: 1; - transform: scaleY(0.8) scaleY(4px); - } +.fade-in-bottom { + animation: fadeInBottom 0.2s cubic-bezier(0.16, 0.75, 0.5, 1); +} + +@keyframes fadeInTop { + from { + opacity: 0.8; + transform: scaleY(0.8) translateY(4px); } - &-enter { - opacity: 0; + to { + opacity: 1; + transform: scaleY(0.9999) translateY(0); } +} - &-enter-active { - @include d-dropdown-fade-animation; +@keyframes fadeInBottom { + from { + opacity: 0.8; + transform: scaleY(0.8) translateY(-4px); } - &-leave { + to { opacity: 1; + transform: scaleY(0.9999) translateY(0); } +} - &-leave-active { - @include d-dropdown-fade-animation; +.devui-dropdown-fade { + &-bottom { + &-enter-from, + &-leave-to { + opacity: 0.8; + transform: scaleY(0.8) translateY(-4px); + } - animation-direction: reverse; + &-enter-to, + &-leave-from { + opacity: 1; + transform: scaleY(0.9999) translateY(0); + } + + &-enter-active { + transition: transform 0.2s cubic-bezier(0.16, 0.75, 0.5, 1), opacity 0.2s cubic-bezier(0.16, 0.75, 0.5, 1); + } + + &-leave-active { + transition: transform 0.2s cubic-bezier(0.5, 0, 0.84, 0.25), opacity 0.2s cubic-bezier(0.5, 0, 0.84, 0.25); + } + } + + &-top { + &-enter-from, + &-leave-to { + opacity: 0.8; + transform: scaleY(0.8) translateY(4px); + } + + &-enter-to, + &-leave-from { + opacity: 1; + transform: scaleY(0.9999) translateY(0); + } + + &-enter-active { + transition: transform 0.2s cubic-bezier(0.16, 0.75, 0.5, 1), opacity 0.2s cubic-bezier(0.16, 0.75, 0.5, 1); + } + + &-leave-active { + transition: transform 0.2s cubic-bezier(0.5, 0, 0.84, 0.25), opacity 0.2s cubic-bezier(0.5, 0, 0.84, 0.25); + } } } diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.tsx b/packages/devui-vue/devui/dropdown/src/dropdown.tsx index ea7b0e024f..53f2895fb7 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown.tsx +++ b/packages/devui-vue/devui/dropdown/src/dropdown.tsx @@ -1,72 +1,65 @@ -import { defineComponent, watch, ref, toRefs, Transition, computed } from 'vue' -import { dropdownProps, DropdownProps } from './dropdown-types' -import { useDropdown } from './use-dropdown'; - +import { defineComponent, ref, toRefs, Transition, Teleport, computed } from 'vue'; +import { dropdownProps, DropdownProps } from './dropdown-types'; +import { useDropdown, useDropdownEvent } from './use-dropdown'; import { FlexibleOverlay } from '../../overlay'; +import './dropdown.scss'; -import './dropdown.scss' +let dropdownId = 1; export default defineComponent({ name: 'DDropdown', + inheritAttrs: false, props: dropdownProps, - emits: [], - setup(props: DropdownProps, ctx) { - const { + emits: ['toggle'], + setup(props: DropdownProps, { slots, attrs, emit }) { + const { visible, position, align, offset } = toRefs(props); + const origin = ref(); + const dropdownRef = ref(); + const id = `dropdown_${dropdownId++}`; + const isOpen = ref(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, origin, - trigger, - closeScope, - closeOnMouseLeaveMenu, - } = toRefs(props); - - const visible = ref(false); - watch(isOpen, (value) => { - visible.value = value; - }, { immediate: true }); - - const position = { - originX: 'center', - originY: 'bottom', - overlayX: 'center', - overlayY: 'top' - } as const; - - const { dropdownEl } = useDropdown({ - visible, - origin, - trigger, - closeScope, - closeOnMouseLeaveMenu, - }); - - const animatedVisible = computed(() => { - return props.showAnimation ? visible.value : true; - }); - - const wrapStyle = computed(() => typeof props.width === 'string' ? { - width: props.width, - }: { - width: `${props.width}px`, + dropdownRef, + props, + emit, }); - + useDropdown(id, visible, isOpen, origin, dropdownRef, currentPosition, emit); return () => ( - - -
- {ctx.slots.default?.()} -
-
-
+ <> +
+ {slots.default?.()} +
+ + + +
+ {slots.menu?.()} +
+
+
+
+ ); - } -}) + }, +}); diff --git a/packages/devui-vue/devui/dropdown/src/use-dropdown.ts b/packages/devui-vue/devui/dropdown/src/use-dropdown.ts index 596a457931..0bae60f6f0 100644 --- a/packages/devui-vue/devui/dropdown/src/use-dropdown.ts +++ b/packages/devui-vue/devui/dropdown/src/use-dropdown.ts @@ -1,120 +1,157 @@ -import { Ref, ref, watch } from 'vue'; +import { watch, onMounted, onUnmounted, toRefs } from 'vue'; +import type { Ref } from 'vue'; import { getElement } from '../../shared/util/dom'; -import { CloseScopeArea, TriggerType } from './dropdown-types'; +import { UseDropdownProps, EmitEvent } from './dropdown-types'; -function subscribeEvent(dom: Element | Document, type: string, callback: (event: E) => void) { - dom?.addEventListener(type, callback as any); - return () => { - dom?.removeEventListener(type, callback as any); - } -} +const dropdownMap = new Map(); -type ReadonlyRef = Readonly>; - -interface UseDropdownProps { - visible: Ref - trigger: ReadonlyRef - origin: ReadonlyRef - closeScope: ReadonlyRef - closeOnMouseLeaveMenu: ReadonlyRef -} - -interface UseDropdownResult { - dropdownEl: Ref +function subscribeEvent(dom: Element | Document, type: string, callback: (event: any) => void) { + dom?.addEventListener(type, callback); + return () => { + dom?.removeEventListener(type, callback); + }; } -export const useDropdown = ({ - visible, - trigger, - origin, - closeScope, - closeOnMouseLeaveMenu -}: UseDropdownProps): UseDropdownResult => { - const dropdownElRef = ref(); - - const closeByScope = () => { - if (closeScope.value === 'none') { +export const useDropdownEvent = ({ id, isOpen, origin, dropdownRef, props, emit }: UseDropdownProps): void => { + let overlayEnter = false; + let originEnter = false; + const { trigger, closeScope, closeOnMouseLeaveMenu } = toRefs(props); + const toggle = (status: boolean) => { + isOpen.value = status; + emit('toggle', isOpen.value); + }; + const handleLeave = async (elementType: 'origin' | 'dropdown', e?) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) { return; } - visible.value = false; - } - watch( - [trigger, origin, dropdownElRef], - ([trigger, origin, dropdownEl], ov, onInvalidate) => { - const originEl = getElement(origin); - if (!originEl || !dropdownEl) { - return; - } - const subscriptions = [ - subscribeEvent(dropdownEl, 'click', () => { - if (closeScope.value === 'all') { - visible.value = false; + if (e) { + [...dropdownMap.values()].reverse().forEach((item) => { + setTimeout(() => { + item.toggle?.(); + }, 0); + }); + } + toggle(false); + }; + watch([trigger, origin, dropdownRef], ([triggerVal, originVal, dropdownEl], ov, onInvalidate) => { + const originEl = getElement(originVal); + const subscriptions = []; + setTimeout(() => { + subscriptions.push( + subscribeEvent(document, 'click', (e: Event) => { + const dropdownValues = [...dropdownMap.values()]; + if ( + !isOpen.value || + closeScope.value === 'none' || + (dropdownEl.contains(e.target) && closeScope.value === 'blank') || + (dropdownValues.some((item) => item.toggleEl?.contains(e.target)) && + dropdownValues.some((item) => item.menuEl?.contains(e.target))) + ) { + return; + } + [...dropdownMap.values()].reverse().forEach((item) => { + setTimeout(() => { + if (!item.toggleEl?.contains(e.target)) { + item.toggle?.(); + } + }, 0); + }); + overlayEnter = false; + }) + ); + }, 0); + if (triggerVal === 'click') { + subscriptions.push( + subscribeEvent(originEl, 'click', () => toggle(!isOpen.value)), + subscribeEvent(dropdownEl, 'mouseleave', (e: MouseEvent) => { + if (closeOnMouseLeaveMenu.value && !dropdownMap.get(id).child?.contains(e.relatedTarget)) { + handleLeave('dropdown', e); } + }) + ); + } else if (triggerVal === 'hover') { + subscriptions.push( + subscribeEvent(originEl, 'mouseenter', () => { + originEnter = true; + toggle(true); }), - ]; - - if (trigger === 'click') { - // 点击触发 - subscriptions.push( - subscribeEvent(originEl, 'click', () => visible.value = !visible.value), - subscribeEvent(document, 'click', (e) => { - if (!visible.value) { - return; - } - const target = e.target as HTMLElement; - const isContain = originEl.contains(target) || dropdownEl.contains(target); - if (isContain) { - return; - } - closeByScope(); - }), - subscribeEvent(dropdownEl, 'mouseleave', () => { - // 判断鼠标是否已经进入 origin - if (closeOnMouseLeaveMenu.value) { - visible.value = false; - } - }) - ); - } else if (trigger === 'hover') { - // 鼠标悬浮触发 - let overlayEnter = false; - let originEnter = false; - const handleLeave = async (elementType: 'origin' | 'dropdown') => { - // 由于关联元素和 dropdown 元素间有间距, - // 悬浮时在两者之间移动可能会导致多次关闭打开, - // 所以需要给关闭触发节流。 - await new Promise((resolve) => setTimeout(resolve, 50)); - if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) { + subscribeEvent(originEl, 'mouseleave', () => { + originEnter = false; + handleLeave('origin'); + }), + subscribeEvent(dropdownEl, 'mouseenter', () => { + overlayEnter = true; + isOpen.value = true; + }), + subscribeEvent(dropdownEl, 'mouseleave', (e: MouseEvent) => { + overlayEnter = false; + if (e.relatedTarget && (originEl?.contains(e.relatedTarget) || dropdownMap.get(id).child?.contains(e.relatedTarget))) { return; } - closeByScope(); - }; - subscriptions.push( - subscribeEvent(originEl, 'mouseenter', () => { - originEnter = true; - visible.value = true; - }), - subscribeEvent(originEl, 'mouseleave', () => { - originEnter = false; - // 判断鼠标是否已经进入 overlay - if (!closeOnMouseLeaveMenu.value) { - handleLeave('origin'); - } - }), - subscribeEvent(dropdownEl, 'mouseenter', () => { - overlayEnter = true; - visible.value = true; - }), - subscribeEvent(dropdownEl, 'mouseleave', () => { - overlayEnter = false; - // 判断鼠标是否已经进入 origin - handleLeave('dropdown'); - }) - ); - } - onInvalidate(() => subscriptions.forEach(v => v())); + handleLeave('dropdown', e); + }) + ); } - ); + onInvalidate(() => subscriptions.forEach((v) => v())); + }); +}; - return { dropdownEl: dropdownElRef }; +export function useDropdown( + id: string, + visible: Ref, + isOpen: Ref, + origin: Ref, + dropdownRef: Ref, + popDirection: Ref, + emit: EmitEvent +): void { + const calcPopDirection = (dropdownEl: HTMLElement) => { + const elementHeight = dropdownEl.offsetHeight; + const bottomDistance = window.innerHeight - origin.value.getBoundingClientRect().bottom; + const isBottomEnough = bottomDistance >= elementHeight; + if (!isBottomEnough) { + popDirection.value = 'top'; + } else { + popDirection.value = 'bottom'; + } + }; + + watch( + visible, + (newVal, oldVal) => { + if (oldVal === undefined) { + return; + } + isOpen.value = newVal; + emit('toggle', isOpen.value); + }, + { immediate: true } + ); + watch([isOpen, dropdownRef], ([isOpenVal, dropdownEl]) => { + if (isOpenVal) { + dropdownMap.set(id, { + ...dropdownMap.get(id), + menuEl: dropdownEl, + toggle: () => { + isOpen.value = false; + emit('toggle', isOpen.value); + }, + }); + for (const value of dropdownMap.values()) { + if (value.menuEl?.contains(origin.value)) { + value.child = dropdownEl; + } + } + } + if (dropdownEl) { + calcPopDirection(dropdownEl); + } + }); + onMounted(() => { + dropdownMap.set(id, { toggleEl: origin.value }); + }); + onUnmounted(() => { + dropdownMap.delete(id); + }); } diff --git a/packages/devui-vue/docs/components/dropdown/index.md b/packages/devui-vue/docs/components/dropdown/index.md index 8dd187a5b8..19820ff951 100644 --- a/packages/devui-vue/docs/components/dropdown/index.md +++ b/packages/devui-vue/docs/components/dropdown/index.md @@ -1,14 +1,68 @@ # Dropdown 下拉菜单 + 按下弹出列表组件。 -### 何时使用 +#### 何时使用 + 当页面上的操作命令过多时,用此组件可以收纳操作元素。点击或移入触点,会出现一个下拉菜单。可在列表中进行选择,并执行相应的命令。 +### 基本用法 +:::demo 组件默认插槽中定义触发元素,`menu`插槽中定义菜单。默认通过点击触发元素展开菜单。 -### 基本用法 +```vue + -:::demo + + + +``` + +::: + +### 触发方式 + +:::demo 组件默认通过`click`方式展开;`hover`方式为鼠标移上触发元素展开菜单;`manually`方式为手动控制,通过设置`visible`来控制组件是否展开。 ```vue + +``` + +::: + +### 可关闭区域 + +:::demo 通过`close-scope`参数设置点击关闭区域,默认值为`all`表示点击菜单内外都关闭,`blank`点击非菜单空白才关闭,`none`菜单内外均不关闭仅下拉按键可以关闭。 + +```vue + + + +``` - +:::demo + +```vue + + + ``` + ::: -### d-dropdown +### 单独使用 DropdownMenu -d-dropdown 参数 +:::demo -| 参数 | 类型 | 默认 | 说明 | -| --------------------- | ------------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------- | -| origin | `Element \| ComponentPublicInstance` | 无 | 必选,必须指定 dropdown 的关联元素 | -| isOpen | `boolean` | `false` | 可选,可以显示指定 dropdown 是否打开 | -| disabled | `boolean` | `false` | 可选,设置为 true 禁用 dropdown | -| trigger | `TriggerType` | `click` | 可选,dropdown 触发方式, click 为点击,hover 为悬停(也包含点击)、manually 为完全手动控制 | -| closeScope | `CloseScopeArea` | `all` | 可选,点击关闭区域,blank 点击非菜单空白才关闭, all 点击菜单内外都关闭,none 菜单内外均不关闭仅下拉按键可以关闭 | -| closeOnMouseLeaveMenu | `boolean` | `false` | 可选,是否进入菜单后离开菜单的时候关闭菜单 | -| showAnimation | `boolean` | `true` | 可选,是否开启动画 | -| width | `number \| string` | `102px` | 可选,对 dropdown 内容的宽度进行自定义 | +```vue + + + +``` + +::: + +### d-dropdown 参数 + +| 参数 | 类型 | 默认 | 说明 | +| ------------------------- | ------------------------- | ------------ | --------------------------------------------------------------------------------------------- | +| visible | `boolean` | `false` | 可选,可以显示指定 dropdown 是否打开 | +| trigger | `TriggerType` | `click` | 可选,dropdown 触发方式, click 为点、hover 为悬停、manually 为完全手动控制 | +| close-scope | `CloseScopeArea` | `all` | 可选,点击关闭区域,blank 点击非菜单空白关闭, all 点击菜单内外关闭,none 仅触发元素关闭 | +| position | `Placement[]` | `['bottom']` | 可选,展开位置,若位置包含`start`或`end`,需通过`align`参数设置对齐方式 | +| align | `start \| end \| null` | `null` | 可选,对齐方式,默认居中对齐。若指定`start`对齐,当`start`位置放不下时,会自动调整为`end`对齐 | +| offset | `number \| OffsetOptions` | `4` | 可选,指定与触发元素的间距 | +| close-on-mouse-leave-menu | `boolean` | `false` | 可选,是否进入菜单后离开菜单的时候关闭菜单 | + +### d-dropdown 事件 + +| 事件名 | 说明 | 参数 | +| ------ | ------------------------------------------------------------- | ----------------------- | +| toggle | 组件收起和展开的布尔值,true 表示将要展开,false 表示将要关闭 | `EventEmitter` | + +### d-dropdown 插槽 + +| 名称 | 说明 | +| ------- | -------------------- | +| default | 菜单打开时的触发元素 | +| menu | 下拉菜单的内容 | + +### 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 | + +### TriggerType 类型 -TriggerType 类型 ```typescript type TriggerType = 'click' | 'hover' | 'manually'; ``` -CloseScopeArea 类型 +### CloseScopeArea 类型 + ```typescript type CloseScopeArea = 'all' | 'blank' | 'none'; -``` \ No newline at end of file +``` + +### Placement 类型 + +```typescript +type Placement = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'top-start' + | 'top-end' + | 'right-start' + | 'right-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end'; +``` + +### OffsetOptions 类型 + +```typescript +type OffsetOptions = { mainAxis?: number; crossAxis?: number }; +```