Text Field Input
한 줄 텍스트를 입력받고 Field의 레이블·설명·오류 상태와 조합하는 컴포넌트입니다.
<input>import "./styles";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<view className="text-field-input-preview">
<TextField label="라벨">
<TextFieldInput accessibility-label="라벨" />
</TextField>
</view>
);
}Lynx의 TextFieldInput은 React의 autoFocus prop을 지원하지 않습니다. 처음부터 포커스해야 한다면 입력에 id를 지정하고, 화면이 준비된 뒤 Lynx native input의 focus UI method를 호출하세요.
Installation
npx @seed-design/cli add ui:text-fieldpnpm dlx @seed-design/cli add ui:text-fieldyarn dlx @seed-design/cli add ui:text-fieldbun x @seed-design/cli add ui:text-field의존성 설치
npm install @seed-design/lynx-reactyarn add @seed-design/lynx-reactpnpm add @seed-design/lynx-reactbun add @seed-design/lynx-react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:text-field
* @requires @seed-design/lynx-react@^1.0.0
* @requires @seed-design/lynx-css@^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" | "nativeInsertionMaxLength"> {
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.
*/
Props
TextField
Prop
Type
children?React.ReactNodelabel?React.ReactNodelabelWeight?"medium" | "bold" | undefinedindicator?React.ReactNodeprefixIcon?React.ReactElement<LynxIconElementProps, string | React.JSXElementConstructor<any>> | undefinedprefix?React.ReactNodesuffixIcon?React.ReactElement<LynxIconElementProps, string | React.JSXElementConstructor<any>> | undefinedsuffix?React.ReactNodedescription?React.ReactNodeerrorMessage?React.ReactNodehideCharacterCount?boolean | undefinedmaxGraphemeCount?number | undefinedshowRequiredIndicator?boolean | undefinedfieldRef?React.Ref<NodesRef> | undefinedonValueChange?((values: { value: string; graphemes: string[]; slicedValue: string; slicedGraphemes: string[]; }) => void) | undefinedvalue?string | undefinedTextFieldInput
Prop
Type
Examples
State
Enabled
import "./styles";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField label="라벨" description="설명을 써주세요">
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
label="라벨"
description="설명을 써주세요"
invalid
errorMessage="오류가 발생한 이유를 써주세요"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField variant="underline" description="설명을 써주세요">
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
variant="underline"
description="설명을 써주세요"
invalid
errorMessage="오류가 발생한 이유를 써주세요"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Disabled
import "./styles";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField label="라벨" description="설명을 써주세요" disabled>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
label="라벨"
description="설명을 써주세요"
disabled
invalid
errorMessage="오류가 발생한 이유를 써주세요"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField variant="underline" description="설명을 써주세요" disabled>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
variant="underline"
description="설명을 써주세요"
disabled
invalid
errorMessage="오류가 발생한 이유를 써주세요"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Read Only
import "./styles";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField label="라벨" description="설명을 써주세요" readOnly>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
label="라벨"
description="설명을 써주세요"
readOnly
invalid
errorMessage="오류가 발생한 이유를 써주세요"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField variant="underline" description="설명을 써주세요" readOnly>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
variant="underline"
description="설명을 써주세요"
readOnly
invalid
errorMessage="오류가 발생한 이유를 써주세요"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Size
size로 TextField의 크기를 정합니다. (default: large)
Lynx에서는 large와 medium을 지원합니다. CSS viewport breakpoint가 없어 responsive는 지원하지 않습니다.
import "./styles";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField label="라벨" description="size=large (default)" size="large">
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField label="라벨" description="size=medium" size="medium">
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField variant="underline" description="size=large (default)" size="large">
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField variant="underline" description="size=medium" size="medium">
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Customizable Parts
아이콘만으로 맥락을 전달할 때는 description 또는 label에 아이콘의 의미를 설명하는 텍스트를 포함하세요. 아이콘 자체를 읽어야 한다면 accessibility-label을 지정하세요.
Prefix
import "./styles";
import IconMagnifyingglassLine from "@karrotmarket/lynx-monochrome-icon/IconMagnifyingglassLine";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField
label="소셜 미디어 URL"
description="프로필이나 페이지 URL을 입력해주세요."
prefix="https://"
>
<TextFieldInput accessibility-label="소셜 미디어 URL" placeholder="example.com" />
</TextField>
<TextField
label="검색"
description="글 제목 또는 내용으로 검색할 수 있습니다."
prefixIcon={<IconMagnifyingglassLine />}
>
<TextFieldInput accessibility-label="검색" placeholder="레모네이드 레시피" />
</TextField>
<TextField
variant="underline"
description="프로필이나 페이지 URL을 입력해주세요."
prefix="https://"
>
<TextFieldInput accessibility-label="소셜 미디어 URL" placeholder="example.com" />
</TextField>
<TextField
variant="underline"
description="글 제목 또는 내용으로 검색할 수 있습니다."
prefixIcon={<IconMagnifyingglassLine />}
>
<TextFieldInput accessibility-label="검색" placeholder="레모네이드 레시피" />
</TextField>
</VStack>
);
}Suffix
import "./styles";
import IconWonLine from "@karrotmarket/lynx-monochrome-icon/IconWonLine";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField label="너비" description="직접 측정 후 입력해주세요." suffix="cm">
<TextFieldInput accessibility-label="너비" placeholder="200" />
</TextField>
<TextField label="금액" description="단위: 원" suffixIcon={<IconWonLine />}>
<TextFieldInput accessibility-label="금액" placeholder="50,000" />
</TextField>
<TextField variant="underline" description="직접 측정 후 입력해주세요." suffix="cm">
<TextFieldInput accessibility-label="너비" placeholder="200" />
</TextField>
<TextField variant="underline" description="단위: 원" suffixIcon={<IconWonLine />}>
<TextFieldInput accessibility-label="금액" placeholder="50,000" />
</TextField>
</VStack>
);
}Both Affixes
import "./styles";
import IconPlusCircleLine from "@karrotmarket/lynx-monochrome-icon/IconPlusCircleLine";
import IconWonLine from "@karrotmarket/lynx-monochrome-icon/IconWonLine";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField
label="나이"
description="오늘 기준, 만 나이를 입력해주세요."
prefix="만"
suffix="세"
>
<TextFieldInput accessibility-label="나이" placeholder="플레이스홀더" />
</TextField>
<TextField
label="금액"
description="정산할 금액을 입력해주세요."
prefixIcon={<IconPlusCircleLine />}
suffixIcon={<IconWonLine accessibility-label="원" />}
>
<TextFieldInput accessibility-label="금액" placeholder="플레이스홀더" />
</TextField>
<TextField
variant="underline"
description="오늘 기준, 만 나이를 입력해주세요."
prefix="만"
suffix="세"
>
<TextFieldInput accessibility-label="나이" placeholder="플레이스홀더" />
</TextField>
<TextField
variant="underline"
description="정산할 금액을 입력해주세요."
prefixIcon={<IconPlusCircleLine />}
suffixIcon={<IconWonLine accessibility-label="원" />}
>
<TextFieldInput accessibility-label="금액" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Indicator
indicator 또는 showRequiredIndicator prop을 사용할 수 있습니다.
import "./styles";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField
label="선택 필드"
labelWeight="bold"
description="설명을 써주세요"
indicator="선택"
>
<TextFieldInput accessibility-label="선택 필드" placeholder="플레이스홀더" />
</TextField>
<TextField label="필수 필드" description="설명을 써주세요" required>
<TextFieldInput accessibility-label="필수 필드" placeholder="플레이스홀더" />
</TextField>
<TextField label="필수 필드" description="설명을 써주세요" required showRequiredIndicator>
<TextFieldInput accessibility-label="필수 필드" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Grapheme Count
import "./styles";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
return (
<VStack className="text-field-input-preview__content" gap="spacingY.componentDefault">
<TextField label="라벨" description="설명을 써주세요" maxGraphemeCount={8}>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<TextField
label="라벨"
description="설명을 써주세요"
maxGraphemeCount={8}
invalid
errorMessage="에러 메시지"
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}value를 사용자 인식 문자(grapheme cluster) 단위로 나눈 결과를 onValueChange 콜백의 graphemes와 slicedGraphemes로 제공합니다.
import "./styles";
import { useState } from "@lynx-js/react";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
const [value, setValue] = useState("");
const [graphemes, setGraphemes] = useState<string[]>([]);
return (
<VStack className="text-field-input-preview__content" gap="x4">
<TextField
label="라벨"
description="국기 이모지 🇰🇷 를 추가해보세요."
maxGraphemeCount={100}
value={value}
onValueChange={({ slicedValue, slicedGraphemes }) => {
setValue(slicedValue);
setGraphemes(slicedGraphemes);
}}
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
<VStack gap="x2">
<text className="text-field-input-preview__status">
graphemes.length: {JSON.stringify(graphemes.length)}
</text>
<text className="text-field-input-preview__status">
value.length: {JSON.stringify(value.length)}
</text>
<text className="text-field-input-preview__status">
graphemes: {JSON.stringify(graphemes)}
</text>
<text className="text-field-input-preview__status">value: {value}</text>
</VStack>
</VStack>
);
}Use Cases
Controlled State
Lynx는 HTML Form을 지원하지 않습니다. value와 onValueChange를 사용해 입력값을 React state로 관리할 수 있습니다.
import "./styles";
import { useState } from "@lynx-js/react";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
const [value, setValue] = useState("안녕하세요");
return (
<VStack className="text-field-input-preview__content" gap="x3">
<TextField
label="이름"
description="입력값을 React state로 관리합니다."
value={value}
onValueChange={({ value: nextValue }) => setValue(nextValue)}
>
<TextFieldInput accessibility-label="이름" placeholder="홍길동" />
</TextField>
<text className="text-field-input-preview__status">입력값: {value}</text>
</VStack>
);
}Number Formatting
import "./styles";
import { useState } from "@lynx-js/react";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
// Lynx JS 런타임에는 `Intl`이 없어 `toLocaleString()`이 구분 기호를 넣지 않으므로 직접 넣습니다.
function formatNumber(value: string) {
if (value === "") return value;
const number = Number(value.replace(/,/g, ""));
if (Number.isNaN(number)) return "";
const [integer = "", fraction] = String(number).split(".");
const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return fraction === undefined ? grouped : `${grouped}.${fraction}`;
}
export default function Example() {
const [value, setValue] = useState("1000");
const formattedValue = formatNumber(value);
return (
<VStack className="text-field-input-preview__content">
<TextField
label="금액"
description="금액을 써주세요"
value={formattedValue}
onValueChange={({ value: nextValue }) => setValue(nextValue)}
>
<TextFieldInput accessibility-label="금액" placeholder="9,999,999" />
</TextField>
</VStack>
);
}Slicing
import "./styles";
import { useState } from "@lynx-js/react";
import { VStack } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export default function Example() {
const [value, setValue] = useState("");
return (
<VStack className="text-field-input-preview__content">
<TextField
label="라벨"
description="6글자까지 입력 가능합니다"
maxGraphemeCount={6}
value={value}
onValueChange={({ slicedValue }) => setValue(slicedValue)}
>
<TextFieldInput accessibility-label="라벨" placeholder="플레이스홀더" />
</TextField>
</VStack>
);
}Lynx Usage
Keyboard Avoidance
KeyboardAvoidingScrollView.Content 안에 배치하면 TextFieldInput이 focus될 때 자동으로 등록되어 키보드에 가려지지 않는 위치로 스크롤됩니다. KeyboardAvoidingScrollView.Footer 안에 배치하면 Footer와 함께 키보드 위로 올라가므로 스크롤하지 않습니다.
import { KeyboardAvoidingScrollView } from "@seed-design/lynx-react";
import { TextField, TextFieldInput } from "@/components/ui/text-field";
export function KeyboardAwareFields() {
return (
<KeyboardAvoidingScrollView.Root className="screen-body">
<KeyboardAvoidingScrollView.Content>
<TextField label="제목">
<TextFieldInput accessibility-label="제목" />
</TextField>
</KeyboardAvoidingScrollView.Content>
</KeyboardAvoidingScrollView.Root>
);
}Web Version Differences
- 편집 가능한 상태에서는 HTML
<input>대신 Lynx native<input>을 렌더링합니다. readOnly상태에서는 focus, selection, 잘라내기 메뉴가 없는<text>를 렌더링합니다. 이때 ref는<text>를 가리키므로 input 전용 UI method와 이벤트를 사용할 수 없습니다.onChange대신TextField의onValueChange를 사용합니다. nativebindinput은TextFieldInput에 직접 전달할 수 있습니다.- DOM id로 label을 연결하지 않으므로 입력에
accessibility-label을 직접 지정하세요. show-soft-input-on-focus의 기본값은true입니다. 커스텀 키보드를 사용한다면false로 지정하세요.android-set-soft-input-mode는 기본값이"unspecified"이며 host window 전체에 영향을 줍니다. 같은 window의 입력에는 같은 값을 사용하고,KeyboardAvoidingScrollView가 키보드 회피를 맡는 화면에서는"nothing"을 사용하세요.
Unsupported Lynx Features
- HTML form submit, browser validation, React Hook Form,
aria-describedbyid 연결은 지원하지 않습니다.
Last updated on