initial
This commit is contained in:
187
layers/ui/components/dropdown/index.vue
Normal file
187
layers/ui/components/dropdown/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user