fix: 드롭다운 메뉴를 body Teleport(fixed)로 띄워 잘림 해결 + 보고 모달 높이 확대
DropdownSelect 메뉴를 absolute → body Teleport + fixed 위치 계산으로 변경. 스크롤/overflow 컨테이너(보고 모달 등) 안에서도 메뉴가 잘리지 않음(스크롤·리사이즈 시 닫힘). 보고 열람/작성 모달 max-height 를 calc(100vh - 3rem) 로 확대. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts" generic="T extends string">
|
||||
// 커스텀 드롭다운(셀렉트) — 네이티브 select 대신 디자인 토큰에 맞춘 정렬/필터 공통 UI.
|
||||
// 사용처: 프로젝트 목록·업무 목록 정렬, 내 업무 프로젝트/정렬, 활동 멤버 필터.
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
// 메뉴는 body 로 Teleport + fixed 위치로 띄워, 스크롤/overflow 컨테이너 안에서도 잘리지 않는다.
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
interface Option {
|
||||
value: T
|
||||
@@ -25,36 +25,67 @@ const emit = defineEmits<{ (e: 'update:modelValue', value: T): void }>()
|
||||
|
||||
const open = ref(false)
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const triggerRef = ref<HTMLElement | null>(null)
|
||||
const menuRef = ref<HTMLElement | null>(null)
|
||||
// 메뉴 fixed 위치(트리거 기준 계산)
|
||||
const menuStyle = ref<Record<string, string>>({})
|
||||
|
||||
// 현재 선택된 옵션 라벨(트리거에 표시)
|
||||
const selectedLabel = computed(
|
||||
() => props.options.find((o) => o.value === props.modelValue)?.label ?? '',
|
||||
)
|
||||
|
||||
// 트리거 위치 기준으로 메뉴 좌표 계산(뷰포트 fixed)
|
||||
function positionMenu(): void {
|
||||
const el = triggerRef.value
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
const style: Record<string, string> = {
|
||||
top: `${r.bottom + 6}px`,
|
||||
minWidth: `${r.width}px`,
|
||||
}
|
||||
if (props.align === 'right') {
|
||||
style.right = `${Math.max(0, window.innerWidth - r.right)}px`
|
||||
} else {
|
||||
style.left = `${r.left}px`
|
||||
}
|
||||
menuStyle.value = style
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
open.value = !open.value
|
||||
if (open.value) void nextTick(positionMenu)
|
||||
}
|
||||
function select(value: T): void {
|
||||
emit('update:modelValue', value)
|
||||
open.value = false
|
||||
}
|
||||
|
||||
// 바깥 클릭 / Esc 로 닫기
|
||||
// 바깥 클릭(트리거/메뉴 외부) / Esc 로 닫기
|
||||
function onDocClick(e: MouseEvent): void {
|
||||
if (open.value && root.value && !root.value.contains(e.target as Node)) {
|
||||
open.value = false
|
||||
}
|
||||
if (!open.value) return
|
||||
const t = e.target as Node
|
||||
if (root.value?.contains(t) || menuRef.value?.contains(t)) return
|
||||
open.value = false
|
||||
}
|
||||
function onKeydown(e: KeyboardEvent): void {
|
||||
if (open.value && e.key === 'Escape') open.value = false
|
||||
}
|
||||
// 스크롤/리사이즈 시 위치가 어긋나므로 닫는다(간단·안전)
|
||||
function onReposition(): void {
|
||||
if (open.value) open.value = false
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocClick)
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
window.addEventListener('scroll', onReposition, true)
|
||||
window.addEventListener('resize', onReposition)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocClick)
|
||||
document.removeEventListener('keydown', onKeydown)
|
||||
window.removeEventListener('scroll', onReposition, true)
|
||||
window.removeEventListener('resize', onReposition)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -65,6 +96,7 @@ onBeforeUnmount(() => {
|
||||
:class="{ push: pushRight, block }"
|
||||
>
|
||||
<button
|
||||
ref="triggerRef"
|
||||
type="button"
|
||||
class="dd-trigger"
|
||||
:class="{ open }"
|
||||
@@ -90,34 +122,37 @@ onBeforeUnmount(() => {
|
||||
><path d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
v-if="open"
|
||||
class="dd-menu"
|
||||
:class="align === 'right' ? 'right' : 'left'"
|
||||
role="listbox"
|
||||
>
|
||||
<li
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
class="dd-opt"
|
||||
:class="{ active: opt.value === modelValue }"
|
||||
role="option"
|
||||
:aria-selected="opt.value === modelValue"
|
||||
@click="select(opt.value)"
|
||||
<Teleport to="body">
|
||||
<ul
|
||||
v-if="open"
|
||||
ref="menuRef"
|
||||
class="dd-menu"
|
||||
:style="menuStyle"
|
||||
role="listbox"
|
||||
>
|
||||
<span class="dd-opt-label">{{ opt.label }}</span>
|
||||
<svg
|
||||
v-if="opt.value === modelValue"
|
||||
class="dd-check"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</li>
|
||||
</ul>
|
||||
<li
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
class="dd-opt"
|
||||
:class="{ active: opt.value === modelValue }"
|
||||
role="option"
|
||||
:aria-selected="opt.value === modelValue"
|
||||
@click="select(opt.value)"
|
||||
>
|
||||
<span class="dd-opt-label">{{ opt.label }}</span>
|
||||
<svg
|
||||
v-if="opt.value === modelValue"
|
||||
class="dd-check"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</li>
|
||||
</ul>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -191,12 +226,10 @@ onBeforeUnmount(() => {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 메뉴 */
|
||||
/* 메뉴 — body 로 Teleport, fixed 위치(좌표는 인라인 style) */
|
||||
.dd-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.375rem);
|
||||
z-index: 60;
|
||||
min-width: 100%;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
padding: 0.25rem;
|
||||
list-style: none;
|
||||
@@ -205,12 +238,6 @@ onBeforeUnmount(() => {
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 24px rgba(20, 24, 33, 0.14);
|
||||
}
|
||||
.dd-menu.left {
|
||||
left: 0;
|
||||
}
|
||||
.dd-menu.right {
|
||||
right: 0;
|
||||
}
|
||||
.dd-opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3576,7 +3576,7 @@ a.file-info {
|
||||
/* 보고 열람 모달 */
|
||||
.cp-modal {
|
||||
width: min(34rem, calc(100vw - 2rem));
|
||||
max-height: calc(100vh - 4rem);
|
||||
max-height: calc(100vh - 3rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
Reference in New Issue
Block a user