(component): [dropdown]滚动、触发器或面板尺寸变化时更新下拉面板位置; clickOutsideToClose 属性

This commit is contained in:
sight 2022-06-22 21:06:47 +08:00
parent 04035e9a4e
commit 49b0d96b48
2 changed files with 128 additions and 19 deletions

View File

@ -9,33 +9,46 @@ import "./index.less";
import { import {
CSSProperties, CSSProperties,
nextTick, nextTick,
onBeforeUnmount,
onMounted,
provide, provide,
ref, ref,
shallowRef, shallowRef,
} from "vue"; } from "vue";
import { onClickOutside, useResizeObserver, useScroll, useWindowSize } from "@vueuse/core"; import {
onClickOutside,
useResizeObserver,
useThrottleFn,
useWindowSize,
} from "@vueuse/core";
import { DropdownTrigger, dropdownPlacement } from "./interface"; import { DropdownTrigger, dropdownPlacement } from "./interface";
export interface LayDropdownProps { export interface LayDropdownProps {
trigger?: DropdownTrigger; trigger?: DropdownTrigger;
placement?: dropdownPlacement; placement?: dropdownPlacement;
disabled?: boolean; disabled?: boolean;
autoFitPlacement?: boolean; autoFitPosition?: boolean;
autoFitWidth?: boolean; autoFitWidth?: boolean;
autoFitMinWidth?: boolean; autoFitMinWidth?: boolean;
updateAtScroll?: boolean;
autoFixPosition?: boolean;
clickOutsideToClose?: boolean;
} }
const props = withDefaults(defineProps<LayDropdownProps>(), { const props = withDefaults(defineProps<LayDropdownProps>(), {
trigger: "click", trigger: "click",
disabled: false, disabled: false,
placement: "bottom-left", placement: "bottom-left",
autoFitPlacement: true, autoFitPosition: true,
autoFitMinWidth: true, autoFitMinWidth: true,
autoFitWidth: false, autoFitWidth: false,
updateAtScroll: false,
autoFixPosition: true,
clickOutsideToClose: true,
}); });
const dropdownRef = shallowRef<null | HTMLElement>(); const dropdownRef = shallowRef<HTMLElement | undefined>();
const contentRef = shallowRef<null | HTMLElement>(); const contentRef = shallowRef<HTMLElement | undefined>();
const contentStyle = ref<CSSProperties>({}); const contentStyle = ref<CSSProperties>({});
const { width: windowWidth, height: windowHeight } = useWindowSize(); const { width: windowWidth, height: windowHeight } = useWindowSize();
const openState = ref(false); const openState = ref(false);
@ -44,7 +57,9 @@ const contentSpace = 2;
const emit = defineEmits(["open", "hide"]); const emit = defineEmits(["open", "hide"]);
onClickOutside(dropdownRef, () => { onClickOutside(dropdownRef, () => {
changeVisible(false); if (props.clickOutsideToClose) {
changeVisible(false);
}
}); });
const open = (): void => { const open = (): void => {
@ -85,7 +100,7 @@ const updateContentStyle = () => {
const triggerRect = dropdownRef.value.getBoundingClientRect(); const triggerRect = dropdownRef.value.getBoundingClientRect();
const contentRect = contentRef.value.getBoundingClientRect(); const contentRect = contentRef.value.getBoundingClientRect();
const { style } = getContentStyle(props.placement, triggerRect, contentRect, { const { style } = getContentStyle(props.placement, triggerRect, contentRect, {
autoFitPlacement: props.autoFitPlacement, autoFitPosition: props.autoFitPosition,
}); });
if (props.autoFitMinWidth) { if (props.autoFitMinWidth) {
@ -103,15 +118,15 @@ const getContentStyle = (
triggerRect: DOMRect, triggerRect: DOMRect,
contentRect: DOMRect, contentRect: DOMRect,
{ {
autoFitPlacement = false, autoFitPosition = false,
customStyle = {}, customStyle = {},
}: { }: {
autoFitPlacement?: boolean; autoFitPosition?: boolean;
customStyle?: CSSProperties; customStyle?: CSSProperties;
} = {} } = {}
) => { ) => {
let { top, left } = getContentOffset(placement, triggerRect, contentRect); let { top, left } = getContentOffset(placement, triggerRect, contentRect);
if (autoFitPlacement) { if (autoFitPosition) {
const { top: fitTop, left: fitLeft } = getFitPlacement( const { top: fitTop, left: fitLeft } = getFitPlacement(
top, top,
left, left,
@ -229,13 +244,72 @@ const getContentOffset = (
} }
}; };
useResizeObserver(contentRef, () => { const isScrollElement = (element: HTMLElement) => {
if (openState.value) { return (
updateContentStyle() 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 }); defineExpose({ open, hide, toggle });
</script> </script>

View File

@ -206,8 +206,9 @@ export default {
</template> </template>
</lay-dropdown> </lay-dropdown>
&nbsp;&nbsp; &nbsp;&nbsp;
<lay-dropdown placement="bottom-left" :autoFitWidth="true"> <br><br>
<lay-button type="primary">开启 autoFitWidth</lay-button> <lay-dropdown placement="bottom-left" autoFitWidth>
<lay-button type="primary">autoFitWidth</lay-button>
<template #content> <template #content>
<lay-dropdown-menu> <lay-dropdown-menu>
<lay-dropdown-menu-item>选项一</lay-dropdown-menu-item> <lay-dropdown-menu-item>选项一</lay-dropdown-menu-item>
@ -227,6 +228,30 @@ export default {
</lay-dropdown-menu> </lay-dropdown-menu>
</template> </template>
</lay-dropdown> </lay-dropdown>
&nbsp;&nbsp;
<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>
&nbsp;&nbsp;
<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>
&nbsp;&nbsp;
<lay-button @click="toogleSize">切换左边按钮</lay-button>
</template> </template>
<script> <script>
@ -235,7 +260,14 @@ import { ref } from 'vue'
export default { export default {
setup() { setup() {
const btnSize = ref('')
const toogleSize = () => {
btnSize.value = btnSize.value ? '' : 'lg'
}
return { return {
btnSize,
toogleSize
} }
} }
} }
@ -253,9 +285,12 @@ 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`| | placement | 下拉面板位置 |`top` `top-left` `top-right` `bottom` `bottom-left` `bottom-right`|
| autoFitPlacement| 是否自适应下拉面板位置,默认 `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` |
| autoFixPosition | 是否在触发器或下拉面板尺寸变化时更新下拉面板位置,面板尺寸变化参见级联选择器,默认 `true` | `true` `false` |
| clickOutsideToClose| 是否点击外部关闭下拉面板,默认 `true`| `true` `false`|
::: :::