(component): [dropdown]下拉面板位置自适应

This commit is contained in:
sight 2022-06-22 13:08:48 +08:00
parent aed6091707
commit 753f0f545b
6 changed files with 247 additions and 27 deletions

View File

@ -42,6 +42,6 @@
background-color: var(--global-checked-color); background-color: var(--global-checked-color);
color: white; color: white;
} }
.layui-cascader.layui-dropdown-up > dl { // .layui-cascader.layui-dropdown-up > dl {
min-width: unset; // min-width: unset;
} // }

View File

@ -1,5 +1,5 @@
<template> <template>
<lay-dropdown class="layui-cascader" ref="dropdownRef"> <lay-dropdown class="layui-cascader" ref="dropdownRef" :autoFitMinWidth="false">
<lay-input <lay-input
v-model="displayValue" v-model="displayValue"
readonly readonly

View File

@ -6,9 +6,7 @@
.layui-dropdown dl { .layui-dropdown dl {
display: none; display: none;
position: absolute; position: absolute;
margin-top: 2px;
z-index: 899; z-index: 899;
min-width: 100%;
background-color: #fff; background-color: #fff;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;

View File

@ -6,37 +6,50 @@ export default {
<script setup lang="ts"> <script setup lang="ts">
import "./index.less"; import "./index.less";
import { provide, ref } from "vue"; import { CSSProperties, nextTick, provide, ref, shallowRef, watch, watchEffect } from "vue";
import { onClickOutside } from "@vueuse/core"; import { onClickOutside, useWindowSize } from "@vueuse/core";
import { DropdownTrigger } from "./interface"; import { DropdownTrigger, dropdownPlacement } from "./interface";
export interface LayDropdownProps { export interface LayDropdownProps {
trigger?: DropdownTrigger; trigger?: DropdownTrigger;
placement?: dropdownPlacement;
disabled?: boolean; disabled?: boolean;
autoFitPlacement?: boolean;
autoFitWidth?: boolean;
autoFitMinWidth?: boolean;
} }
const props = withDefaults(defineProps<LayDropdownProps>(), { const props = withDefaults(defineProps<LayDropdownProps>(), {
trigger: "click", trigger: "click",
disabled: false, disabled: false,
placement: 'bottom-left',
autoFitPlacement: true,
autoFitMinWidth: true,
autoFitWidth: false,
}); });
const dropdownRef = shallowRef<null | HTMLElement>();
const contentRef = shallowRef<null | HTMLElement>();
const contentStyle = ref<CSSProperties>({});
const { width: windowWidth, height: windowHeight } = useWindowSize();
const openState = ref(false); const openState = ref(false);
const dropdownRef = ref<null | HTMLElement>(); const contentSpace = 2;
const emit = defineEmits(["open", "hide"]); const emit = defineEmits(["open", "hide"]);
onClickOutside(dropdownRef, () => { onClickOutside(dropdownRef, () => {
openState.value = false; changeVisible(false)
}); });
const open = (): void => { const open = (): void => {
if (props.disabled == false) { if (props.disabled == false) {
openState.value = true; changeVisible(true);
emit("open"); emit("open");
} }
}; };
const hide = (): void => { const hide = (): void => {
openState.value = false; changeVisible(false);
emit("hide"); emit("hide");
}; };
@ -49,26 +62,172 @@ const toggle = (): void => {
} }
}; };
const changeVisible = (visible: boolean) => {
if (visible === openState.value) {
return;
}
openState.value = visible;
nextTick(() => {
updateContentStyle();
});
}
const updateContentStyle = () => {
if (!dropdownRef.value || !contentRef.value){
return
}
const triggerRect = dropdownRef.value.getBoundingClientRect()
const contentRect = contentRef.value.getBoundingClientRect()
const { style } = getContentStyle(
props.placement,
triggerRect,
contentRect,
{autoFitPlacement: props.autoFitPlacement}
);
if (props.autoFitMinWidth) {
style.minWidth = `${triggerRect.width}px`;
}
if (props.autoFitWidth) {
style.width = `${triggerRect.width}px`;
}
contentStyle.value = style
}
const getContentStyle = (
placement: dropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect,
{
autoFitPlacement = false,
customStyle = {}
}: {
autoFitPlacement?: boolean;
customStyle?: CSSProperties;
} = {}
) => {
let { top, left } = getContentOffset(placement, triggerRect, contentRect)
if(autoFitPlacement){
const { top: fitTop, left: fitLeft } = getFitPlacement(top, left, placement, triggerRect, contentRect)
top = fitTop;
left = fitLeft;
}
const style = {
top: `${top}px`,
left: `${left}px`,
...customStyle,
}
return {
style
}
}
const getFitPlacement = (
top: number,
left: number,
placement: dropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect
) => {
//
if (triggerRect.bottom + contentRect.height > windowHeight.value) {
top = -contentRect.height - contentSpace
}
//
if (triggerRect.top - contentRect.height < 0) {
top = triggerRect.height + contentSpace
}
if(["bottom-right", "top-right"].includes(placement) ){
//
const contentRectLeft = triggerRect.left - (contentRect.width - triggerRect.width)
if (contentRectLeft < 0) {
left = left + (0 - contentRectLeft)
}
}
if(["bottom-left", "top-left"].includes(placement)){
//
const contentRectRight = triggerRect.right + (contentRect.width - triggerRect.width)
if (contentRectRight > windowWidth.value) {
left = left - (contentRectRight - windowWidth.value)
}
}
if(["bottom", "top"].includes(placement)){
const contentRectLeft = triggerRect.left - (contentRect.width - triggerRect.width) / 2
const contentRectRight = triggerRect.right + (contentRect.width - triggerRect.width) / 2
//
if (contentRectLeft < 0) {
left = left + (0 - contentRectLeft)
}
//
if (contentRectRight > windowWidth.value) {
left = left - (contentRectRight - windowWidth.value)
}
}
return {
top,
left
}
}
const getContentOffset = (
placement: dropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect
) => {
switch (placement) {
case "top":
return {
top: -contentRect.height - contentSpace,
left: -(contentRect.width - triggerRect.width) / 2
}
case "top-left":
return {
top: -contentRect.height - contentSpace,
left: 0
}
case "top-right":
return {
top: -contentRect.height - contentSpace,
left: -(contentRect.width - triggerRect.width)
}
case "bottom":
return {
top: triggerRect.height + contentSpace,
left: -(contentRect.width - triggerRect.width) / 2
}
case "bottom-left":
return {
top: triggerRect.height + contentSpace,
left: 0
}
case "bottom-right":
return {
top: triggerRect.height + contentSpace,
left: -(contentRect.width - triggerRect.width)
}
default:
return {
left: 0,
top: 0,
};
}
}
provide("openState", openState); provide("openState", openState);
defineExpose({ open, hide, toggle }); defineExpose({ open, hide, toggle });
</script> </script>
<template> <template>
<div <div ref="dropdownRef" class="layui-dropdown" @mouseenter="trigger == 'hover' && open()"
ref="dropdownRef" @mouseleave="trigger == 'hover' && hide()" :class="{ 'layui-dropdown-up': openState }">
class="layui-dropdown" <div @click="trigger == 'click' && toggle()" @contextmenu.prevent="trigger == 'contextMenu' && toggle()">
@mouseenter="trigger == 'hover' && open()"
@mouseleave="trigger == 'hover' && hide()"
:class="{ 'layui-dropdown-up': openState }"
>
<div
@click="trigger == 'click' && toggle()"
@contextmenu.prevent="trigger == 'contextMenu' && toggle()"
>
<slot></slot> <slot></slot>
</div> </div>
<dl class="layui-anim layui-anim-upbit"> <dl ref="contentRef" class="layui-anim layui-anim-upbit" :style="contentStyle">
<slot name="content"></slot> <slot name="content"></slot>
</dl> </dl>
</div> </div>

