From e8e5385ead526f30c5926e825f846bd4d252e916 Mon Sep 17 00:00:00 2001 From: zcating Date: Mon, 1 Nov 2021 18:30:58 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(overlay):=20=E6=96=B0=E5=A2=9E=20hasBa?= =?UTF-8?q?ckdrop=20=E5=92=8C=20backgroundStyle=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui/overlay/src/fixed-overlay.tsx | 12 +- .../devui/overlay/src/flexible-overlay.tsx | 114 ++++-------------- .../devui/overlay/src/overlay-types.ts | 84 ++++++++++++- .../devui-vue/devui/overlay/src/overlay.scss | 4 + packages/devui-vue/devui/overlay/src/utils.ts | 6 +- packages/devui-vue/devui/shared/util/dom.ts | 31 +++++ 6 files changed, 148 insertions(+), 103 deletions(-) create mode 100644 packages/devui-vue/devui/shared/util/dom.ts diff --git a/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx b/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx index 4a84b1c4..c4ccfa03 100644 --- a/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx +++ b/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx @@ -1,19 +1,13 @@ import { defineComponent, ref, renderSlot, CSSProperties, PropType } from 'vue'; import { CommonOverlay } from './common-overlay'; -import { overlayProps } from './overlay-types'; +import { fixedOverlayProps, FixedOverlayProps } from './overlay-types'; import { useOverlayLogic } from './utils'; import './overlay.scss'; export const FixedOverlay = defineComponent({ name: 'DFixedOverlay', - props: { - ...overlayProps, - overlayStyle: { - type: Object as PropType, - default: undefined, - }, - }, - setup(props, ctx) { + props: fixedOverlayProps, + setup(props: FixedOverlayProps, ctx) { const { backgroundClass, overlayClass, diff --git a/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx b/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx index 19c55c4f..42556b58 100644 --- a/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx +++ b/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx @@ -1,5 +1,4 @@ import { - ComponentPublicInstance, CSSProperties, defineComponent, getCurrentInstance, @@ -7,41 +6,26 @@ import { nextTick, onBeforeUnmount, onMounted, - PropType, reactive, ref, - Ref, renderSlot, toRef, watch, } from 'vue'; import { CommonOverlay } from './common-overlay'; -import { overlayProps } from './overlay-types'; +import { OriginOrDomRef, flexibleOverlayProps, FlexibleOverlayProps, Point, Origin, ConnectionPosition } from './overlay-types'; import { useOverlayLogic } from './utils'; +import { getElement, isComponent } from '../../shared/util/dom'; + /** * 弹性的 Overlay,用于连接固定的和相对点 */ export const FlexibleOverlay = defineComponent({ name: 'DFlexibleOverlay', - props: { - origin: { - type: Object as PropType, - require: true, - }, - position: { - type: Object as PropType, - default: () => ({ - originX: 'left', - originY: 'top', - overlayX: 'left', - overlayY: 'top', - }), - }, - ...overlayProps, - }, + props: flexibleOverlayProps, emits: ['onUpdate:visible'], - setup(props, ctx) { + setup(props: FlexibleOverlayProps, ctx) { // lift cycle const overlayRef = ref(null); const positionedStyle = reactive({ position: 'absolute' }); @@ -70,10 +54,12 @@ export const FlexibleOverlay = defineComponent({ positionedStyle.left = `${point.x}px`; positionedStyle.top = `${point.y}px`; }; - const handleChange = () => - handleRectChange(overlay.getBoundingClientRect()); + const handleChange = () => handleRectChange(overlay.getBoundingClientRect()); + + const visibleRef = toRef(props, 'visible'); + const positionRef = toRef(props, 'position'); - watch(toRef(props, 'visible'), (visible, ov, onInvalidate) => { + watch(visibleRef, (visible, ov, onInvalidate) => { if (visible) { subscribeLayoutEvent(handleChange); } else { @@ -84,7 +70,7 @@ export const FlexibleOverlay = defineComponent({ }); }); - watch(toRef(props, 'position'), () => { + watch([visibleRef, positionRef], () => { handleChange(); }); @@ -97,8 +83,7 @@ export const FlexibleOverlay = defineComponent({ }, instance); if (origin instanceof Element) { - // Only when the style changing, you can change - // the position. + // Only when the style changing, you can change the position. const observer = new MutationObserver(handleChange); observer.observe(origin, { attributeFilter: ['style'], @@ -118,7 +103,12 @@ export const FlexibleOverlay = defineComponent({ return () => ( -
+
- | Rect; - -type Origin = Element | Rect; - -type HorizontalConnectionPos = 'left' | 'center' | 'right'; -type VerticalConnectionPos = 'top' | 'center' | 'bottom'; - -interface ConnectionPosition { - originX: HorizontalConnectionPos - originY: VerticalConnectionPos - overlayX: HorizontalConnectionPos - overlayY: VerticalConnectionPos -} /** * 获取原点,可能是 Element 或者 Rect @@ -207,6 +139,10 @@ function getOrigin(origin: OriginOrDomRef): Origin { return getElement(origin.value); } + if (isComponent(origin)) { + return getElement(origin); + } + // is point { x: number, y: number, width: number, height: number } return origin; } @@ -243,9 +179,9 @@ function calculatePosition( /** * 返回原点元素的 ClientRect * @param origin - * @returns {ClientRect} + * @returns {DOMRect} */ -function getOriginRect(origin: Origin): ClientRect { +function getOriginRect(origin: Origin): DOMRect { if (origin instanceof Element) { return origin.getBoundingClientRect(); } @@ -261,7 +197,7 @@ function getOriginRect(origin: Origin): ClientRect { right: origin.x + width, height, width, - }; + } as DOMRect; } /** diff --git a/packages/devui-vue/devui/overlay/src/overlay-types.ts b/packages/devui-vue/devui/overlay/src/overlay-types.ts index 766963dc..f90ccfb7 100644 --- a/packages/devui-vue/devui/overlay/src/overlay-types.ts +++ b/packages/devui-vue/devui/overlay/src/overlay-types.ts @@ -1,4 +1,4 @@ -import { ExtractPropTypes, PropType, CSSProperties } from 'vue'; +import { ExtractPropTypes, PropType, StyleValue, ComponentPublicInstance, Ref } from 'vue'; export const overlayProps = { visible: { @@ -16,7 +16,7 @@ export const overlayProps = { default: '' }, backgroundStyle: { - type: [String, Object] as PropType + type: [String, Object] as PropType }, backdropClick: { type: Function, @@ -24,8 +24,84 @@ export const overlayProps = { backdropClose: { type: Boolean, default: true - } + }, + hasBackdrop: { + type: Boolean, + default: true + }, } as const; +export type OverlayProps = ExtractPropTypes; + + +export const fixedOverlayProps = { + ...overlayProps, + overlayStyle: { + type: [String, Object] as PropType, + default: undefined, + }, +}; + +export type FixedOverlayProps = ExtractPropTypes; + +export const flexibleOverlayProps = { + origin: { + type: Object as PropType, + require: true, + }, + position: { + type: Object as PropType, + default: () => ({ + originX: 'left', + originY: 'top', + overlayX: 'left', + overlayY: 'top', + }), + }, + ...overlayProps, +} + + +export interface ClientRect { + bottom: number + readonly height: number + left: number + right: number + top: number + readonly width: number +} + +export interface Point { + x: number + y: number +} + +export interface Rect { + x: number + y: number + width?: number + height?: number +} + +export type Origin = Element | Rect; + +type HorizontalConnectionPos = 'left' | 'center' | 'right'; +type VerticalConnectionPos = 'top' | 'center' | 'bottom'; + +export interface ConnectionPosition { + originX: HorizontalConnectionPos + originY: VerticalConnectionPos + overlayX: HorizontalConnectionPos + overlayY: VerticalConnectionPos +} + +export type OriginOrDomRef = + | Element + | ComponentPublicInstance + | Ref + | Rect + | null; + +export type FlexibleOverlayProps = ExtractPropTypes; + -export type OverlayProps = ExtractPropTypes; \ No newline at end of file diff --git a/packages/devui-vue/devui/overlay/src/overlay.scss b/packages/devui-vue/devui/overlay/src/overlay.scss index 66c0f4a8..d9c754e5 100644 --- a/packages/devui-vue/devui/overlay/src/overlay.scss +++ b/packages/devui-vue/devui/overlay/src/overlay.scss @@ -15,6 +15,10 @@ z-index: 1000; pointer-events: auto; } + + &__disabled { + pointer-events: none; + } } .devui-overlay-fade { diff --git a/packages/devui-vue/devui/overlay/src/utils.ts b/packages/devui-vue/devui/overlay/src/utils.ts index 262253e8..4f115130 100644 --- a/packages/devui-vue/devui/overlay/src/utils.ts +++ b/packages/devui-vue/devui/overlay/src/utils.ts @@ -10,7 +10,11 @@ interface CommonInfo { export function useOverlayLogic(props: OverlayProps): CommonInfo { const backgroundClass = computed(() => { - return ['devui-overlay-background', 'devui-overlay-background__color', props.backgroundClass]; + return [ + 'devui-overlay-background', + props.backgroundClass, + !props.hasBackdrop ? 'devui-overlay-background__disabled' : 'devui-overlay-background__color', + ]; }); const overlayClass = computed(() => { return 'devui-overlay'; diff --git a/packages/devui-vue/devui/shared/util/dom.ts b/packages/devui-vue/devui/shared/util/dom.ts new file mode 100644 index 00000000..b2cb9566 --- /dev/null +++ b/packages/devui-vue/devui/shared/util/dom.ts @@ -0,0 +1,31 @@ +import { ComponentPublicInstance } from "@vue/runtime-core"; + +/** + * + * @param {any} origin + * @returns + */ +export function isComponent(target: any): target is ComponentPublicInstance { + return !!(target?.$el); +} + +/** + * 提取 Vue Intance 中的元素,如果本身就是元素,直接返回。 + * @param {any} element + * @returns {Element | null} + */ +export function getElement( + element: Element | ComponentPublicInstance | null +): Element | null { + if (element instanceof Element) { + return element; + } + if ( + element && + typeof element === 'object' && + element.$el instanceof Element + ) { + return element.$el; + } + return null; +} \ No newline at end of file -- Gitee From 628ea805ef7d56043c843bd68b0578fc63d1b8e0 Mon Sep 17 00:00:00 2001 From: zcating Date: Mon, 1 Nov 2021 22:51:24 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(overlay):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/components/overlay/index.md | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/devui-vue/docs/components/overlay/index.md b/packages/devui-vue/docs/components/overlay/index.md index 2fecc9af..7d604b35 100644 --- a/packages/devui-vue/docs/components/overlay/index.md +++ b/packages/devui-vue/docs/components/overlay/index.md @@ -169,7 +169,7 @@ export default defineComponent({ -### API +### API d-fixed-overlay 参数 | 参数 | 类型 | 默认 | 说明 | @@ -178,8 +178,10 @@ d-fixed-overlay 参数 | onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | | backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | | backgroundClass | `string` | -- | 可选,背景的样式类 | +| backgroundStyle | `StyleValue` | -- | 可选,背景的样式 | | backdropClick | `() => void` | -- | 可选,点击背景触发的事件 | | backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,默认参数是 false | +| hasBackdrop | `boolean` | true | 可选,如果为false,背景元素的 `point-event` 会设为 `none`,且不显示默认背景 | | overlayStyle | `CSSProperties` | -- | 可选,遮罩层的样式 | d-flexible-overlay 参数 @@ -190,7 +192,33 @@ d-flexible-overlay 参数 | onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | | backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | | backgroundClass | `string` | -- | 可选,背景的样式类 | +| backgroundStyle | `StyleValue` | -- | 可选,背景的样式 | | backdropClick | `() => void` | -- | 可选,点击背景触发的事件 | | backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,参数是 false | -| origin | `Element \| ComponentPublicInstance \| { x: number, y: number, width?: number, height?: number }` | false | 必选,你必须指定起点元素才能让遮罩层与该元素连接在一起 | -| position | `{originX: HorizontalPos, originY: VerticalPos, overlayX: HorizontalPos, overlayY: VerticalPos } (type HorizontalPos = 'left' \| 'center' \| 'right') ( type VerticalPos = 'top' \| 'center' \| 'bottom')` | false | 可选,指定遮罩层与原点的连接点 | +| hasBackdrop | `boolean` | true | 可选,如果为false,背景元素的 `point-event` 会设为 `none`,且不显示默认背景 | +| origin | `Element \| ComponentPublicInstance \| Rect` | false | 必选,你必须指定起点元素才能让遮罩层与该元素连接在一起 | +| position | `ConnectionPosition` | false | 可选,指定遮罩层与原点的连接点 | + +Rect 数据结构 +```typescript +interface Rect { + x: number + y: number + width?: number + height?: number +} +``` + +ConnectionPosition 数据结构 +```typescript +type HorizontalConnectionPos = 'left' | 'center' | 'right'; +type VerticalConnectionPos = 'top' | 'center' | 'bottom'; + +export interface ConnectionPosition { + originX: HorizontalConnectionPos + originY: VerticalConnectionPos + overlayX: HorizontalConnectionPos + overlayY: VerticalConnectionPos +} +``` + -- Gitee From 8c5e1c3caafbbe179885393508b715ad3fc3e89f Mon Sep 17 00:00:00 2001 From: zhuchenxi Date: Mon, 1 Nov 2021 18:32:34 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(dropdown):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E4=B8=8B=E6=8B=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui/dropdown/__tests__/dropdown.spec.ts | 8 ++ packages/devui-vue/devui/dropdown/index.ts | 17 +++ .../devui/dropdown/src/dropdown-types.ts | 41 ++++++ .../devui/dropdown/src/dropdown.scss | 27 ++++ .../devui-vue/devui/dropdown/src/dropdown.tsx | 117 ++++++++++++++++++ .../devui-vue/devui/style/core/_dropdown.scss | 7 +- .../devui-vue/devui/style/theme/_shadow.scss | 2 +- .../docs/components/dropdown/index.md | 57 +++++++++ 8 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 packages/devui-vue/devui/dropdown/__tests__/dropdown.spec.ts create mode 100644 packages/devui-vue/devui/dropdown/index.ts create mode 100644 packages/devui-vue/devui/dropdown/src/dropdown-types.ts create mode 100644 packages/devui-vue/devui/dropdown/src/dropdown.scss create mode 100644 packages/devui-vue/devui/dropdown/src/dropdown.tsx create mode 100644 packages/devui-vue/docs/components/dropdown/index.md diff --git a/packages/devui-vue/devui/dropdown/__tests__/dropdown.spec.ts b/packages/devui-vue/devui/dropdown/__tests__/dropdown.spec.ts new file mode 100644 index 00000000..f842c738 --- /dev/null +++ b/packages/devui-vue/devui/dropdown/__tests__/dropdown.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { Dropdown } from '../index'; + +describe('dropdown test', () => { + it('dropdown init render', async () => { + // todo + }) +}) diff --git a/packages/devui-vue/devui/dropdown/index.ts b/packages/devui-vue/devui/dropdown/index.ts new file mode 100644 index 00000000..5e26b71b --- /dev/null +++ b/packages/devui-vue/devui/dropdown/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import Dropdown from './src/dropdown' + +Dropdown.install = function (app: App): void { + app.component(Dropdown.name, Dropdown) +} + +export { Dropdown } + +export default { + title: 'Dropdown 下拉菜单', + category: '导航', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Dropdown as any) + } +} diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts new file mode 100644 index 00000000..0c416cf6 --- /dev/null +++ b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts @@ -0,0 +1,41 @@ +import type { PropType, ExtractPropTypes, ComponentPublicInstance } from 'vue' + +export const dropdownProps = { + + origin: { + type: Object as PropType + }, + + isOpen: { + type: Boolean, + default: false + }, + + disabled: { + type: Boolean, + default: false + }, + + trigger: { + type: String as PropType<'click' | 'hover' | 'manually'>, + default: 'click' + }, + + closeScope: { + type: String as PropType<'all' | 'blank' | 'none'>, + default: 'all' + }, + + closeOnMouseLeaveMenu: { + type: Boolean, + default: true + }, + + showAnimation: { + type: Boolean, + default: true + } + +} as const + +export type DropdownProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.scss b/packages/devui-vue/devui/dropdown/src/dropdown.scss new file mode 100644 index 00000000..164fd17c --- /dev/null +++ b/packages/devui-vue/devui/dropdown/src/dropdown.scss @@ -0,0 +1,27 @@ +@import '@devui/styles-var/devui-var'; + +.devui-dropdown-menu { + box-shadow: $devui-shadow-length-connected-overlay; +} + +.devui-dropdown span { + &.icon-chevron-down, + &.icon-select-arrow { + display: inline-block; + vertical-align: text-top; + } +} + +.devui-dropdown-animation span { + &.icon-chevron-down, + &.icon-select-arrow { + transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out-smooth; + } +} + +.devui-dropdown.open span { + &.icon-chevron-down, + &.icon-select-arrow { + transform: rotate(180deg); + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.tsx b/packages/devui-vue/devui/dropdown/src/dropdown.tsx new file mode 100644 index 00000000..7550ceb6 --- /dev/null +++ b/packages/devui-vue/devui/dropdown/src/dropdown.tsx @@ -0,0 +1,117 @@ +import { defineComponent, watch, ref, cloneVNode } from 'vue' +import { dropdownProps, DropdownProps } from './dropdown-types' +import { FlexibleOverlay } from '../../overlay'; +import { getElement } from '../../shared/util/dom'; + +import './dropdown.scss' + +function subscribeEvent(dom: Element | Document, type: string, callback: (event: E) => void) { + console.log(dom); + dom?.addEventListener(type, callback as any); + return () => { + dom?.removeEventListener(type, callback as any); + } +} + + +export default defineComponent({ + name: 'DDropdown', + props: dropdownProps, + emits: [], + setup(props: DropdownProps, ctx) { + const visible = ref(); + watch(() => props.isOpen, (value) => { + visible.value = value; + }, { immediate: true }); + const show = () => { + visible.value = true; + } + const close = () => { + visible.value = false; + } + + const position = { + originX: 'left', + originY: 'bottom', + overlayX: 'left', + overlayY: 'top' + } as const; + + const dropdownElRef = ref(); + watch([() => props.origin, () => props.trigger, dropdownElRef], ([origin, trigger, dropdownEl], ov, onInvalidate) => { + const originEl = getElement(origin); + if (!originEl || !dropdownElRef) { + return; + } + if (trigger === 'click') { + const subs = [ + subscribeEvent(originEl, 'click', show), + subscribeEvent(document, 'click', (e) => { + if (!visible.value) { + return; + } + const target = e.target as HTMLElement; + const isContain = originEl.contains(target) || dropdownEl.contains(target); + if (!isContain) { + close(); + } + }), + ]; + onInvalidate(() => { + subs.forEach(v => v()); + }); + } else if (trigger === 'hover') { + let overlayEnter = false; + let originEnter = false; + const handleLeave = (elementType: 'origin' | 'dropdown') => { + setTimeout(() => { + if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) { + return; + } + close(); + }, 50); + }; + const subs = [ + subscribeEvent(originEl, 'mouseenter', () => { + originEnter = true; + show(); + }), + subscribeEvent(originEl, 'mouseleave', () => { + originEnter = false; + // 判断鼠标是否已经进入 overlay + handleLeave('origin'); + }), + subscribeEvent(dropdownEl, 'mouseenter', () => { + overlayEnter = true; + show(); + }), + subscribeEvent(dropdownEl, 'mouseleave', () => { + overlayEnter = false; + // 判断鼠标是否已经进入 origin + handleLeave('dropdown'); + }), + ]; + onInvalidate(() => subs.forEach(v => v())); + } + }); + + return () => { + let vnodes = ctx.slots.default?.() ?? []; + return ( + <> + {vnodes} + +
+
+
+ + ) + }; + } +}) diff --git a/packages/devui-vue/devui/style/core/_dropdown.scss b/packages/devui-vue/devui/style/core/_dropdown.scss index 085045d0..f6e6defa 100755 --- a/packages/devui-vue/devui/style/core/_dropdown.scss +++ b/packages/devui-vue/devui/style/core/_dropdown.scss @@ -46,7 +46,6 @@ top: calc(100% - 1px); left: 0; z-index: 1000; - display: none; min-width: calc(min(100%, 102px)); margin: 4px 0; padding-bottom: 5px; @@ -67,9 +66,9 @@ margin-bottom: -1px; } - .open > & { - display: block; - } + // .open > & { + // display: block; + // } > li { position: relative; diff --git a/packages/devui-vue/devui/style/theme/_shadow.scss b/packages/devui-vue/devui/style/theme/_shadow.scss index a86b13c3..313b2e33 100644 --- a/packages/devui-vue/devui/style/theme/_shadow.scss +++ b/packages/devui-vue/devui/style/theme/_shadow.scss @@ -4,7 +4,7 @@ $devui-shadow-length-base: var(--devui-shadow-length-base, 0 1px 4px 0); //直 $devui-shadow-length-slide-left: var(--devui-shadow-length-slide-left, -2px 0 8px 0); //向左滑动时出现在右侧边缘的阴影 (dataTable固定右侧列向左滑动) $devui-shadow-length-slide-right: var(--devui-shadow-length-slide-right, 2px 0 8px 0); //向右滑动时出现在左侧边缘的阴影 (dataTable固定左侧列向右滑动) -$devui-shadow-length-connected-overlay : var(--devui-shadow-connected-overlay, 0 2px 8px 0); //有连接点的弹出(覆盖)层 (DatePicker Cascader Select TagsInput Pagination的下拉菜单等) +$devui-shadow-length-connected-overlay: var(--devui-shadow-connected-overlay, 0 2px 8px 0); //有连接点的弹出(覆盖)层 (DatePicker Cascader Select TagsInput Pagination的下拉菜单等) $devui-shadow-length-hover : var(--devui-shadow-length-hover, 0 4px 16px 0); //涉及到hover的地方 $devui-shadow-length-feedback-overlay : var(--devui-shadow-length-feedback-overlay, 0 4px 16px 0); //信息提示反馈类 (PopOver Tooltip Toast StepsGuide等) diff --git a/packages/devui-vue/docs/components/dropdown/index.md b/packages/devui-vue/docs/components/dropdown/index.md new file mode 100644 index 00000000..c5fe0dd8 --- /dev/null +++ b/packages/devui-vue/docs/components/dropdown/index.md @@ -0,0 +1,57 @@ +# Dropdown 下拉菜单 + +// todo 组件描述 + +### 何时使用 + +// todo 使用时机描述 + + +### 基本用法 +// todo 用法描述 +:::demo // todo 展开代码的内部描述 + +```vue + + + + + +``` + +::: + +### d-dropdown + +d-dropdown 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| ---- | ---- | ---- | ---- | --------- | ---------- | +| | | | | | | +| | | | | | | +| | | | | | | + +d-dropdown 事件 + +| 事件 | 类型 | 说明 | 跳转 Demo | +| ---- | ---- | ---- | --------- | +| | | | | +| | | | | +| | | | | + -- Gitee From 04f2b9262ee0bf25c8e0a866b32b53ba090fd836 Mon Sep 17 00:00:00 2001 From: zhuchenxi Date: Tue, 2 Nov 2021 18:23:34 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20closeOnMouseLeaveMenu?= =?UTF-8?q?=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui/dropdown/src/dropdown.scss | 4 - .../devui-vue/devui/dropdown/src/dropdown.tsx | 114 +++++++++--------- .../docs/components/dropdown/index.md | 78 +++++++++++- 3 files changed, 134 insertions(+), 62 deletions(-) diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.scss b/packages/devui-vue/devui/dropdown/src/dropdown.scss index 164fd17c..227310a1 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown.scss +++ b/packages/devui-vue/devui/dropdown/src/dropdown.scss @@ -1,9 +1,5 @@ @import '@devui/styles-var/devui-var'; -.devui-dropdown-menu { - box-shadow: $devui-shadow-length-connected-overlay; -} - .devui-dropdown span { &.icon-chevron-down, &.icon-select-arrow { diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.tsx b/packages/devui-vue/devui/dropdown/src/dropdown.tsx index 7550ceb6..62053d5d 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown.tsx +++ b/packages/devui-vue/devui/dropdown/src/dropdown.tsx @@ -6,7 +6,6 @@ import { getElement } from '../../shared/util/dom'; import './dropdown.scss' function subscribeEvent(dom: Element | Document, type: string, callback: (event: E) => void) { - console.log(dom); dom?.addEventListener(type, callback as any); return () => { dom?.removeEventListener(type, callback as any); @@ -38,76 +37,79 @@ export default defineComponent({ } as const; const dropdownElRef = ref(); - watch([() => props.origin, () => props.trigger, dropdownElRef], ([origin, trigger, dropdownEl], ov, onInvalidate) => { - const originEl = getElement(origin); - if (!originEl || !dropdownElRef) { - return; - } - if (trigger === 'click') { - const subs = [ - subscribeEvent(originEl, 'click', show), - subscribeEvent(document, 'click', (e) => { - if (!visible.value) { - return; - } - const target = e.target as HTMLElement; - const isContain = originEl.contains(target) || dropdownEl.contains(target); - if (!isContain) { - close(); - } - }), - ]; - onInvalidate(() => { - subs.forEach(v => v()); - }); - } else if (trigger === 'hover') { - let overlayEnter = false; - let originEnter = false; - const handleLeave = (elementType: 'origin' | 'dropdown') => { - setTimeout(() => { + watch( + [() => props.origin, () => props.trigger, dropdownElRef], + ([origin, trigger, dropdownEl], ov, onInvalidate) => { + const originEl = getElement(origin); + if (!originEl || !dropdownElRef) { + return; + } + if (trigger === 'click') { + const subs = [ + 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) { + close(); + } + }), + ]; + onInvalidate(() => { + subs.forEach(v => v()); + }); + } else if (trigger === 'hover') { + let overlayEnter = false; + let originEnter = false; + const handleLeave = async (elementType: 'origin' | 'dropdown') => { + await new Promise((resolve) => setTimeout(resolve, 50)); if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) { return; } close(); - }, 50); - }; - const subs = [ - subscribeEvent(originEl, 'mouseenter', () => { - originEnter = true; - show(); - }), - subscribeEvent(originEl, 'mouseleave', () => { - originEnter = false; - // 判断鼠标是否已经进入 overlay - handleLeave('origin'); - }), - subscribeEvent(dropdownEl, 'mouseenter', () => { - overlayEnter = true; - show(); - }), - subscribeEvent(dropdownEl, 'mouseleave', () => { - overlayEnter = false; - // 判断鼠标是否已经进入 origin - handleLeave('dropdown'); - }), - ]; - onInvalidate(() => subs.forEach(v => v())); + }; + const subs = [ + subscribeEvent(originEl, 'mouseenter', () => { + originEnter = true; + show(); + }), + subscribeEvent(originEl, 'mouseleave', () => { + originEnter = false; + // 判断鼠标是否已经进入 overlay + if (!props.closeOnMouseLeaveMenu) { + handleLeave('origin'); + } + }), + subscribeEvent(dropdownEl, 'mouseenter', () => { + overlayEnter = true; + show(); + }), + subscribeEvent(dropdownEl, 'mouseleave', () => { + overlayEnter = false; + // 判断鼠标是否已经进入 origin + handleLeave('dropdown'); + }), + ]; + onInvalidate(() => subs.forEach(v => v())); + } } - }); + ); return () => { - let vnodes = ctx.slots.default?.() ?? []; + // let vnodes = ctx.slots.default?.() ?? []; return ( <> - {vnodes} -
+
+ {ctx.slots.default?.()}
diff --git a/packages/devui-vue/docs/components/dropdown/index.md b/packages/devui-vue/docs/components/dropdown/index.md index c5fe0dd8..3588eb66 100644 --- a/packages/devui-vue/docs/components/dropdown/index.md +++ b/packages/devui-vue/docs/components/dropdown/index.md @@ -14,16 +14,80 @@ ```vue + + + +``` + +::: + +hover 触发 +:::demo // todo 展开代码的内部描述 +```vue + ``` -- Gitee From 9add5d907ca090bd731e4d3eb8277cb96ef8d54f Mon Sep 17 00:00:00 2001 From: zcating Date: Wed, 3 Nov 2021 18:41:36 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat(dropdown):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/devui-vue/devui/dropdown/index.ts | 1 + .../devui/dropdown/src/dropdown-types.ts | 9 +- .../devui-vue/devui/dropdown/src/dropdown.tsx | 96 ++++--------------- .../devui/dropdown/src/use-dropdown.ts | 92 ++++++++++++++++++ .../docs/components/dropdown/index.md | 52 ++++++---- 5 files changed, 148 insertions(+), 102 deletions(-) create mode 100644 packages/devui-vue/devui/dropdown/src/use-dropdown.ts diff --git a/packages/devui-vue/devui/dropdown/index.ts b/packages/devui-vue/devui/dropdown/index.ts index 5e26b71b..d3bf24e7 100644 --- a/packages/devui-vue/devui/dropdown/index.ts +++ b/packages/devui-vue/devui/dropdown/index.ts @@ -1,4 +1,5 @@ import type { App } from 'vue' + import Dropdown from './src/dropdown' Dropdown.install = function (app: App): void { diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts index 0c416cf6..7b1adeb3 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts +++ b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts @@ -1,7 +1,10 @@ import type { PropType, ExtractPropTypes, ComponentPublicInstance } from 'vue' -export const dropdownProps = { +export type TriggerType = 'click' | 'hover' | 'manually'; +export type CloseScopeArea = 'all' | 'blank' | 'none'; + +export const dropdownProps = { origin: { type: Object as PropType }, @@ -17,12 +20,12 @@ export const dropdownProps = { }, trigger: { - type: String as PropType<'click' | 'hover' | 'manually'>, + type: String as PropType, default: 'click' }, closeScope: { - type: String as PropType<'all' | 'blank' | 'none'>, + type: String as PropType, default: 'all' }, diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.tsx b/packages/devui-vue/devui/dropdown/src/dropdown.tsx index 62053d5d..6e8256d0 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown.tsx +++ b/packages/devui-vue/devui/dropdown/src/dropdown.tsx @@ -1,16 +1,10 @@ -import { defineComponent, watch, ref, cloneVNode } from 'vue' +import { defineComponent, watch, ref, cloneVNode, toRefs } from 'vue' import { dropdownProps, DropdownProps } from './dropdown-types' import { FlexibleOverlay } from '../../overlay'; import { getElement } from '../../shared/util/dom'; import './dropdown.scss' - -function subscribeEvent(dom: Element | Document, type: string, callback: (event: E) => void) { - dom?.addEventListener(type, callback as any); - return () => { - dom?.removeEventListener(type, callback as any); - } -} +import { useDropdown } from './use-dropdown'; export default defineComponent({ @@ -18,16 +12,17 @@ export default defineComponent({ props: dropdownProps, emits: [], setup(props: DropdownProps, ctx) { - const visible = ref(); - watch(() => props.isOpen, (value) => { + const { + isOpen, + origin, + trigger, + closeOnMouseLeaveMenu, + } = toRefs(props); + + const visible = ref(false); + watch(isOpen, (value) => { visible.value = value; }, { immediate: true }); - const show = () => { - visible.value = true; - } - const close = () => { - visible.value = false; - } const position = { originX: 'left', @@ -36,67 +31,12 @@ export default defineComponent({ overlayY: 'top' } as const; - const dropdownElRef = ref(); - watch( - [() => props.origin, () => props.trigger, dropdownElRef], - ([origin, trigger, dropdownEl], ov, onInvalidate) => { - const originEl = getElement(origin); - if (!originEl || !dropdownElRef) { - return; - } - if (trigger === 'click') { - const subs = [ - 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) { - close(); - } - }), - ]; - onInvalidate(() => { - subs.forEach(v => v()); - }); - } else if (trigger === 'hover') { - let overlayEnter = false; - let originEnter = false; - const handleLeave = async (elementType: 'origin' | 'dropdown') => { - await new Promise((resolve) => setTimeout(resolve, 50)); - if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) { - return; - } - close(); - }; - const subs = [ - subscribeEvent(originEl, 'mouseenter', () => { - originEnter = true; - show(); - }), - subscribeEvent(originEl, 'mouseleave', () => { - originEnter = false; - // 判断鼠标是否已经进入 overlay - if (!props.closeOnMouseLeaveMenu) { - handleLeave('origin'); - } - }), - subscribeEvent(dropdownEl, 'mouseenter', () => { - overlayEnter = true; - show(); - }), - subscribeEvent(dropdownEl, 'mouseleave', () => { - overlayEnter = false; - // 判断鼠标是否已经进入 origin - handleLeave('dropdown'); - }), - ]; - onInvalidate(() => subs.forEach(v => v())); - } - } - ); + const { dropdownEl } = useDropdown({ + visible, + origin, + trigger, + closeOnMouseLeaveMenu, + }); return () => { // let vnodes = ctx.slots.default?.() ?? []; @@ -108,7 +48,7 @@ export default defineComponent({ position={position} hasBackdrop={false} > -
+
{ctx.slots.default?.()}
diff --git a/packages/devui-vue/devui/dropdown/src/use-dropdown.ts b/packages/devui-vue/devui/dropdown/src/use-dropdown.ts new file mode 100644 index 00000000..54537884 --- /dev/null +++ b/packages/devui-vue/devui/dropdown/src/use-dropdown.ts @@ -0,0 +1,92 @@ +import { Ref, ref, watch } from 'vue'; +import { getElement } from '../../shared/util/dom'; +import { TriggerType } 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); + } +} + +interface UseDropdownProps { + visible: Ref + trigger: Ref + origin: Ref + closeOnMouseLeaveMenu: Ref +} + +interface UseDropdownResult { + dropdownEl: Ref +} + +export const useDropdown = ({ + visible, + trigger, + origin, + closeOnMouseLeaveMenu +}: UseDropdownProps): UseDropdownResult => { + const dropdownElRef = ref(); + + watch( + [trigger, origin, dropdownElRef], + ([trigger, origin, dropdownEl], ov, onInvalidate) => { + const originEl = getElement(origin); + if (!originEl || !dropdownEl) { + return; + } + if (trigger === 'click') { + const subs = [ + 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; + } + visible.value = false; + }), + ]; + onInvalidate(() => subs.forEach(v => v())); + } else if (trigger === 'hover') { + let overlayEnter = false; + let originEnter = false; + const handleLeave = async (elementType: 'origin' | 'dropdown') => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) { + return; + } + visible.value = false; + }; + const subs = [ + 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(() => subs.forEach(v => v())); + } + } + ); + + return { dropdownEl: dropdownElRef }; +} diff --git a/packages/devui-vue/docs/components/dropdown/index.md b/packages/devui-vue/docs/components/dropdown/index.md index 3588eb66..4c9a13bb 100644 --- a/packages/devui-vue/docs/components/dropdown/index.md +++ b/packages/devui-vue/docs/components/dropdown/index.md @@ -13,7 +13,7 @@ ```vue