Files
money.sori.studio/components/BottomSheetSelect.vue
2026-08-13 16:27:44 +09:00

102 lines
3.3 KiB
Vue

<template>
<div>
<button
type="button"
class="field flex min-h-[50px] items-center justify-between text-left"
aria-haspopup="dialog"
:aria-expanded="open"
@click="open = true"
>
<span :class="selected ? 'text-ink' : 'text-muted'">{{ selected?.label || placeholder }}</span>
</button>
<Teleport to="body">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="open" class="fixed inset-0 z-40 bg-ink/35" @click="open = false" />
</Transition>
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-full"
enter-to-class="translate-y-0"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0"
leave-to-class="translate-y-full"
>
<section
v-if="open"
class="fixed inset-x-0 bottom-0 z-50 mx-auto max-h-[78vh] max-w-lg overflow-hidden rounded-t-2xl bg-white shadow-2xl"
role="dialog"
:aria-label="title"
>
<div class="sticky top-0 border-b border-line bg-white px-4 pb-3 pt-2">
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-line" />
<div class="flex items-center justify-between">
<h2 class="text-base font-bold">{{ title }}</h2>
<button type="button" class="rounded-md px-2 py-1 text-sm font-semibold text-muted" @click="open = false">닫기</button>
</div>
</div>
<div class="max-h-[60vh] overflow-y-auto px-2 py-2">
<button
v-for="option in options"
:key="option.value"
type="button"
class="flex w-full items-center justify-between rounded-md px-3 py-3 text-left active:bg-paper"
@click="select(option.value)"
>
<span>
<span class="block text-sm font-semibold text-ink">{{ option.label }}</span>
<span v-if="option.description" class="mt-1 block text-xs text-muted">{{ option.description }}</span>
</span>
<span
class="ml-3 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border text-sm"
:class="option.value === modelValue ? 'border-brand-700 bg-brand-700 text-white' : 'border-line text-transparent'"
>
✓
</span>
</button>
</div>
</section>
</Transition>
</Teleport>
</div>
</template>
<script setup lang="ts">
interface Option {
value: string
label: string
description?: string
}
const props = withDefaults(defineProps<{
modelValue: string
options: Option[]
title: string
placeholder?: string
}>(), {
placeholder: '선택'
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const open = ref(false)
const selected = computed(() => props.options.find(option => option.value === props.modelValue))
function select(value: string) {
emit('update:modelValue', value)
open.value = false
}
</script>