트리거 요소에 앵커링되어 화면 위에 떠 있는 컨테이너입니다. Header/Body/Footer 구조로 부가 정보나 액션을 제공할 때 사용됩니다.
import { HStack } from "@seed-design/react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
export default function PopoverPreview () {
return (
< PopoverRoot >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >Open Popover</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "제목" description = "설명을 작성할 수 있어요" >
< PopoverBody >
Popover 본문에는 사용자가 확인해야 할 내용이나 추가 액션을 배치할 수 있습니다.
</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
);
}
npx @seed-design/cli@latest add ui:popover pnpm dlx @seed-design/cli@latest add ui:popover yarn dlx @seed-design/cli@latest add ui:popover bun x @seed-design/cli@latest add ui:popover
의존성 설치
npm install @karrotmarket/react-monochrome-icon @seed-design/react yarn add @karrotmarket/react-monochrome-icon @seed-design/react pnpm add @karrotmarket/react-monochrome-icon @seed-design/react bun add @karrotmarket/react-monochrome-icon @seed-design/react 아래 코드를 복사 후 붙여넣고 사용하세요 /**
* @file ui:popover
* @requires @seed-design/react@^3.0.0
**/
"use client" ;
import IconXmarkLine from "@karrotmarket/react-monochrome-icon/IconXmarkLine" ;
import { Icon, Popover as SeedPopover } from "@seed-design/react" ;
import type * as React from "react" ;
import { forwardRef } from "react" ;
import { ActionButton, type ActionButtonProps } from "./action-button" ;
export interface PopoverRootProps extends SeedPopover . RootProps {}
/**
* @see https://seed-design.io/react/components/popover
*/
export const PopoverRoot = SeedPopover.Root;
export interface PopoverTriggerProps extends SeedPopover . TriggerProps {}
export const PopoverTrigger = SeedPopover.Trigger;
export interface PopoverAnchorProps extends SeedPopover . AnchorProps {}
export const PopoverAnchor = SeedPopover.Anchor;
export interface PopoverContentProps extends Omit < SeedPopover . ContentProps , "title" | "asChild" > {
title ?: React . ReactNode ;
description ?: React . ReactNode ;
/**
* @default true
*/
showCloseButton ?: boolean ;
positionerContainer ?: SeedPopover . PositionerProps [ "container" ];
}
export const PopoverContent = forwardRef < HTMLDivElement , PopoverContentProps >(
(
{ children, title, description, showCloseButton = true , positionerContainer, ... otherProps },
ref,
) => {
if (
! title &&
! otherProps[ "aria-labelledby" ] &&
! otherProps[ "aria-label" ] &&
process.env. NODE_ENV !== "production"
) {
console. warn (
"PopoverContent: aria-labelledby or aria-label should be provided if title is not provided." ,
);
}
const shouldRenderHeader = title || description || showCloseButton;
return (
< SeedPopover.Positioner container = {positionerContainer}>
< SeedPopover.Content ref = {ref} { ... otherProps}>
{shouldRenderHeader && (
< SeedPopover.Header >
{title && < SeedPopover.Title >{title}</ SeedPopover.Title >}
{description && < SeedPopover.Description >{description}</ SeedPopover.Description >}
{showCloseButton && (
// You may implement your own i18n for dismiss label
< SeedPopover.CloseButton aria-label = "닫기" >
< Icon svg = {< IconXmarkLine />} />
</ SeedPopover.CloseButton >
)}
</ SeedPopover.Header >
)}
{children}
< SeedPopover.Arrow >
< SeedPopover.ArrowTip />
</ SeedPopover.Arrow >
</ SeedPopover.Content >
</ SeedPopover.Positioner >
);
},
);
PopoverContent.displayName = "PopoverContent" ;
export interface PopoverBodyProps extends SeedPopover . BodyProps {}
export const PopoverBody = SeedPopover.Body;
export interface PopoverFooterProps extends SeedPopover . FooterProps {}
export const PopoverFooter = SeedPopover.Footer;
export interface PopoverActionProps
extends Omit < SeedPopover . ActionProps , "color" >,
ActionButtonProps {}
export const PopoverAction = forwardRef < HTMLButtonElement , PopoverActionProps >(( props , ref ) => {
return (
< SeedPopover.Action asChild >
< ActionButton { ... props} ref = {ref} />
</ SeedPopover.Action >
);
});
PopoverAction.displayName = "PopoverAction" ;
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
title?React.ReactNode
description?React.ReactNode
positionerContainer?React.RefObject < HTMLElement | null > | undefined
width?ResponsiveValue < (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full" > | undefined
maxWidth?ResponsiveValue < (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full" > | undefined
asChild?boolean | undefined
paddingX?ResponsiveValue < 0 | (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" > | undefined
height?ResponsiveValue < (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full" > | undefined
maxHeight?ResponsiveValue < (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full" > | undefined
minHeight?ResponsiveValue < (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full" > | undefined
justifyContent?"flex-start" | "flex-end" | "center" | "space-between" | "space-around" | undefined
alignItems?"flex-start" | "flex-end" | "center" | "stretch" | undefined
<PopoverTrigger>는 aria-haspopup="dialog" 속성을 설정하고, Popover의 open 상태에 따라 aria-expanded 속성을 자동으로 설정합니다. 열려 있는 동안에는 <PopoverContent>를 가리키는 aria-controls 속성도 함께 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다.
import { HStack } from "@seed-design/react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
export default function PopoverTriggerExample () {
return (
< PopoverRoot >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >Trigger</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "제목" >
< PopoverBody >트리거를 눌러 Popover를 열 수 있습니다.</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
);
}
Trigger 외의 방식으로 Popover를 열고 닫을 수 있습니다. 이 경우 open prop을 사용하여 Popover의 상태를 제어합니다.
import { HStack, VStack } from "@seed-design/react" ;
import { useState } from "react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
import { Switch } from "seed-design/ui/switch" ;
export default function PopoverControlled () {
const [ open , setOpen ] = useState ( false );
return (
< VStack gap = "spacingY.componentDefault" align = "center" >
< Switch size = "24" tone = "neutral" label = "열림" checked = {open} onCheckedChange = {setOpen} />
< PopoverRoot open = {open} onOpenChange = {setOpen}>
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >Popover</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "제어 상태" >
< PopoverBody >open prop으로 Popover의 열림 상태를 직접 제어합니다.</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
</ VStack >
);
}
onOpenChange(open, details)에서 열림 상태가 바뀐 이유를 확인할 수 있습니다.
trigger: 트리거로 열거나 닫음
closeButton: PopoverAction 또는 닫기 버튼으로 닫음
escapeKeyDown: Escape 키로 닫음
interactOutside: 외부 영역을 눌러 닫음
focusOut: Tab 등으로 포커스가 밖으로 이동하여 닫음
cascadeDismiss: 상위 레이어가 닫혀 함께 닫음
cascadeDismiss는 details.dismissedParent를, 나머지는 details.event를 제공합니다. focusOut의 이벤트는 FocusEvent입니다.
Popover는 열릴 때 콘텐츠로 포커스를 옮기지만 가두지 않습니다. closeOnInteractOutside={false}가 아니라면 Tab 등으로 포커스가 밖으로 이동할 때 닫히며, 이동한 곳의 포커스를 유지합니다.
<PopoverRoot>에 placement prop을 설정하여 트리거 기준 위치를 지정합니다. 뷰포트 경계를 벗어나면 자동으로 뒤집히거나(flip) 이동합니다(shift).
import { HStack } from "@seed-design/react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
export default function PopoverPlacement () {
return (
< PopoverRoot placement = "right-start" >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >right-start</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "Placement" >
< PopoverBody >
placement prop으로 트리거 기준 위치를 지정합니다. 뷰포트를 벗어나면 자동으로 뒤집히거나
이동합니다.
</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
);
}
<PopoverAnchor>를 사용하면 트리거와 분리된 요소를 기준으로 Popover의 위치를 잡을 수 있습니다. 이 경우 열림 상태는 open prop으로 직접 제어합니다.
import { HStack } from "@seed-design/react" ;
import { useState } from "react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import { Avatar } from "seed-design/ui/avatar" ;
import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder" ;
import {
PopoverAction,
PopoverAnchor,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
} from "seed-design/ui/popover" ;
export default function PopoverAnchorExample () {
const [ open , setOpen ] = useState ( false );
return (
< HStack align = "center" justify = "space-between" width = "full" >
< ActionButton variant = "neutralSolid" onClick = {() => setOpen ( true )}>
Popover 열기
</ ActionButton >
< PopoverRoot open = {open} onOpenChange = {setOpen}>
< PopoverAnchor asChild >
< Avatar
size = "80"
src = "https://avatars.githubusercontent.com/u/54893898?v=4"
fallback = {< IdentityPlaceholder />}
/>
</ PopoverAnchor >
< PopoverContent title = "Anchor" >
< PopoverBody >
트리거와 분리된 요소를 기준으로 Popover의 위치를 잡을 수 있습니다.
</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
</ HStack >
);
}
<PopoverContent>의 max-height(600px)를 넘는 긴 콘텐츠는 <PopoverBody>가 스크롤됩니다. 콘텐츠가 실제로 넘칠 때에만 하단에 scroll fog가 표시되고, 위에 Header가 있는 Body를 아래로 스크롤하면 상단에 divider가 나타납니다. 뷰포트가 작으면 max-height는 가용 높이에 맞춰 줄어듭니다.
import { HStack } from "@seed-design/react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
export default function PopoverScroll () {
return (
< PopoverRoot >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >긴 콘텐츠 Popover</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "약관 동의" description = "아래 내용을 확인해주세요" >
< PopoverBody >
{Array. from ({ length: 20 }, ( _ , index ) => (
< p key = {index} style = {{ margin: 0 }}>
{index + 1 }. 본문이 길어지면 Body가 스크롤되고, 스크롤 시 상단 divider와 하단 scroll
fog가 나타납니다.
</ p >
))}
</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >동의</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
);
}
<PopoverContent>에 showCloseButton prop을 전달하여 닫기 버튼 표시 여부를 제어합니다. 기본값은 true이며, 표시될 때 Header 우측에 여백이 확보됩니다.
import { HStack } from "@seed-design/react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
export default function PopoverShowCloseButton () {
return (
< HStack gap = "x3" >
< PopoverRoot >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >닫기 버튼 있음</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "닫기 버튼" showCloseButton >
< PopoverBody >기본적으로 Header 우측에 닫기 버튼이 표시됩니다.</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
< PopoverRoot >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >닫기 버튼 없음</ ActionButton >
</ PopoverTrigger >
< PopoverContent title = "닫기 버튼 없음" showCloseButton = { false }>
< PopoverBody >
닫기 버튼을 숨길 때는 본문이나 푸터에 닫을 수 있는 액션을 제공하세요.
</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralSolid" >확인</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
</ HStack >
);
}
PopoverAction의 onClick에서 e.preventDefault()를 호출하면 Popover가 닫히지 않습니다.
import { HStack } from "@seed-design/react" ;
import { useState } from "react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
PopoverAction,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverRoot,
PopoverTrigger,
} from "seed-design/ui/popover" ;
import { Switch } from "seed-design/ui/switch" ;
export default function PopoverPreventClose () {
const [ preventClose , setPreventClose ] = useState ( true );
return (
< PopoverRoot >
< PopoverTrigger asChild >
< ActionButton variant = "neutralSolid" >열기</ ActionButton >
</ PopoverTrigger >
< PopoverContent
title = "닫기 방지"
description = "확인 버튼을 눌러도 Popover가 닫히지 않도록 설정할 수 있습니다."
>
< PopoverBody >
< Switch
size = "16"
tone = "neutral"
label = "preventDefault 사용"
checked = {preventClose}
onCheckedChange = {setPreventClose}
/>
</ PopoverBody >
< PopoverFooter >
< HStack gap = "x2" justify = "flex-end" >
< PopoverAction variant = "neutralWeak" >취소</ PopoverAction >
< PopoverAction
variant = "neutralSolid"
onClick = {( e ) => {
if (preventClose) e. preventDefault ();
}}
>
확인
</ PopoverAction >
</ HStack >
</ PopoverFooter >
</ PopoverContent >
</ PopoverRoot >
);
}