한 줄 텍스트를 입력받고 Field의 레이블·설명·오류 상태와 조합하는 컴포넌트입니다.
@seed-design/lynx-react@0.4.0, @seed-design/lynx-css@0.7.0
npx @seed-design/cli add ui:text-field pnpm dlx @seed-design/cli add ui:text-field yarn dlx @seed-design/cli add ui:text-field bun x @seed-design/cli add ui:text-field
의존성 설치
npm install @seed-design/lynx-react yarn add @seed-design/lynx-react pnpm add @seed-design/lynx-react bun add @seed-design/lynx-react 아래 코드를 복사 후 붙여넣고 사용하세요 /**
* @file ui:text-field
* @requires @seed-design/lynx-react@>=0.1.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.1.0 <1.0.0
**/
import * as React from "@lynx-js/react" ;
import {
Field as SeedField,
TextField as SeedTextField,
type UseTextFieldWithGraphemesParams,
useTextFieldWithGraphemes,
} from "@seed-design/lynx-react" ;
type TextFieldRootRef = React . ComponentRef < typeof SeedTextField.Root>;
type FieldRootRef = React . ComponentRef < typeof SeedField.Root>;
export interface TextFieldProps
extends Omit < SeedTextField . RootProps , "children" | "onValueChange" > {
children ?: React . ReactNode ;
label ?: React . ReactNode ;
labelWeight ?: SeedField . LabelProps [ "weight" ];
indicator ?: React . ReactNode ;
prefixIcon ?: SeedTextField . PrefixIconProps [ "icon" ];
prefix ?: React . ReactNode ;
suffixIcon ?: SeedTextField . SuffixIconProps [ "icon" ];
suffix ?: React . ReactNode ;
description ?: React . ReactNode ;
errorMessage ?: React . ReactNode ;
hideCharacterCount ?: boolean ;
maxGraphemeCount ?: number ;
showRequiredIndicator ?: boolean ;
fieldRef ?: React . Ref < FieldRootRef >;
onValueChange ?: UseTextFieldWithGraphemesParams [ "onValueChange" ];
}
/**
* @see https://seed-design.io/lynx/components/text-field-input
*/
export const TextField = React. forwardRef < TextFieldRootRef , TextFieldProps >(
(
{
children,
label,
labelWeight,
indicator,
prefixIcon,
prefix,
suffixIcon,
suffix,
description,
errorMessage,
hideCharacterCount,
maxGraphemeCount,
showRequiredIndicator,
fieldRef,
value,
defaultValue,
onValueChange,
required,
disabled,
invalid,
readOnly,
name,
... rootProps
},
ref,
) => {
const { textFieldRootProps , counterProps } = useTextFieldWithGraphemes ({
value,
defaultValue,
onValueChange,
maxGraphemeCount,
});
const renderHeader = label != null || indicator != null ;
const renderDescription = description != null && ! (invalid && errorMessage != null );
const renderErrorMessage = invalid && errorMessage != null ;
const renderCharacterCount = ! hideCharacterCount && maxGraphemeCount !== undefined ;
const renderFooter = renderDescription || renderErrorMessage || renderCharacterCount;
return (
< SeedField.Root
ref = {fieldRef}
required = {required}
disabled = {disabled}
invalid = {invalid}
readOnly = {readOnly}
>
{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 }
< SeedTextField.Root ref = {ref} name = {name} { ... rootProps} { ... textFieldRootProps}>
{prefixIcon ? < SeedTextField.PrefixIcon icon = {prefixIcon} /> : null }
{prefix != null ? < SeedTextField.PrefixText >{prefix}</ SeedTextField.PrefixText > : null }
{children}
{suffix != null ? < SeedTextField.SuffixText >{suffix}</ SeedTextField.SuffixText > : null }
{suffixIcon ? < SeedTextField.SuffixIcon icon = {suffixIcon} /> : null }
</ SeedTextField.Root >
{renderFooter ? (
< SeedField.Footer >
{renderDescription ? (
< SeedField.Description >{description}</ SeedField.Description >
) : null }
{renderErrorMessage ? (
< SeedField.ErrorMessage >{errorMessage}</ SeedField.ErrorMessage >
) : null }
{renderCharacterCount ? < SeedField.CharacterCount { ... counterProps} /> : null }
</ SeedField.Footer >
) : null }
</ SeedField.Root >
);
},
);
TextField.displayName = "TextField" ;
export interface TextFieldInputProps extends SeedTextField . InputProps {}
/**
* @see https://seed-design.io/lynx/components/text-field-input
*/
export const TextFieldInput = SeedTextField.Input;
export interface TextFieldTextareaProps extends SeedTextField . TextareaProps {}
/**
* @see https://seed-design.io/lynx/components/text-field-textarea
*/
export const TextFieldTextarea = SeedTextField.Textarea;
/**
* 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.
*/
children?React.ReactNode
label?React.ReactNode
labelWeight?"medium" | "bold" | undefined
indicator?React.ReactNode
prefixIcon?React.ReactElement < LynxIconElementProps, string | React.JSXElementConstructor < any >> | undefined
prefix?React.ReactNode
suffixIcon?React.ReactElement < LynxIconElementProps, string | React.JSXElementConstructor < any >> | undefined
suffix?React.ReactNode
description?React.ReactNode
errorMessage?React.ReactNode
hideCharacterCount?boolean | undefined
maxGraphemeCount?number | undefined
showRequiredIndicator?boolean | undefined
fieldRef?React.Ref < NodesRef > | undefined
onValueChange?(( values : { value : string ; graphemes : string []; slicedValue : string ; slicedGraphemes : string []; }) => void ) | undefined
variant?"outline" | "underline" | undefined
size?"medium" | "large" | undefined
invalid?boolean | undefined
readOnly?boolean | undefined
disabled?boolean | undefined
value?string | undefined
defaultValue?string | undefined
required?boolean | undefined
name?string | undefined
style?CSSProperties | undefined
className?string | undefined
placeholder?string | undefined
confirm-type?"send" | "search" | "go" | "done" | "next" | undefined
maxlength?number | undefined
readonly?boolean | undefined
disabled?boolean | undefined
show-soft-input-on-focus?boolean | undefined
input-filter?string | undefined
type?"number" | "text" | "digit" | "password" | "tel" | "email" | undefined
ios-auto-correct?boolean | undefined
ios-spell-check?boolean | undefined
android-fullscreen-mode?boolean | undefined
bindfocus?(( e : BaseEvent < "bindfocus" , InputFocusEvent >) => void ) | undefined
bindblur?(( e : BaseEvent < "bindblur" , InputBlurEvent >) => void ) | undefined
bindconfirm?(( e : BaseEvent < "bindconfirm" , InputConfirmEvent >) => void ) | undefined
bindinput?(( e : BaseEvent < "bindinput" , InputInputEvent >) => void ) | undefined
bindselection?(( e : BaseEvent < "bindselection" , InputSelectionEvent >) => void ) | undefined
id?string | undefined
name?string | undefined
hidden?boolean | undefined
flatten?boolean | undefined
focusable?boolean | undefined
bindlayoutchange?EventHandler < LayoutChangeDetailEvent < Target >> | undefined
main-thread:bindlayoutchange?EventHandler < LayoutChangeDetailEvent < Element >> | undefined
style?CSSProperties | undefined
className?string | undefined
Snippet의 TextField가 label, description, required indicator와 한 줄 입력 영역을 조합합니다. Lynx에는 HTML label for 연결이 없으므로 native 입력에는 accessibility-label을 함께 지정합니다.
import { TextField, TextFieldInput } from "@/components/ui/text-field" ;
export function TitleField () {
return (
< TextField label = "제목" required showRequiredIndicator name = "title" >
< TextFieldInput accessibility-label = "제목" placeholder = "제목을 입력해 주세요" />
</ TextField >
);
}
value와 onValueChange를 전달하면 controlled mode로 동작합니다. Lynx native 입력에는 value attribute가 없으므로 컴포넌트가 setValue UI method로 값을 동기화합니다.
import { useState } from "@lynx-js/react" ;
import { TextField, TextFieldInput } from "@/components/ui/text-field" ;
export function ControlledTitle () {
const [ value , setValue ] = useState ( "" );
return (
< TextField value = {value} onValueChange = {({ value }) => setValue (value)}>
< TextFieldInput accessibility-label = "제목" />
</ TextField >
);
}
이모지·조합 문자를 사용자에게 보이는 한 글자로 계산하려면 snippet의 maxGraphemeCount와 onValueChange를 사용합니다.
import { useState } from "@lynx-js/react" ;
import { TextField, TextFieldInput } from "@/components/ui/text-field" ;
export function LimitedTitleField () {
const [ value , setValue ] = useState ( "" );
return (
< TextField
label = "제목"
value = {value}
maxGraphemeCount = { 40 }
onValueChange = {({ slicedValue }) => setValue (slicedValue)}
>
< TextFieldInput accessibility-label = "제목" />
</ TextField >
);
}
KeyboardAvoidingScrollView 안에 배치하면 TextFieldInput이 focus될 때 자동으로 등록되어 키보드에 가려지지 않는 위치로 스크롤됩니다.
import { KeyboardAvoidingScrollView } from "@seed-design/lynx-react" ;
import { TextField, TextFieldInput } from "@/components/ui/text-field" ;
export function Form () {
return (
< KeyboardAvoidingScrollView >
< TextField label = "제목" >
< TextFieldInput accessibility-label = "제목" />
</ TextField >
</ KeyboardAvoidingScrollView >
);
}
편집 가능한 상태에서는 HTML <input> 대신 Lynx native <input> element를 렌더링합니다.
readOnly 상태에서는 native focus·selection·잘라내기 메뉴를 제거하기 위해 <text> element로 렌더링합니다. 이 상태의 ref는 <text>를 가리키며 input 전용 UI method와 이벤트는 사용할 수 없습니다.
onChange 대신 snippet TextField의 onValueChange를 사용합니다. 원문과 grapheme 단위로 자른 값을 함께 제공합니다.
native bindinput은 TextFieldInput에 추가로 전달할 수 있습니다.
Field.Label과 입력의 DOM id 연결이 없으므로 accessibility-label을 입력에 직접 제공합니다.
KeyboardAvoidingScrollView가 키보드 회피를 전담하는 화면에서는 Android host window의 별도 pan/resize를 막기 위해 android-set-soft-input-mode="nothing"을 명시할 수 있습니다. 이 값은 host window 전역에 영향을 주므로 컴포넌트가 자동으로 적용하지 않습니다.
size="responsive"는 CSS viewport breakpoint가 없는 Lynx에서 지원하지 않습니다. large 또는 medium을 명시합니다.
HTML form submit, browser validation, aria-describedby id 연결은 지원하지 않습니다.