✨(component): [dropdown]滚动、触发器或面板尺寸变化时更新下拉面板位置; clickOutsideToClose 属性
This commit is contained in:
parent
04035e9a4e
commit
49b0d96b48
@ -9,33 +9,46 @@ import "./index.less";
|
||||
import {
|
||||
CSSProperties,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
shallowRef,
|
||||
} from "vue";
|
||||
import { onClickOutside, useResizeObserver, useScroll, useWindowSize } from "@vueuse/core";
|
||||
import {
|
||||
onClickOutside,
|
||||
useResizeObserver,
|
||||
useThrottleFn,
|
||||
useWindowSize,
|
||||
} from "@vueuse/core";
|
||||
import { DropdownTrigger, dropdownPlacement } from "./interface";
|
||||
|
||||
export interface LayDropdownProps {
|
||||
trigger?: DropdownTrigger;
|
||||
placement?: dropdownPlacement;
|
||||
disabled?: boolean;
|
||||
autoFitPlacement?: boolean;
|
||||
autoFitPosition?: boolean;
|
||||
autoFitWidth?: boolean;
|
||||
autoFitMinWidth?: boolean;
|
||||
updateAtScroll?: boolean;
|
||||
autoFixPosition?: boolean;
|
||||
clickOutsideToClose?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<LayDropdownProps>(), {
|
||||
trigger: "click",
|
||||
disabled: false,
|
||||
placement: "bottom-left",
|
||||
autoFitPlacement: true,
|
||||
autoFitPosition: true,
|
||||
autoFitMinWidth: true,
|
||||
autoFitWidth: false,
|
||||
updateAtScroll: false,
|
||||
autoFixPosition: true,
|
||||
clickOutsideToClose: true,
|
||||
});
|
||||
|
||||
const dropdownRef = shallowRef<null | HTMLElement>();
|
||||
const contentRef = shallowRef<null | HTMLElement>();
|
||||
const dropdownRef = shallowRef<HTMLElement | undefined>();
|
||||
const contentRef = shallowRef<HTMLElement | undefined>();
|
||||
const contentStyle = ref<CSSProperties>({});
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
const openState = ref(false);
|
||||
@ -44,7 +57,9 @@ const contentSpace = 2;
|
||||
const emit = defineEmits(["open", "hide"]);
|
||||
|
||||
onClickOutside(dropdownRef, () => {
|
||||
changeVisible(false);
|
||||
if (props.clickOutsideToClose) {
|
||||
changeVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
const open = (): void => {
|
||||
@ -85,7 +100,7 @@ const updateContentStyle = () => {
|
||||
const triggerRect = dropdownRef.value.getBoundingClientRect();
|
||||
const contentRect = contentRef.value.getBoundingClientRect();
|
||||
const { style } = getContentStyle(props.placement, triggerRect, contentRect, {
|
||||
autoFitPlacement: props.autoFitPlacement,
|
||||
autoFitPosition: props.autoFitPosition,
|
||||
});
|
||||
|
||||
if (props.autoFitMinWidth) {
|
||||
@ -103,15 +118,15 @@ const getContentStyle = (
|
||||
triggerRect: DOMRect,
|
||||
contentRect: DOMRect,
|
||||
{
|
||||
autoFitPlacement = false,
|
||||
autoFitPosition = false,
|
||||
customStyle = {},
|
||||
}: {
|
||||
autoFitPlacement?: boolean;
|
||||
autoFitPosition?: boolean;
|
||||
customStyle?: CSSProperties;
|
||||
} = {}
|
||||
) => {
|
||||
let { top, left } = getContentOffset(placement, triggerRect, contentRect);
|
||||
if (autoFitPlacement) {
|
||||
if (autoFitPosition) {
|
||||
const { top: fitTop, left: fitLeft } = getFitPlacement(
|
||||
top,
|
||||
left,
|
||||
@ -229,13 +244,72 @@ const getContentOffset = (
|
||||
}
|
||||
};
|
||||
|
||||
useResizeObserver(contentRef, () => {
|
||||
if (openState.value) {
|
||||
updateContentStyle()
|
||||
}
|
||||
})
|
||||
const isScrollElement = (element: HTMLElement) => {
|
||||
return (
|
||||
element.scrollHeight > element.offsetHeight ||
|
||||
element.scrollWidth > element.offsetWidth
|
||||
);
|
||||
};
|
||||
|
||||
provide("openState", openState);
|
||||
const getScrollElements = (container: HTMLElement | undefined) => {
|
||||
const scrollElements: HTMLElement[] = [];
|
||||
let element: HTMLElement | undefined = container;
|
||||
while (element && element !== document.documentElement) {
|
||||
if (isScrollElement(element)) {
|
||||
scrollElements.push(element);
|
||||
}
|
||||
element = element.parentElement ?? undefined;
|
||||
}
|
||||
return scrollElements;
|
||||
};
|
||||
|
||||
const handleScroll = useThrottleFn(() => {
|
||||
if (openState.value) {
|
||||
updateContentStyle();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const { stop: removeContentResizeObserver } = useResizeObserver(
|
||||
contentRef,
|
||||
() => {
|
||||
if (openState.value && props.autoFixPosition) {
|
||||
updateContentStyle();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { stop: removeTriggerResizeObserver } = useResizeObserver(
|
||||
dropdownRef,
|
||||
() => {
|
||||
if (openState.value && props.autoFixPosition) {
|
||||
updateContentStyle();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let scrollElements: HTMLElement[] | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
if (props.updateAtScroll) {
|
||||
scrollElements = getScrollElements(dropdownRef.value);
|
||||
for (const item of scrollElements) {
|
||||
item.addEventListener("scroll", handleScroll);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (scrollElements) {
|
||||
for (const item of scrollElements) {
|
||||
item.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
scrollElements = undefined;
|
||||
}
|
||||
|
||||
removeContentResizeObserver();
|
||||
removeTriggerResizeObserver();
|
||||
}),
|
||||
provide("openState", openState);
|
||||
|
||||
defineExpose({ open, hide, toggle });
|
||||
</script>
|
||||
|
@ -206,8 +206,9 @@ export default {
|
||||
</template>
|
||||
</lay-dropdown>
|
||||
|
||||
<lay-dropdown placement="bottom-left" :autoFitWidth="true">
|
||||
<lay-button type="primary">开启 autoFitWidth</lay-button>
|
||||
<br><br>
|
||||
<lay-dropdown placement="bottom-left" autoFitWidth>
|
||||
<lay-button type="primary">autoFitWidth</lay-button>
|
||||
<template #content>
|
||||
<lay-dropdown-menu>
|
||||
<lay-dropdown-menu-item>选项一</lay-dropdown-menu-item>
|
||||
@ -227,6 +228,30 @@ export default {
|
||||
</lay-dropdown-menu>
|
||||
</template>
|
||||
</lay-dropdown>
|
||||
|
||||
<lay-dropdown placement="bottom-left" updateAtScroll>
|
||||
<lay-button type="primary">updateAtScroll</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>
|
||||
|
||||
<lay-dropdown placement="bottom-left" :autoFixPosition="true" :clickOutsideToClose="false">
|
||||
<lay-button type="primary" :size="btnSize">autoFixPosition</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>
|
||||
|
||||
<lay-button @click="toogleSize">切换左边按钮</lay-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -235,7 +260,14 @@ import { ref } from 'vue'
|
||||
export default {
|
||||
setup() {
|
||||
|
||||
const btnSize = ref('')
|
||||
const toogleSize = () => {
|
||||
btnSize.value = btnSize.value ? '' : 'lg'
|
||||
}
|
||||
|
||||
return {
|
||||
btnSize,
|
||||
toogleSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -253,9 +285,12 @@ export default {
|
||||
| trigger | 触发方式 | `click` `hover` `contextMenu` |
|
||||
| disabled | 是否禁用触发 | `true` `false` |
|
||||
| placement | 下拉面板位置 |`top` `top-left` `top-right` `bottom` `bottom-left` `bottom-right`|
|
||||
| autoFitPlacement| 是否自适应下拉面板位置,默认 `true` | `true` `false` |
|
||||
| autoFitPosition| 是否自动调整下拉面板位置,默认 `true` | `true` `false` |
|
||||
| autoFitWidth | 是否将下拉面板宽度设置为触发器宽度, 默认 `false` | `true` `false` |
|
||||
| autoFitMinWidth | 是否将下拉面板最小宽度设置为触发器宽度, 默认 `true` | `true` `false` |
|
||||
| updateAtScroll | 是否在容器滚动时更新下拉面板的位置,默认 `false` | `true` `false` |
|
||||
| autoFixPosition | 是否在触发器或下拉面板尺寸变化时更新下拉面板位置,面板尺寸变化参见级联选择器,默认 `true` | `true` `false` |
|
||||
| clickOutsideToClose| 是否点击外部关闭下拉面板,默认 `true`| `true` `false`|
|
||||
|
||||
|
||||
:::
|
||||
|
Loading…
Reference in New Issue
Block a user