Skip to content
API 风格:

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화살표 표시 여부booleantrue
width팝업 너비string | number—
popperClass팝업 커스텀 클래스명string—
teleportTargetTeleport 대상(선택자 또는 요소)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'

국풍의 아름다움으로 코드의 토대를 세웁니다