Popover 팝오버
서식이 풍부한 콘텐츠의 팝업: Tooltip보다 더 많은 정보(제목, 본문, 동작 버튼)를 담습니다. 트리거는 기본 슬롯으로 감싸고, 팝업 콘텐츠는 content 슬롯으로 전달합니다. 클릭으로 트리거할 때 팝업은 role="dialog"이며 포커스를 가두고, 마우스 올림으로 트리거할 때는 콘텐츠만 표시하고 키보드 흐름을 끊지 않습니다.
기본 사용법
클릭 트리거
vue
<script setup lang="ts">
</script>
<template>
<LhPopover title="수묵 알림" placement="top">
<LhButton>클릭하여 팝업</LhButton>
<template #content>
<p>이것은 서식이 풍부한 콘텐츠이며, 임의의 markup을 넣을 수 있습니다.</p>
</template>
</LhPopover>
</template>vue
<script>
export default {}
</script>
<template>
<LhPopover title="수묵 알림" placement="top">
<LhButton>클릭하여 팝업</LhButton>
<template #content>
<p>이것은 서식이 풍부한 콘텐츠이며, 임의의 markup을 넣을 수 있습니다.</p>
</template>
</LhPopover>
</template>트리거 방식
hover / focus / manual
vue
<template>
<!-- 마우스 올림 트리거 -->
<LhPopover trigger="hover" title="마우스 올림 트리거" :show-after="0">
<LhButton>마우스를 올리세요</LhButton>
<template #content><p>포인터가 팝업으로 들어가도 닫히지 않습니다.</p></template>
</LhPopover>
<!-- 포커스 트리거 -->
<LhPopover trigger="focus" title="포커스 트리거" :show-after="0">
<LhInput placeholder="포커스 입력 상자" />
<template #content><p>입력 상자가 포커스를 받으면 표시됩니다.</p></template>
</LhPopover>
<!-- 수동 제어 -->
<LhPopover v-model:visible="manualVisible" trigger="manual" title="수동 제어">
<LhButton @click="manualVisible = !manualVisible">외부 스위치</LhButton>
<template #content><p>표시/숨김은 외부 v-model이 완전히 제어합니다.</p></template>
</LhPopover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const manualVisible = ref(false)
</script>vue
<template>
<!-- 마우스 올림 트리거 -->
<LhPopover trigger="hover" title="마우스 올림 트리거" :show-after="0">
<LhButton>마우스를 올리세요</LhButton>
<template #content><p>포인터가 팝업으로 들어가도 닫히지 않습니다.</p></template>
</LhPopover>
<!-- 포커스 트리거 -->
<LhPopover trigger="focus" title="포커스 트리거" :show-after="0">
<LhInput placeholder="포커스 입력 상자" />
<template #content><p>입력 상자가 포커스를 받으면 표시됩니다.</p></template>
</LhPopover>
<!-- 수동 제어 -->
<LhPopover v-model:visible="manualVisible" trigger="manual" title="수동 제어">
<LhButton @click="manualVisible = !manualVisible">외부 스위치</LhButton>
<template #content><p>표시/숨김은 외부 v-model이 완전히 제어합니다.</p></template>
</LhPopover>
</template>
<script>
export default {
data() {
return { manualVisible: false }
},
}
</script>12가지 방위
placement
vue
<template>
<LhPopover
v-for="p in placements"
:key="p"
:placement="p"
:title="p"
trigger="hover"
:show-after="0"
>
<LhButton size="small">{{ p }}</LhButton>
<template #content><p>위치: {{ p }}</p></template>
</LhPopover>
</template>
<script setup lang="ts">
const placements = [
'top', 'top-start', 'top-end',
'bottom', 'bottom-start', 'bottom-end',
'left', 'left-start', 'left-end',
'right', 'right-start', 'right-end',
] as const
</script>vue
<template>
<LhPopover
v-for="p in placements"
:key="p"
:placement="p"
:title="p"
trigger="hover"
:show-after="0"
>
<LhButton size="small">{{ p }}</LhButton>
<template #content><p>위치: {{ p }}</p></template>
</LhPopover>
</template>
<script>
export default {
data() {
return {
placements: [
'top', 'top-start', 'top-end',
'bottom', 'bottom-start', 'bottom-end',
'left', 'left-start', 'left-end',
'right', 'right-start', 'right-end',
],
}
},
}
</script>뷰포트 공간이 부족하면 팝업이 자동으로 방향을 뒤집고(flip) 교차축으로 끼워 넣으며(shift), 화살표 위치도 함께 전환됩니다.
중첩 인터랙션 콘텐츠
폼 예시
vue
<template>
<LhPopover placement="right" title="빠른 메모">
<LhButton>메모 추가</LhButton>
<template #content>
<div class="demo-popover-form">
<LhInput v-model="note" placeholder="메모 내용을 입력하세요" />
<LhButton type="primary" @click="saveNote">저장</LhButton>
</div>
</template>
</LhPopover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const note = ref('')
const saved = ref('')
function saveNote() {
saved.value = note.value
note.value = ''
}
</script>vue
<template>
<LhPopover placement="right" title="빠른 메모">
<LhButton>메모 추가</LhButton>
<template #content>
<div class="demo-popover-form">
<LhInput v-model="note" placeholder="메모 내용을 입력하세요" />
<LhButton type="primary" @click="saveNote">저장</LhButton>
</div>
</template>
</LhPopover>
</template>
<script>
export default {
data() {
return { note: '', saved: '' }
},
methods: {
saveNote() {
this.saved = this.note
this.note = ''
},
},
}
</script>더 많은 프로퍼티
disabled / arrow / width / hideAfter / popperClass / teleportTarget
Teleport 컨테이너(#demo-popover-host)
vue
<script setup lang="ts">
</script>
<template>
<!-- 비활성화됨 -->
<LhPopover disabled title="비활성화됨">
<LhButton disabled>비활성화된 트리거</LhButton>
<template #content><p>disabled일 때 트리거와 팝업 모두 반응하지 않습니다.</p></template>
</LhPopover>
<!-- 화살표 없음 -->
<LhPopover title="화살표 없음" :arrow="false" :show-after="0">
<LhButton>화살표 없음</LhButton>
<template #content><p>arrow가 false이면 가리키는 화살표를 렌더링하지 않습니다.</p></template>
</LhPopover>
<!-- 고정 너비 -->
<LhPopover title="고정 너비" width="240" :show-after="0">
<LhButton>고정 너비</LhButton>
<template #content><p>width로 팝업 너비를 지정합니다. 여기서는 240px입니다.</p></template>
</LhPopover>
<!-- 지연 숨김 -->
<LhPopover title="지연 숨김" :hide-after="800" :show-after="0">
<LhButton>지연 숨김</LhButton>
<template #content><p>트리거에서 벗어난 후 800ms가 지나야 접힙니다.</p></template>
</LhPopover>
<!-- 커스텀 클래스명 -->
<LhPopover title="커스텀 클래스명" popper-class="demo-popper-accent" :show-after="0">
<LhButton>커스텀 클래스명</LhButton>
<template #content><p>popperClass는 팝업 루트 요소에 추가되며 스타일 커스터마이징에 사용할 수 있습니다.</p></template>
</LhPopover>
<!-- 전송 대상 지정 -->
<LhPopover title="전송 대상 지정" teleport-target="#demo-popover-host" :show-after="0">
<LhButton>Teleport 컨테이너</LhButton>
<template #content><p>팝업이 body 대신 아래 컨테이너로 전송됩니다.</p></template>
</LhPopover>
</template>vue
<script>
export default {}
</script>
<template>
<!-- 비활성화됨 -->
<LhPopover disabled title="비활성화됨">
<LhButton disabled>비활성화된 트리거</LhButton>
<template #content><p>disabled일 때 트리거와 팝업 모두 반응하지 않습니다.</p></template>
</LhPopover>
<!-- 화살표 없음 -->
<LhPopover title="화살표 없음" :arrow="false" :show-after="0">
<LhButton>화살표 없음</LhButton>
<template #content><p>arrow가 false이면 가리키는 화살표를 렌더링하지 않습니다.</p></template>
</LhPopover>
<!-- 고정 너비 -->
<LhPopover title="고정 너비" width="240" :show-after="0">
<LhButton>고정 너비</LhButton>
<template #content><p>width로 팝업 너비를 지정합니다. 여기서는 240px입니다.</p></template>
</LhPopover>
<!-- 지연 숨김 -->
<LhPopover title="지연 숨김" :hide-after="800" :show-after="0">
<LhButton>지연 숨김</LhButton>
<template #content><p>트리거에서 벗어난 후 800ms가 지나야 접힙니다.</p></template>
</LhPopover>
<!-- 커스텀 클래스명 -->
<LhPopover title="커스텀 클래스명" popper-class="demo-popper-accent" :show-after="0">
<LhButton>커스텀 클래스명</LhButton>
<template #content><p>popperClass는 팝업 루트 요소에 추가되며 스타일 커스터마이징에 사용할 수 있습니다.</p></template>
</LhPopover>
<!-- 전송 대상 지정 -->
<LhPopover title="전송 대상 지정" teleport-target="#demo-popover-host" :show-after="0">
<LhButton>Teleport 컨테이너</LhButton>
<template #content><p>팝업이 body 대신 아래 컨테이너로 전송됩니다.</p></template>
</LhPopover>
</template>제어되는 표시/숨김
v-model:visible으로 팝업의 표시/숨김을 수동으로 제어합니다.
제어되는 표시/숨김
vue
<script setup lang="ts">
import { ref } from 'vue'
const popVisible = ref(false)
</script>
<template>
<LhPopover v-model:visible="popVisible" title="제어되는 팝업" trigger="manual">
<LhButton>{{ popVisible ? '접기' : '펼치기' }}</LhButton>
</LhPopover>
</template>vue
<script>
export default {
data() {
return { popVisible: false }
},
}
</script>
<template>
<LhPopover v-model:visible="popVisible" title="제어되는 팝업" trigger="manual">
<LhButton>{{ popVisible ? '접기' : '펼치기' }}</LhButton>
</LhPopover>
</template>Popover API
Props
| 프로퍼티 | 설명 | 타입 | 기본값 |
|---|---|---|---|
| placement | 표시 위치 | "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end" | 'top' |
| trigger | 트리거 방식 | "click" | "hover" | "focus" | "manual" | 'click' |
| title | 제목(title 슬롯으로도 가능) | string | — |
| visible | 수동으로 제어하는 표시 상태(v-model:visible) | boolean | — |
| disabled | 비활성화 여부 | boolean | — |
| arrow | 화살표 표시 여부 | boolean | true |
| width | 팝업 너비 | string | number | — |
| popperClass | 팝업 커스텀 클래스명 | string | — |
| teleportTarget | Teleport 대상(선택자 또는 요소) | string | HTMLElement | 'body' |
| showAfter | 표시 지연 시간(ms) | number | — |
| hideAfter | 숨김 지연 시간(ms) | number | — |
| themeVars | 컴포넌트 수준 테마 변수, ConfigProvider 설정을 덮어씀 | { [x: string]: string; } | — |
| bgPattern | 컴포넌트 수준 배경 문양, ConfigProvider 설정을 덮어씀 | string | — |
| sx | 빠른 레이아웃 속성(트리거 루트 요소에 적용) | LhSxProps | — |
Events
| 이벤트 | 설명 | 매개변수 |
|---|---|---|
| update:visible | 표시/숨김 변경 | (value: boolean) |
| show | 표시된 후 발생 | — |
| hide | 숨겨진 후 발생 | — |
Slots
| 슬롯 | 설명 |
|---|---|
| default | 트리거 콘텐츠 |
| title | 커스텀 제목 |
| content | 팝업 콘텐츠 |
Methods
| 메서드 | 설명 |
|---|---|
| show | 프로그래밍 방식으로 표시 |
| hide | 프로그래밍 방식으로 숨김 |
| toggle | 프로그래밍 방식으로 전환 |
접근성
- 클릭으로 트리거할 때 팝업은
role="dialog"+aria-modal="true"이며, 트리거에는aria-haspopup/aria-expanded가 있습니다 - 클릭 트리거는 포커스를 팝업의 첫 번째 포커스 가능 요소로 옮기고, Tab이 팝업 안에서 순환하며, 닫은 후 트리거로 포커스를 돌려줍니다
- 마우스 올림/포커스 트리거에서는 포커스를 옮기지 않아 키보드 조작을 끊지 않습니다
- Escape로 닫고, 팝업 바깥을 클릭해도 닫습니다; 제목이 있으면
aria-labelledby로 연결됩니다 - 위치 엔진은 자체 제작한 순수 함수입니다(flip/shift로 오버플로우 방지), 서드파티 런타임 의존이 없습니다
트리거가 뷰포트 밖으로 스크롤되면 접힙니다: 팝업은
fixed로 위치하며, 트리거가 뷰포트를 벗어나면 더 이상 트리거를 따라 이동하지 않고 높은 z-index로 페이지 콘텐츠 위에 떠 있게 됩니다. 따라서 어떤 트리거 방식이든(manual제어 모드 포함), 트리거 전체가 뷰포트 밖으로 스크롤되면 패널이 접히며update:visible을 내보내 외부 상태를 동기화합니다.
타입 정의
이 컴포넌트가 외부로 노출하는 TypeScript 타입 구조는 다음과 같습니다(packages/components/popover/src/types.ts와 @lh-ui/share/core/* 소스에서 자동 추출, 각 항목을 클릭하여 전체 정의를 펼쳐 볼 수 있습니다; 프로퍼티 설명은 위쪽 Props 표를 참고하세요):
LhPopoverProps
ts
/**
* Popover 컴포넌트 Props
*/
export interface LhPopoverProps {
/** 표시 위치 */
placement?: LhPlacement
/** 트리거 방식 */
trigger?: LhPopoverTrigger
/** 제목 */
title?: string
/** 수동으로 제어하는 표시 상태(v-model:visible) */
visible?: boolean
/** 비활성화 여부 */
disabled?: boolean
/** 화살표 표시 여부 */
arrow?: boolean
/** 팝업 너비 */
width?: string | number
/** 팝업 커스텀 클래스명 */
popperClass?: string
/** Teleport 대상(선택자 또는 요소, 기본값 body) */
teleportTarget?: string | HTMLElement
/** 표시 지연 시간(ms) */
showAfter?: number
/** 숨김 지연 시간(ms) */
hideAfter?: number
/** 컴포넌트 수준 테마 변수, ConfigProvider 설정을 덮어씀 */
themeVars?: Record<string, string>
/** 컴포넌트 수준 배경 문양, ConfigProvider 설정을 덮어씀 */
bgPattern?: string
/** 빠른 레이아웃 속성(트리거 루트 요소에 적용) */
sx?: LhSxProps
}LhPopoverPlacement (출처: @lh-ui/share/core/shared/popper)
ts
/**
* Popper 위치 엔진(순수 함수, 프레임워크 의존 없음)
*
* 팝업(Tooltip / Popover / Popconfirm / Dropdown 등)이 공통으로 사용하는
* 기하학 계산: 12가지 placement 배치, 주축 오버플로우 뒤집기(flip),
* 교차축 끼워 넣기(shift). 매개변수는 모두 rect 리터럴이며 DOM을
* 건드리지 않으므로 직접 단위 테스트할 수 있습니다.
*/
/** 팝업 방위(방향 + 정렬) */
export type LhPlacement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
| 'right'
| 'right-start'
| 'right-end'LhPopoverTrigger (출처: @lh-ui/share/core/popover (logic.ts))
ts
/** 트리거 방식 */
export type LhPopoverTrigger = 'click' | 'hover' | 'focus' | 'manual'