(component): [dropdown]添加 visible,clickToClose,blueToClose 属性; hover 延时

This commit is contained in:
sight 2022-06-24 00:51:41 +08:00
parent 45b23a5898
commit e3c2b471b3
3 changed files with 125 additions and 38 deletions

View File

@ -6,14 +6,16 @@ export default {
<script setup lang="ts"> <script setup lang="ts">
import "./index.less"; import "./index.less";
import type { CSSProperties } from "vue";
import { import {
CSSProperties, computed,
nextTick, nextTick,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
provide, provide,
ref, ref,
shallowRef, shallowRef,
watch,
} from "vue"; } from "vue";
import { import {
onClickOutside, onClickOutside,
@ -21,22 +23,28 @@ import {
useThrottleFn, useThrottleFn,
useWindowSize, useWindowSize,
} from "@vueuse/core"; } from "@vueuse/core";
import { DropdownTrigger, dropdownPlacement } from "./interface"; import type { DropdownPlacement } from "./interface";
export type DropdownTrigger = "click" | "hover" | "focus" | "contextMenu";
export interface LayDropdownProps { export interface LayDropdownProps {
trigger?: DropdownTrigger; visible?: boolean;
placement?: dropdownPlacement; trigger?: DropdownTrigger | DropdownTrigger[];
placement?: DropdownPlacement;
disabled?: boolean; disabled?: boolean;
autoFitPosition?: boolean; autoFitPosition?: boolean;
autoFitWidth?: boolean; autoFitWidth?: boolean;
autoFitMinWidth?: boolean; autoFitMinWidth?: boolean;
updateAtScroll?: boolean; updateAtScroll?: boolean;
autoFixPosition?: boolean; autoFixPosition?: boolean;
clickToClose?: boolean;
blurToClose?: boolean;
clickOutsideToClose?: boolean; clickOutsideToClose?: boolean;
contentOffset?: number; contentOffset?: number;
} }
const props = withDefaults(defineProps<LayDropdownProps>(), { const props = withDefaults(defineProps<LayDropdownProps>(), {
visible: false,
trigger: "click", trigger: "click",
disabled: false, disabled: false,
placement: "bottom-left", placement: "bottom-left",
@ -45,6 +53,8 @@ const props = withDefaults(defineProps<LayDropdownProps>(), {
autoFitWidth: false, autoFitWidth: false,
updateAtScroll: false, updateAtScroll: false,
autoFixPosition: true, autoFixPosition: true,
clickToClose: true,
blurToClose: true,
clickOutsideToClose: true, clickOutsideToClose: true,
contentOffset: 2, contentOffset: 2,
}); });
@ -55,23 +65,30 @@ const contentStyle = ref<CSSProperties>({});
const { width: windowWidth, height: windowHeight } = useWindowSize(); const { width: windowWidth, height: windowHeight } = useWindowSize();
const openState = ref(false); const openState = ref(false);
const triggerMethods = computed(() =>
([] as Array<DropdownTrigger>).concat(props.trigger)
);
const emit = defineEmits(["open", "hide"]); const emit = defineEmits(["open", "hide"]);
onClickOutside(dropdownRef, () => { let delayTimer = 0;
if (props.clickOutsideToClose) {
changeVisible(false);
}
});
const open = (): void => { const cleanDelayTimer = () => {
if (delayTimer) {
window.clearTimeout(delayTimer);
delayTimer = 0;
}
};
const open = (delay?: number): void => {
if (props.disabled == false) { if (props.disabled == false) {
changeVisible(true); changeVisible(true, delay);
emit("open"); emit("open");
} }
}; };
const hide = (): void => { const hide = (delay?: number): void => {
changeVisible(false); changeVisible(false, delay);
emit("hide"); emit("hide");
}; };
@ -84,14 +101,25 @@ const toggle = (): void => {
} }
}; };
const changeVisible = (visible: boolean) => { const changeVisible = (visible: boolean, delay?: number) => {
if (visible === openState.value) { if (visible === openState.value && delayTimer === 0) {
return; return;
} }
openState.value = visible; const update = () => {
nextTick(() => { openState.value = visible;
updateContentStyle(); nextTick(() => {
}); updateContentStyle();
});
}
if(delay){
cleanDelayTimer();
if (visible !== openState.value) {
delayTimer = window.setTimeout(update, delay);
}
}else{
update();
}
}; };
const updateContentStyle = () => { const updateContentStyle = () => {
@ -115,7 +143,7 @@ const updateContentStyle = () => {
}; };
const getContentStyle = ( const getContentStyle = (
placement: dropdownPlacement, placement: DropdownPlacement,
triggerRect: DOMRect, triggerRect: DOMRect,
contentRect: DOMRect, contentRect: DOMRect,
{ {
@ -151,7 +179,7 @@ const getContentStyle = (
const getFitPlacement = ( const getFitPlacement = (
top: number, top: number,
left: number, left: number,
placement: dropdownPlacement, placement: DropdownPlacement,
triggerRect: DOMRect, triggerRect: DOMRect,
contentRect: DOMRect contentRect: DOMRect
) => { ) => {
@ -202,7 +230,7 @@ const getFitPlacement = (
}; };
const getContentOffset = ( const getContentOffset = (
placement: dropdownPlacement, placement: DropdownPlacement,
triggerRect: DOMRect, triggerRect: DOMRect,
contentRect: DOMRect contentRect: DOMRect
) => { ) => {
@ -268,7 +296,47 @@ const handleScroll = useThrottleFn(() => {
if (openState.value) { if (openState.value) {
updateContentStyle(); updateContentStyle();
} }
}, 300); }, 250);
const handleClick = () => {
if (props.disabled || (openState.value && !props.clickToClose)) {
return;
}
if (triggerMethods.value.includes("click") || triggerMethods.value.includes("contextMenu")){
toggle()
}
}
const handleMouseEnter = () => {
if (props.disabled || !triggerMethods.value.includes('hover')) {
return;
}
open(250);
}
const handleMouseLeave = () => {
if (props.disabled || !triggerMethods.value.includes('hover')) {
return;
}
hide(150);
}
const handleFocusin = () => {
if (props.disabled || !triggerMethods.value.includes('focus')) {
return;
}
open()
}
const handleFocusout = () => {
if (props.disabled || !triggerMethods.value.includes('focus')) {
return;
}
if (!props.blurToClose) {
return;
}
hide();
}
const { stop: removeContentResizeObserver } = useResizeObserver( const { stop: removeContentResizeObserver } = useResizeObserver(
contentRef, contentRef,
@ -288,6 +356,12 @@ const { stop: removeTriggerResizeObserver } = useResizeObserver(
} }
); );
onClickOutside(dropdownRef, () => {
if (props.clickOutsideToClose) {
hide();
}
});
let scrollElements: HTMLElement[] | undefined; let scrollElements: HTMLElement[] | undefined;
onMounted(() => { onMounted(() => {
@ -306,11 +380,19 @@ onBeforeUnmount(() => {
} }
scrollElements = undefined; scrollElements = undefined;
} }
removeContentResizeObserver(); removeContentResizeObserver();
removeTriggerResizeObserver(); removeTriggerResizeObserver();
}), })
provide("openState", openState);
watch(
() => props.visible,
(newVal, oldVal) => {
openState.value = newVal;
},
{ immediate: true }
);
provide("openState", openState);
defineExpose({ open, hide, toggle }); defineExpose({ open, hide, toggle });
</script> </script>
@ -319,13 +401,15 @@ defineExpose({ open, hide, toggle });
<div <div
ref="dropdownRef" ref="dropdownRef"
class="layui-dropdown" class="layui-dropdown"
@mouseenter="trigger == 'hover' && open()" @mouseenter="handleMouseEnter()"
@mouseleave="trigger == 'hover' && hide()" @mouseleave="handleMouseLeave()"
@focusin="handleFocusin()"
@focusout="handleFocusout()"
:class="{ 'layui-dropdown-up': openState }" :class="{ 'layui-dropdown-up': openState }"
> >
<div <div
@click="trigger == 'click' && toggle()" @click="handleClick()"
@contextmenu.prevent="trigger == 'contextMenu' && toggle()" @contextmenu.prevent="handleClick()"
> >
<slot></slot> <slot></slot>
</div> </div>

View File

@ -1,5 +1,5 @@
export type DropdownTrigger = "click" | "hover" | "contextMenu"; export type DropdownTrigger = "click" | "hover" | "focus" | "contextMenu";
export type dropdownPlacement = export type DropdownPlacement =
| "top" | "top"
| "top-left" | "top-left"
| "top-right" | "top-right"

View File

@ -293,15 +293,18 @@ export default {
| 属性 | 描述 | 可选值 | | 属性 | 描述 | 可选值 |
| ------- | -------- | --------------- | | ------- | -------- | --------------- |
| trigger | 触发方式 | `click` `hover` `contextMenu` | | visible | 下拉面板是否可见 |`true` `false`|
| trigger | 触发方式,类型 `string` 或 trigger 数组 | `click` `hover` `focus` `contextMenu` |
| disabled | 是否禁用触发 | `true` `false` | | disabled | 是否禁用触发 | `true` `false` |
| placement | 下拉面板位置 |`top` `top-left` `top-right` `bottom` `bottom-left` `bottom-right`| | placement | 下拉面板位置 |`top` `top-left` `top-right` `bottom` `bottom-left` `bottom-right`|
| autoFitPosition| 是否自动调整下拉面板位置,默认 `true` | `true` `false` | | autoFitPosition| 是否自动调整下拉面板位置,默认 `true` |`true` `false` |
| autoFitWidth | 是否将下拉面板宽度设置为触发器宽度, 默认 `false` | `true` `false` | | autoFitWidth | 是否将下拉面板宽度设置为触发器宽度, 默认 `false` |`true` `false` |
| autoFitMinWidth | 是否将下拉面板最小宽度设置为触发器宽度, 默认 `true` | `true` `false` | | autoFitMinWidth | 是否将下拉面板最小宽度设置为触发器宽度, 默认 `true` |`true` `false` |
| updateAtScroll | 是否在容器滚动时更新下拉面板的位置,默认 `false` | `true` `false` | | updateAtScroll | 是否在容器滚动时更新下拉面板的位置,默认 `false` | `true` `false` |
| autoFixPosition | 是否在触发器或下拉面板尺寸变化时更新下拉面板位置,面板尺寸变化参见级联选择器,默认 `true` | `true` `false` | | autoFixPosition | 是否在触发器或下拉面板尺寸变化时更新下拉面板位置,面板尺寸变化参见级联选择器,默认 `true` |`true` `false` |
| clickOutsideToClose| 是否点击外部关闭下拉面板,默认 `true`| `true` `false`| | clickToClose | 是否在点击触发器时关闭面板 |`true` `false`|
| blurToClose | 是否在触发器失去焦点时关闭面板 |`true` `false`|
| clickOutsideToClose| 是否点击外部关闭下拉面板,默认 `true`|`true` `false`|
| contentOffset | 下拉面板距离触发器的偏移距离,默认 2| -| | contentOffset | 下拉面板距离触发器的偏移距离,默认 2| -|