188 lines
3.7 KiB
Vue
188 lines
3.7 KiB
Vue
<template>
|
|
<div
|
|
ref="rootEl"
|
|
:class="[cn.b(), cn.is('active', active), cn.is('focused', focused)]"
|
|
>
|
|
<UiRenderless ref="triggerEl">
|
|
<slot v-bind="{ isActive: active }" />
|
|
</UiRenderless>
|
|
|
|
<Teleport
|
|
to="body"
|
|
:disabled="!teleport"
|
|
>
|
|
<div
|
|
v-if="withOverlay"
|
|
:class="[cn.e('overlay'), cn.is('active', active)]"
|
|
/>
|
|
|
|
<Transition
|
|
name="dropdown"
|
|
@after-leave="scrollbarInstance()?.destroy()"
|
|
>
|
|
<div
|
|
v-if="active"
|
|
ref="popperEl"
|
|
:class="[cn.e('popper')]"
|
|
:style="floatingStyles"
|
|
>
|
|
<ul
|
|
ref="scrollParentEl"
|
|
role="listbox"
|
|
:class="[cn.e('dropdown'), dropdownClass]"
|
|
>
|
|
<slot name="dropdown">
|
|
<span>Content</span>
|
|
</slot>
|
|
</ul>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { provide, ref, toRef, watch } from 'vue'
|
|
import {
|
|
autoUpdate,
|
|
flip as floatingFlip,
|
|
offset as floatingOffset,
|
|
useFloating,
|
|
} from '@floating-ui/vue'
|
|
import {
|
|
onClickOutside,
|
|
useElementHover,
|
|
useEventListener,
|
|
} from '@vueuse/core'
|
|
|
|
import type { OffsetOptions, Placement } from '@floating-ui/vue'
|
|
import { useOverlayScrollbars } from 'overlayscrollbars-vue'
|
|
import useClassname from '../../composables/use-classname.js'
|
|
import UiRenderless from '../renderless/index.vue'
|
|
import { dropdownContextKey } from './constants'
|
|
|
|
export interface Props {
|
|
trigger?: 'hover' | 'click'
|
|
disabled?: false
|
|
offset?: OffsetOptions
|
|
placement?: Placement
|
|
hideOnClick?: boolean
|
|
withOverlay?: boolean
|
|
dropdownClass?: string
|
|
transitionName?: string
|
|
teleport?: boolean
|
|
}
|
|
|
|
defineOptions({
|
|
name: 'UiDropdown',
|
|
})
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
trigger: 'click',
|
|
disabled: false,
|
|
placement: 'bottom-start',
|
|
offset: 4,
|
|
hideOnClick: true,
|
|
withOverlay: false,
|
|
transitionName: 'ui-dropdown',
|
|
teleport: false,
|
|
})
|
|
|
|
defineEmits<{
|
|
'update:modelValue': []
|
|
focus: []
|
|
blur: []
|
|
}>()
|
|
|
|
const rootEl = ref<HTMLElement | null>()
|
|
const triggerEl = ref()
|
|
const popperEl = ref()
|
|
const scrollParentEl = ref()
|
|
|
|
const active = ref(false)
|
|
const focused = ref(false)
|
|
|
|
const cn = useClassname('ui-dropdown')
|
|
|
|
if (props.trigger === 'click') {
|
|
useEventListener(triggerEl, 'click', () => {
|
|
toggle()
|
|
})
|
|
|
|
onClickOutside(
|
|
popperEl,
|
|
() => {
|
|
close()
|
|
},
|
|
{ ignore: [triggerEl] },
|
|
)
|
|
}
|
|
else {
|
|
const isHoveredTrigger = useElementHover(triggerEl, { delayLeave: 300 })
|
|
const isHoveredPopper = useElementHover(popperEl, { delayLeave: 10 })
|
|
|
|
watch(isHoveredTrigger, (value: boolean) => {
|
|
if (value)
|
|
open()
|
|
else if (!isHoveredPopper.value)
|
|
close()
|
|
})
|
|
|
|
watch(isHoveredPopper, async (value: boolean) => {
|
|
if (!value && !isHoveredTrigger.value)
|
|
close()
|
|
})
|
|
}
|
|
|
|
const { floatingStyles } = useFloating(triggerEl, popperEl, {
|
|
placement: toRef(props, 'placement'),
|
|
middleware: [floatingOffset(props.offset), floatingFlip()],
|
|
whileElementsMounted: autoUpdate,
|
|
})
|
|
|
|
const [initScrollbar, scrollbarInstance] = useOverlayScrollbars({
|
|
options: {
|
|
scrollbars: {
|
|
theme: 'os-small-theme',
|
|
},
|
|
},
|
|
})
|
|
|
|
function setActive(state: boolean) {
|
|
if (props.disabled)
|
|
return
|
|
|
|
active.value = state
|
|
}
|
|
|
|
function toggle() {
|
|
setActive(!active.value)
|
|
}
|
|
|
|
function open() {
|
|
setActive(true)
|
|
}
|
|
|
|
function close() {
|
|
setActive(false)
|
|
}
|
|
|
|
function handleItemClick() {
|
|
if (props.hideOnClick)
|
|
close()
|
|
}
|
|
|
|
// watch(scrollParentEl, (value) => {
|
|
// if (value)
|
|
// initScrollbar(value)
|
|
// })
|
|
|
|
provide(dropdownContextKey, {
|
|
handleItemClick,
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@use 'styles';
|
|
</style>
|