feat: Add select component
This commit is contained in:
parent
f9474917af
commit
ceed039a80
156
src/components/common/Select.vue
Normal file
156
src/components/common/Select.vue
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<!-- eslint-disable vue/multi-word-component-names -->
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import FilledButton from './FilledButton.vue';
|
||||||
|
|
||||||
|
const { placeholder, options, optionName } = defineProps({
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '----',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
optionName: {
|
||||||
|
type: String,
|
||||||
|
default: 'value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
const buttonRef = ref(null);
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const menuStyle = ref('');
|
||||||
|
const selected = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const rect = buttonRef.value.$el.getBoundingClientRect();
|
||||||
|
|
||||||
|
menuStyle.value = `transform: translate(${rect.left}px, ${rect.bottom + 4}px)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedName = computed(() => selected.value?.[optionName] ?? selected.value);
|
||||||
|
|
||||||
|
function handleOpen() {
|
||||||
|
console.log('open');
|
||||||
|
isOpen.value = true;
|
||||||
|
body.style.pointerEvents = 'none';
|
||||||
|
body.style.overflow = 'hidden';
|
||||||
|
document.addEventListener('pointerdown', handleClose, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
isOpen.value = false;
|
||||||
|
body.removeAttribute('style');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose({ target }) {
|
||||||
|
if (target && target?.tagName !== 'HTML') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(option) {
|
||||||
|
selected.value = option;
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FilledButton
|
||||||
|
:class="'select' + (isOpen ? ' active' : '')"
|
||||||
|
ref="buttonRef"
|
||||||
|
@mousedown="handleOpen"
|
||||||
|
>
|
||||||
|
<span>{{ selectedName ?? placeholder }}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden=""
|
||||||
|
>
|
||||||
|
<path d="m6 9 6 6 6-6"></path>
|
||||||
|
</svg>
|
||||||
|
</FilledButton>
|
||||||
|
<Teleport v-if="isOpen" to="body">
|
||||||
|
<div class="menu-wrapper" :style="menuStyle">
|
||||||
|
<ul class="menu">
|
||||||
|
<li
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
@click.stop="handleSelect(option)"
|
||||||
|
:class="'menu-item' + (selected === option ? 'selected' : '')"
|
||||||
|
>
|
||||||
|
{{ option[optionName] ?? option }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes show {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, -8px, 0) scale3d(0.95, 0.95, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select svg {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
min-width: max-content;
|
||||||
|
pointer-events: auto;
|
||||||
|
will-change: transform;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
animation: show 0.25s;
|
||||||
|
padding: 4px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.selected {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user