View File

@ -1 +1,2 @@
export type DropdownTrigger = "click" | "hover" | "contextMenu"; export type DropdownTrigger = "click" | "hover" | "contextMenu";
export type dropdownPlacement = 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right';

View File

@ -164,12 +164,69 @@ export default {
::: demo ::: demo
<template> <template>
<lay-dropdown> <lay-dropdown placement="top-left">
<lay-button type="primary">下拉菜单</lay-button> <lay-button type="primary">topLeft</lay-button>
<template #content> <template #content>
<div style="width:300px;height:200px;"></div> <div style="width:300px;height:200px;"></div>
</template> </template>
</lay-dropdown> </lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="top">
<lay-button type="primary">top</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="top-right">
<lay-button type="primary">topRight</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-left">
<lay-button type="primary">bottomLeft</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom">
<lay-button type="primary">bottom</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-right">
<lay-button type="primary">bottomRight</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-left" :autoFitWidth="true">
<lay-button type="primary">开启 autoFitWidth</lay-button>
<template #content>
<lay-dropdown-menu>
<lay-dropdown-menu-item>选项一</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项二1111111111111111111111</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项三</lay-dropdown-menu-item>
</lay-dropdown-menu>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-left" :autoFitMinWidth="false">
<lay-button type="primary">关闭 autoFitMinWidth</lay-button>
<template #content>
<lay-dropdown-menu>
<lay-dropdown-menu-item>选项一</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项二111111111</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项三</lay-dropdown-menu-item>
</lay-dropdown-menu>
</template>
</lay-dropdown>
</template> </template>
<script> <script>
@ -195,6 +252,11 @@ export default {
| ------- | -------- | --------------- | | ------- | -------- | --------------- |
| trigger | 触发方式 | `click` `hover` `contextMenu` | | trigger | 触发方式 | `click` `hover` `contextMenu` |
| disabled | 是否禁用触发 | `true` `false` | | disabled | 是否禁用触发 | `true` `false` |
| placement | 下拉面板位置 |`top` `top-left` `top-right` `bottom` `bottom-left` `bottom-right`|
| autoFitPlacement| 是否自适应下拉面板位置,默认 `true` | `true` `false` |
| autoFitWidth | 是否将下拉面板宽度设置为触发器宽度, 默认 `false` | `true` `false` |
| autoFitMinWidth | 是否将下拉面板最小宽度设置为触发器宽度, 默认 `true` | `true` `false` |
::: :::