트리거를 눌러 목록에서 값을 선택하는 컴포넌트입니다.
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일을 선택하세요" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
npx @seed-design/cli@latest add ui:select pnpm dlx @seed-design/cli@latest add ui:select yarn dlx @seed-design/cli@latest add ui:select bun x @seed-design/cli@latest add ui:select
의존성 설치
npm install @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react yarn add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react pnpm add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react bun add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react 아래 코드를 복사 후 붙여넣고 사용하세요 /**
* @file ui:select
* @requires @seed-design/lynx-react@^1.0.0
* @requires @seed-design/lynx-css@^1.0.0
* @requires @karrotmarket/lynx-monochrome-icon@>=1.20.0 <2.0.0
**/
import IconCheckmarkFatFill from "@karrotmarket/lynx-monochrome-icon/IconCheckmarkFatFill" ;
import IconChevronDownSmallLine from "@karrotmarket/lynx-monochrome-icon/IconChevronDownSmallLine" ;
import * as React from "@lynx-js/react" ;
import { Field as SeedField, Select as SeedSelect, mergeProps } from "@seed-design/lynx-react" ;
import type { LynxAccessibilityProps, LynxIconElementProps } from "@seed-design/lynx-react" ;
type FieldRootRef = React . ComponentRef < typeof SeedField.Root>;
const SelectAccessibilityLabelContext =
React. createContext < LynxAccessibilityProps [ "accessibility-label" ]>( undefined );
export interface SelectRootProps
extends SeedSelect . RootProps ,
Pick < SeedField . RootProps , "required" | "invalid" | "readOnly" > {
label ?: React . ReactNode ;
labelWeight ?: SeedField . LabelProps [ "weight" ];
indicator ?: React . ReactNode ;
showRequiredIndicator ?: boolean ;
description ?: React . ReactNode ;
errorMessage ?: React . ReactNode ;
fieldRef ?: React . Ref < FieldRootRef >;
"accessibility-label" ?: LynxAccessibilityProps [ "accessibility-label" ];
}
/**
* Field와 Select의 값·열림 상태를 함께 조합합니다. label이 문자열이 아니면
* `accessibility-label`을 `SelectRoot` 또는 `SelectTrigger`에 지정하세요.
*
* @see https://seed-design.io/lynx/components/select
*/
export const SelectRoot = React. forwardRef < FieldRootRef , SelectRootProps >(
(
{
children,
label,
labelWeight,
indicator,
showRequiredIndicator,
description,
errorMessage,
fieldRef,
"accessibility-label" : accessibilityLabel,
value,
defaultValue,
onValueChange,
multiple,
open,
defaultOpen,
onOpenChange,
disabled,
required,
invalid,
readOnly,
size,
placement,
gutter,
overflowPadding,
formatValue,
... fieldProps
},
ref,
) => {
const mergedRefProps = React. useMemo (
() => mergeProps ({ ref }, { ref: fieldRef }),
[ref, fieldRef],
);
const renderHeader = label != null || indicator != null ;
const renderErrorMessage = invalid && errorMessage != null ;
const renderDescription = description != null && ! renderErrorMessage;
const renderFooter = renderDescription || renderErrorMessage;
const defaultAccessibilityLabel = typeof label === "string" ? label : undefined ;
const resolvedAccessibilityLabel = accessibilityLabel ?? defaultAccessibilityLabel;
return (
< SeedField.Root
{ ... mergedRefProps}
required = {required}
disabled = {disabled}
invalid = {invalid}
readOnly = {readOnly}
{ ... fieldProps}
>
{renderHeader ? (
< SeedField.Header >
< SeedField.Label weight = {labelWeight}>
{label}
{showRequiredIndicator ? < SeedField.RequiredIndicator /> : null }
{indicator != null ? (
< SeedField.IndicatorText >{indicator}</ SeedField.IndicatorText >
) : null }
</ SeedField.Label >
</ SeedField.Header >
) : null }
< SeedSelect.Root
value = {value}
defaultValue = {defaultValue}
onValueChange = {onValueChange}
multiple = {multiple}
open = {open}
defaultOpen = {defaultOpen}
onOpenChange = {onOpenChange}
disabled = {disabled}
required = {required}
invalid = {invalid}
readOnly = {readOnly}
size = {size}
placement = {placement}
gutter = {gutter}
overflowPadding = {overflowPadding}
formatValue = {formatValue}
>
< SelectAccessibilityLabelContext.Provider value = {resolvedAccessibilityLabel}>
{children}
</ SelectAccessibilityLabelContext.Provider >
</ SeedSelect.Root >
{renderFooter ? (
< SeedField.Footer >
{renderDescription ? (
< SeedField.Description >{description}</ SeedField.Description >
) : null }
{renderErrorMessage ? (
< SeedField.ErrorMessage >{errorMessage}</ SeedField.ErrorMessage >
) : null }
</ SeedField.Footer >
) : null }
</ SeedField.Root >
);
},
);
SelectRoot.displayName = "SelectRoot" ;
export interface SelectTriggerProps extends Omit < SeedSelect . TriggerProps , "children" > {
placeholder ?: React . ReactNode ;
prefixIcon ?: React . ReactElement < LynxIconElementProps >;
suffixIcon ?: React . ReactElement < LynxIconElementProps >;
}
/**
* 선택 값 또는 placeholder를 표시하고 탭으로 목록을 열고 닫습니다.
*
* @see https://seed-design.io/lynx/components/select
*/
export const SelectTrigger = React. forwardRef < unknown , SelectTriggerProps >(
(
{
placeholder,
prefixIcon,
suffixIcon = < IconChevronDownSmallLine />,
"accessibility-label" : accessibilityLabel,
... props
},
ref,
) => {
const rootAccessibilityLabel = React. useContext (SelectAccessibilityLabelContext);
return (
< SeedSelect.Trigger
ref = {ref}
accessibility-label = {accessibilityLabel ?? rootAccessibilityLabel}
{ ... props}
>
< SeedSelect.PrefixIcon fallback = {prefixIcon} />
< SeedSelect.Value />
{placeholder != null ? (
< SeedSelect.Placeholder >{placeholder}</ SeedSelect.Placeholder >
) : null }
< SeedSelect.SuffixIcon icon = {suffixIcon} />
</ SeedSelect.Trigger >
);
},
);
SelectTrigger.displayName = "SelectTrigger" ;
export interface SelectContentProps extends SeedSelect . ContentProps {
/**
* 지정하면 Lynx view 밖까지 덮는 native overlay에 렌더링합니다. 생략하면 Lynx view 안의 고정 native
* `view`로 렌더링합니다.
*/
container ?: SeedSelect . PositionerProps [ "container" ];
overlayLevel ?: SeedSelect . PositionerProps [ "overlayLevel" ];
}
/**
* 목록 레이어(Positioner), 위치를 계산하는 표면(Content)과 긴 목록의 스크롤 영역(ScrollArea)을
* 조립합니다. 자식에 별도 Positioner나 ScrollArea를 추가하지 마세요.
*
* @see https://seed-design.io/lynx/components/select
*/
export const SelectContent = React. forwardRef < unknown , SelectContentProps >(
({ children , container , overlayLevel , ... otherProps }, ref ) => {
return (
< SeedSelect.Positioner container = {container} overlayLevel = {overlayLevel}>
< SeedSelect.Content ref = {ref} { ... otherProps}>
< SeedSelect.ScrollArea >{children}</ SeedSelect.ScrollArea >
</ SeedSelect.Content >
</ SeedSelect.Positioner >
);
},
);
SelectContent.displayName = "SelectContent" ;
export interface SelectGroupProps extends SeedSelect . GroupProps {
label ?: React . ReactNode ;
}
export const SelectGroup = React. forwardRef < unknown , SelectGroupProps >(
({ label , children , ... props }, ref ) => {
return (
< SeedSelect.Group ref = {ref} { ... props}>
{label != null ? < SeedSelect.GroupLabel >{label}</ SeedSelect.GroupLabel > : null }
{children}
</ SeedSelect.Group >
);
},
);
SelectGroup.displayName = "SelectGroup" ;
export interface SelectItemProps extends Omit < SeedSelect . ItemProps , "children" > {
label : React . ReactNode ;
description ?: React . ReactNode ;
selectedIndicator ?: React . ReactNode ;
prefixIcon ?: React . ReactElement < LynxIconElementProps >;
}
/**
* item body, 설명, prefix icon과 선택 indicator를 조합합니다. `selectedIndicator`로
* 기본 checkmark를 바꿀 수 있습니다.
*
* @see https://seed-design.io/lynx/components/select
*/
export const SelectItem = React. forwardRef < unknown , SelectItemProps >(
(
{
label,
description,
selectedIndicator = < IconCheckmarkFatFill />,
prefixIcon,
"accessibility-label" : accessibilityLabel,
... props
},
ref,
) => {
return (
< SeedSelect.Item
ref = {ref}
accessibility-label = {accessibilityLabel ?? ( typeof label === "string" ? label : undefined )}
prefixIcon = {prefixIcon}
label = {label}
{ ... props}
>
< SeedSelect.ItemPrefixIcon />
< SeedSelect.ItemBody >
< SeedSelect.ItemLabel >{label}</ SeedSelect.ItemLabel >
{description != null ? (
< SeedSelect.ItemDescription >{description}</ SeedSelect.ItemDescription >
) : null }
</ SeedSelect.ItemBody >
< SeedSelect.ItemIndicator selected = {selectedIndicator} />
</ SeedSelect.Item >
);
},
);
SelectItem.displayName = "SelectItem" ;
/**
* 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.
*/
SelectContent는 긴 목록을 스크롤합니다. container를 지정하지 않으면 Lynx view 안에, 지정하면 native overlay에 렌더링됩니다.
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export function App () {
return (
< view style = {{ width: "240px" }}>
< SelectRoot defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일을 선택하세요" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ view >
);
}
multiple일 때는 항목을 탭해도 목록을 닫지 않고 선택 상태를 전환합니다.
SelectRoot의 문자열 label 또는 accessibility-label은 Trigger의 기본 접근성 이름으로 사용됩니다.
목록을 열면 선택된 항목이 보이도록 스크롤하며, Trigger 주변의 여유 공간과 safe area에 맞춰 높이를 제한합니다.
label?React.ReactNode
labelWeight?"medium" | "bold" | undefined
indicator?React.ReactNode
showRequiredIndicator?boolean | undefined
description?React.ReactNode
errorMessage?React.ReactNode
fieldRef?React.Ref < NodesRef > | undefined
size?"medium" | "large" | "responsive" | undefined
value?string[] | undefined
defaultValue?string[] | undefined
open?boolean | undefined
defaultOpen?boolean | undefined
placeholder?React.ReactNode
prefixIcon?React.ReactElement < LynxIconElementProps, string | React.JSXElementConstructor < any >> | undefined
suffixIcon?React.ReactElement < LynxIconElementProps, string | React.JSXElementConstructor < any >> | undefined
container?string | undefined
overlayLevel?1 | 4 | 2 | 3 | undefined
labelReact.ReactNode
description?React.ReactNode
selectedIndicator?React.ReactNode
disabled?boolean | undefined
readOnly?boolean | undefined
valuestring
textValue?string | undefined
size로 trigger와 목록의 크기를 정합니다. 기본값은 large이고, responsive는 native runtime의 화면 폭이 1280 이상이면 medium, 그 외에는 large로 해석합니다.
import { Box, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< VStack width = "full" gap = "x4" >
< Box width = "full" >
< SelectRoot size = "large" defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일 (large)" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< Box width = "full" >
< SelectRoot size = "medium" defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일 (medium)" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
</ VStack >
);
}
SelectGroup으로 옵션을 묶고 label로 그룹 제목을 표시합니다. 두 번째 그룹부터는 그룹 사이에 구분선이 추가됩니다.
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot defaultValue = {[ "seoul" ]}>
< SelectTrigger accessibility-label = "지역" placeholder = "지역 선택" />
< SelectContent >
< SelectGroup label = "아시아" >
< SelectItem value = "seoul" label = "서울" />
< SelectItem value = "tokyo" label = "도쿄" />
< SelectItem value = "singapore" label = "싱가포르" />
< SelectItem value = "dubai" label = "두바이" />
</ SelectGroup >
< SelectGroup label = "유럽" >
< SelectItem value = "london" label = "런던" />
< SelectItem value = "paris" label = "파리" />
< SelectItem value = "berlin" label = "베를린" />
</ SelectGroup >
< SelectGroup label = "아메리카" >
< SelectItem value = "new-york" label = "뉴욕" />
< SelectItem value = "sao-paulo" label = "상파울루" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
multiple을 지정하면 선택한 옵션을 다시 탭해 해제할 수 있습니다. 다중 선택 중에는 목록이 닫히지 않으며 trigger에는 기본적으로 선택된 textValue가 ", "로 연결되어 표시됩니다.
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot multiple defaultValue = {[ "apple" , "cherry" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
< SelectItem value = "grape" label = "포도" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
SelectItem의 description으로 옵션에 부가 설명을 추가합니다.
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot defaultValue = {[ "standard" ]}>
< SelectTrigger accessibility-label = "배송 방법" placeholder = "배송 방법 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "standard" label = "일반 배송" description = "3-5일 소요" />
< SelectItem value = "express" label = "빠른 배송" description = "1-2일 소요" />
< SelectItem value = "same-day" label = "당일 배송" description = "오늘 도착" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
SelectItem의 prefixIcon으로 옵션 앞에 아이콘을 표시합니다. 단일 옵션이 선택되면 해당 아이콘이 trigger prefix에 표시됩니다. 선택이 없거나 여러 옵션이 선택된 경우에는 SelectTrigger의 prefixIcon fallback을 사용합니다.
import IconGlobeLine from "@karrotmarket/lynx-monochrome-icon/IconGlobeLine" ;
import IconLockLine from "@karrotmarket/lynx-monochrome-icon/IconLockLine" ;
import IconPerson2Line from "@karrotmarket/lynx-monochrome-icon/IconPerson2Line" ;
import IconPersonLine from "@karrotmarket/lynx-monochrome-icon/IconPersonLine" ;
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot >
< SelectTrigger
accessibility-label = "공유 대상"
placeholder = "공유 대상"
prefixIcon = {< IconPerson2Line />}
/>
< SelectContent >
< SelectGroup label = "그룹" >
< SelectItem value = "public" label = "전체 공개" prefixIcon = {< IconGlobeLine />} />
< SelectItem value = "followers" label = "팔로워만" prefixIcon = {< IconLockLine />} />
< SelectItem value = "private" label = "나만" prefixIcon = {< IconPersonLine />} />
</ SelectGroup >
< SelectGroup label = "사람" >
< SelectItem value = "kim" label = "김하늘" />
< SelectItem value = "lee" label = "이하늘" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
label에는 복합 JSX를 전달할 수 있습니다. 복합 label을 사용할 때는 trigger에 표시할 문자열과 옵션 접근성 이름을 위해 textValue를 함께 지정하세요.
import IconCarLine from "@karrotmarket/lynx-monochrome-icon/IconCarLine" ;
import IconFigureBikeLine from "@karrotmarket/lynx-monochrome-icon/IconFigureBikeLine" ;
import IconMetroFrontsideLine from "@karrotmarket/lynx-monochrome-icon/IconMetroFrontsideLine" ;
import { Box, HStack } from "@seed-design/lynx-react" ;
import { Badge } from "@/components/ui/badge" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot defaultValue = {[ "metro" ]}>
< SelectTrigger accessibility-label = "이동 수단" placeholder = "이동 수단 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "bike" label = "자전거" prefixIcon = {< IconFigureBikeLine />} />
< SelectItem
value = "metro"
textValue = "지하철"
prefixIcon = {< IconMetroFrontsideLine />}
label = {
< HStack align = "center" gap = "x1_5" >
< text >지하철</ text >
< Badge variant = "weak" tone = "informative" >
가장 빠름
</ Badge >
</ HStack >
}
/>
< SelectItem
value = "car"
textValue = "자동차"
disabled
prefixIcon = {< IconCarLine />}
label = {
< HStack align = "center" gap = "x1_5" >
< text >자동차</ text >
< Badge variant = "weak" tone = "warning" >
고객지원에 문의
</ Badge >
</ HStack >
}
/>
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
SelectItem의 disabled는 특정 옵션을, SelectRoot의 disabled는 Select 전체를 비활성화합니다. readOnly도 목록을 열거나 선택을 바꾸지 않습니다.
import { Box, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< VStack width = "full" gap = "x4" >
< Box width = "full" >
< SelectRoot defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" disabled />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< Box width = "full" >
< SelectRoot disabled defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "비활성 과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
</ VStack >
);
}
목록은 상하 8px 패딩을 포함한 내용 높이만큼 표시하며, 별도의 최소 높이는 없습니다. 최대 높이는 480px이고 trigger 주위의 남은 공간과 safe area에 맞춰 제한합니다. 내용이 이 높이를 넘을 때만 스크롤됩니다.
열릴 때 선택된 옵션이 화면 밖에 있으면 보이는 데 필요한 만큼만 스크롤합니다. 이미 보이는 옵션을 선택하거나 목록이 닫히는 중에는 스크롤 위치를 바꾸지 않습니다.
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
const timeSlots = Array. from ({ length: 48 }, ( _ , index ) => {
const hour = String (Math. floor (index / 2 )). padStart ( 2 , "0" );
return `${ hour }:${ index % 2 === 0 ? "00" : "30"}` ;
});
export default function Example () {
return (
< Box width = "full" >
< SelectRoot defaultValue = {[ "14:00" ]}>
< SelectTrigger accessibility-label = "예약 시간" placeholder = "시간 선택" />
< SelectContent >
< SelectGroup >
{timeSlots. map (( slot ) => (
< SelectItem key = {slot} value = {slot} label = {slot} />
))}
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
placement로 목록 위치를 정합니다. 기본값은 bottom이며, 공간이 부족하면 native positioner가 반대쪽으로 뒤집거나 경계 안으로 이동합니다.
import { Box } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< Box width = "full" >
< SelectRoot placement = "top" defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
);
}
value와 onValueChange로 선택 값을 제어할 수 있습니다.
import { useState } from "@lynx-js/react" ;
import { Box, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
const [ value , setValue ] = useState < string []>([ "apple" ]);
return (
< VStack width = "full" gap = "x2" >
< Box width = "full" >
< SelectRoot value = {value} onValueChange = {setValue}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< text className = "select-preview__status" >
선택된 값: {value. length > 0 ? value. join ( ", " ) : "없음" }
</ text >
</ VStack >
);
}
open과 onOpenChange로 목록 상태를 제어합니다. 초기 상태만 지정하려면 defaultOpen을 사용하세요.
import { useState } from "@lynx-js/react" ;
import { ActionButton, Box, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
const [ open , setOpen ] = useState ( false );
return (
< VStack width = "full" gap = "x2" >
< Box width = "full" >
< SelectRoot open = {open} onOpenChange = {setOpen} defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< ActionButton variant = "neutralWeak" disabled = {open} bindtap = {() => setOpen ( true )}>
목록 열기
</ ActionButton >
< text className = "select-preview__status" >목록 상태: {open ? "열림" : "닫힘" }</ text >
</ VStack >
);
}
container를 지정하지 않으면 Select 레이어는 Lynx view 안의 position: fixed view로 렌더링되고, z-index 99로 같은 화면의 형제 요소 위에 그려집니다. container="window"처럼 native container 이름을 지정하면 native overlay에 렌더링되어 Lynx view 밖의 host 화면까지 덮습니다. overlayLevel은 container를 지정했을 때 overlay의 표시 순서를 정합니다.
두 방식 모두 레이어 바깥을 탭하면 아래 화면에 탭을 전달하지 않고 "interactOutside" reason으로 닫힙니다. 닫힌 동안에도 레이어를 숨긴 채 유지하므로 열기 전에도 defaultValue의 label과 아이콘을 trigger에 표시합니다.
import { useState } from "@lynx-js/react" ;
import { ActionButton, Box, Text, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
const [ closeReason , setCloseReason ] = useState < string | null >( null );
const [ count , setCount ] = useState ( 0 );
function handleOpenChange ( open : boolean , details : { reason : string }) {
"background only" ;
if ( ! open) setCloseReason (details.reason);
}
function handleUnderlyingTap () {
"background only" ;
setCount (( current ) => current + 1 );
}
return (
< VStack width = "full" gap = "x4" >
< Box width = "full" >
< SelectRoot defaultValue = {[ "apple" ]} onOpenChange = {handleOpenChange}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일을 선택하세요" />
< SelectContent container = "window" >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< ActionButton variant = "neutralWeak" bindtap = {handleUnderlyingTap}>
{ `아래 버튼 ${ count }` }
</ ActionButton >
< Text textStyle = "t3Regular" color = "fg.neutralMuted" >
{ `마지막 닫힘 이유: ${ closeReason ?? "-"}` }
</ Text >
</ VStack >
);
}
onOpenChange의 두 번째 인자는 상태를 바꾼 이유와 native event를 제공합니다.
열림: "trigger"
닫힘: "trigger", "itemSelect", "interactOutside"
container를 지정한 native overlay가 닫힘을 요청한 경우(Android 뒤로 가기): "dismiss". container가 없으면 전달하지 않으며, Android 뒤로 가기는 Select가 아니라 host 화면을 닫습니다.
import { useState } from "@lynx-js/react" ;
import { Box, HStack, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
const [ open , setOpen ] = useState ( false );
const [ openReason , setOpenReason ] = useState < string | null >( null );
const [ closeReason , setCloseReason ] = useState < string | null >( null );
function handleOpenChange ( nextOpen : boolean , details : { reason : string }) {
"background only" ;
setOpen (nextOpen);
(nextOpen ? setOpenReason : setCloseReason)(details.reason);
}
return (
< VStack width = "full" gap = "x4" >
< Box width = "full" >
< SelectRoot open = {open} onOpenChange = {handleOpenChange} defaultValue = {[ "apple" ]}>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< HStack width = "full" gap = "x4" style = {{ display: "flex" , flexWrap: "wrap" }}>
< text className = "select-preview__status" >마지막 열림 이유: {openReason ?? "-" }</ text >
< text className = "select-preview__status" >마지막 닫힘 이유: {closeReason ?? "-" }</ text >
</ HStack >
</ VStack >
);
}
formatValue로 trigger에 표시할 선택 값을 바꿉니다. callback은 value, label, textValue, icon, resolved를 담은 선택 item 배열을 받습니다. icon은 SelectItem의 prefixIcon이고, 목록에 없는 값은 resolved: false로 전달합니다.
import { Box, HStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< HStack width = "full" gap = "x4" >
< Box display = "flex" flexDirection = "column" flexGrow flexShrink style = {{ flexBasis: 0 }}>
< SelectRoot
multiple
defaultValue = {[ "apple" , "banana" ]}
formatValue = {( items ) => items. map (( item ) => item.textValue). join ( ", " )}
>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< Box display = "flex" flexDirection = "column" flexGrow flexShrink style = {{ flexBasis: 0 }}>
< SelectRoot
multiple
defaultValue = {[ "apple" , "banana" , "cherry" ]}
formatValue = {([ first , ... rest ]) =>
rest. length > 0 ? `${ first ?. textValue ?? ""} 외 ${ rest . length }개` : first?.textValue
}
>
< SelectTrigger accessibility-label = "과일" placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
</ HStack >
);
}
SelectRoot는 기존 Lynx Field를 조합하므로 label, description, errorMessage, invalid, required와 showRequiredIndicator를 지원합니다. 오류 메시지가 있으면 description 대신 오류 메시지를 표시합니다.
import { Box, VStack } from "@seed-design/lynx-react" ;
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "@/components/ui/select" ;
export default function Example () {
return (
< VStack width = "full" gap = "x6" >
< Box width = "full" >
< SelectRoot
label = "과일"
description = "가장 좋아하는 과일을 선택하세요."
defaultValue = {[ "apple" ]}
>
< SelectTrigger placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
< Box width = "full" >
< SelectRoot label = "과일" labelWeight = "bold" invalid errorMessage = "과일을 선택해주세요." >
< SelectTrigger placeholder = "과일 선택" />
< SelectContent >
< SelectGroup >
< SelectItem value = "apple" label = "사과" />
< SelectItem value = "banana" label = "바나나" />
< SelectItem value = "cherry" label = "체리" />
</ SelectGroup >
</ SelectContent >
</ SelectRoot >
</ Box >
</ VStack >
);
}
Lynx에는 HTML <form>과 native <select> 제출 모델이 없습니다. 선택 값은 onValueChange로 앱 state에 저장하고, 제출 action의 payload에 그 값을 넣으세요.
React Hook Form 통합은 제공하지 않습니다. value, onValueChange, invalid, errorMessage를 앱 상태에 연결하세요.
Trigger는 native 요소이며 asChild를 제공하지 않습니다.
onOpenChange의 reason은 trigger, itemSelect, interactOutside, dismiss입니다.
키보드 focus, typeahead, Escape 키 닫기는 지원하지 않습니다.
container를 지정하면 목록을 native overlay에 렌더링합니다.
HTML form 제출과 브라우저 validation은 지원하지 않습니다. onValueChange로 앱 상태를 갱신하고 제출 action에서 검증하세요.
React Hook Form 통합은 제공하지 않습니다. 값과 오류 상태를 앱 상태에 직접 연결하세요.
DOM focus, typeahead, ARIA id 연결은 지원하지 않습니다. native 접근성 속성과 host의 접근성 탐색을 사용하세요.