로고SEED Design
Text Fields

Multiline Text Field

이 문서는 정리 중이에요. 문의 내용은 #_design_core 채널을 찾아주세요.

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldPreview() {
  return (
    <TextField>
      <TextFieldTextarea autoFocus />
    </TextField>
  );
}

Installation

npx @seed-design/cli@latest add text-field
pnpm dlx @seed-design/cli@latest add text-field
yarn dlx @seed-design/cli@latest add text-field
bun x @seed-design/cli@latest add text-field

Props

TextField

PropTypeDefault
asChild?
boolean
false
maxGraphemeCount?
number
-
onValueChange?
((values: { value: string; graphemes: string[]; slicedValue: string; slicedGraphemes: string[]; }) => void)
-
name?
string
-
invalid?
boolean
false
readOnly?
boolean
false
disabled?
boolean
false
required?
boolean
false
defaultValue?
string
-
value?
string
-
size?
"xlarge" | "large" | "medium"
medium
hideCharacterCount?
boolean
-
errorMessage?
ReactNode
-
description?
ReactNode
-
suffix?
ReactNode
-
suffixIcon?
ReactNode
-
prefix?
ReactNode
-
prefixIcon?
ReactNode
-
indicator?
ReactNode
-
label?
ReactNode
-

TextFieldTextarea

PropTypeDefault
asChild?
boolean
false
autoresize?
boolean
true

Examples

State

Enabled

import { HStack } from "@seed-design/react";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldEnabled() {
  return (
    <HStack width="full" gap="x3">
      <TextField label="라벨" description="설명을 써주세요">
        <TextFieldTextarea placeholder="플레이스홀더" />
      </TextField>
      <TextField
        label="라벨"
        description="설명을 써주세요"
        invalid
        errorMessage="오류가 발생한 이유를 써주세요"
      >
        <TextFieldTextarea placeholder="플레이스홀더" />
      </TextField>
    </HStack>
  );
}

Disabled

import { HStack } from "@seed-design/react";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldDisabled() {
  return (
    <HStack width="full" gap="x3">
      <TextField label="라벨" description="설명을 써주세요" disabled>
        <TextFieldTextarea placeholder="플레이스홀더" />
      </TextField>
      <TextField
        label="라벨"
        description="설명을 써주세요"
        disabled
        invalid
        errorMessage="오류가 발생한 이유를 써주세요"
      >
        <TextFieldTextarea placeholder="플레이스홀더" />
      </TextField>
    </HStack>
  );
}

Read Only

import { HStack } from "@seed-design/react";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldReadOnly() {
  return (
    <HStack width="full" gap="x3">
      <TextField label="라벨" description="설명을 써주세요" readOnly>
        <TextFieldTextarea placeholder="플레이스홀더" />
      </TextField>
      <TextField
        label="라벨"
        description="설명을 써주세요"
        readOnly
        invalid
        errorMessage="오류가 발생한 이유를 써주세요"
      >
        <TextFieldTextarea placeholder="플레이스홀더" />
      </TextField>
    </HStack>
  );
}

Sizing

Fixed Height

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldSpecifiedHeight() {
  return (
    <TextField label="라벨" description="설명을 써주세요">
      <TextFieldTextarea placeholder="플레이스홀더" style={{ height: "250px" }} />
    </TextField>
  );
}

Auto Height with Constraints

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldConstraints() {
  return (
    <TextField label="라벨" description="설명을 써주세요">
      <TextFieldTextarea
        placeholder="플레이스홀더"
        style={{ minHeight: "200px", maxHeight: "300px" }}
      />
    </TextField>
  );
}

Customizable Parts

Required Indicator

(필수)
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldRequired() {
  return (
    <TextField label="라벨" description="설명을 써주세요" required indicator="(필수)">
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Optional Indicator

(선택)
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldOptional() {
  return (
    <TextField label="라벨" description="설명을 써주세요" indicator="(선택)">
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Grapheme Count

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldGraphemeCount() {
  return (
    <TextField label="라벨" description="설명을 써주세요" maxGraphemeCount={8}>
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Size

XLarge

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldXlarge() {
  return (
    <TextField label="라벨" description="설명을 써주세요" size="xlarge">
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Large

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldLarge() {
  return (
    <TextField label="라벨" description="설명을 써주세요" size="large">
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Medium

import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldMedium() {
  return (
    <TextField label="라벨" description="설명을 써주세요" size="medium">
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Use Cases

Form

(필수)
(필수)
import { HStack, VStack } from "@seed-design/react";
import { useCallback, useState, type FormEvent } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

interface FormValues {
  bio: string;
  address: string;
}

type FieldErrors = Record<keyof FormValues, string | null>;

export default function MultilineTextFieldForm() {
  const [formValues, setFormValues] = useState<FormValues>({
    bio: "",
    address: "",
  });

  const [fieldErrors, setFieldStates] = useState<FieldErrors>({
    bio: null,
    address: null,
  });

  const validateForm = useCallback((): boolean => {
    let isValid = true;

    const newFieldErrors: FieldErrors = {
      bio: null,
      address: null,
    };

    // Name validation
    if (!formValues.bio) {
      newFieldErrors.bio = "필수 입력 항목입니다";
      isValid = false;
    }

    if (!formValues.address.startsWith("대한민국")) {
      newFieldErrors.address = "대한민국으로 시작해주세요";
      isValid = false;
    }

    if (!formValues.address) {
      newFieldErrors.address = "필수 입력 항목입니다";
      isValid = false;
    }

    setFieldStates(newFieldErrors);

    return isValid;
  }, [formValues]);

  const handleSubmit = useCallback(
    (event: FormEvent) => {
      event.preventDefault();

      if (validateForm()) {
        window.alert(JSON.stringify(formValues, null, 2));
      }
    },
    [formValues, validateForm],
  );

  const handleReset = useCallback((event: FormEvent) => {
    event.preventDefault();

    setFormValues({ bio: "", address: "" });
    setFieldStates({ bio: null, address: null });
  }, []);

  const handleNameChange = (value: string) => {
    setFormValues((prev) => ({ ...prev, bio: value }));
    setFieldStates((prev) => ({ ...prev, name: null }));
  };

  const handleAddressChange = (value: string) => {
    setFormValues((prev) => ({ ...prev, address: value }));
    setFieldStates((prev) => ({ ...prev, address: null }));
  };

  return (
    <VStack gap="x3" width="full" as="form" onSubmit={handleSubmit} onReset={handleReset}>
      <HStack gap="x2">
        <TextField
          label="자기소개"
          indicator="(필수)"
          description="자기소개를 써주세요"
          required
          value={formValues.bio}
          onValueChange={({ value }) => handleNameChange(value)}
          {...(fieldErrors.bio && { invalid: true, errorMessage: fieldErrors.bio })}
        >
          <TextFieldTextarea placeholder="저는…" />
        </TextField>
        <TextField
          label="주소"
          indicator="(필수)"
          description="주소를 써주세요"
          maxGraphemeCount={30}
          required
          value={formValues.address}
          onValueChange={({ slicedValue }) => handleAddressChange(slicedValue)}
          {...(fieldErrors.address && { invalid: true, errorMessage: fieldErrors.address })}
        >
          <TextFieldTextarea placeholder="대한민국" />
        </TextField>
      </HStack>
      <HStack gap="x2">
        <ActionButton type="reset" variant="neutralWeak">
          초기화
        </ActionButton>
        <ActionButton type="submit" flexGrow={1}>
          제출
        </ActionButton>
      </HStack>
    </VStack>
  );
}

React Hook Form

(필수)
(필수)
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent, type KeyboardEvent } from "react";
import { useController, useForm } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

interface FormValues {
  bio: string;
  address: string;
}

export default function MultilineTextFieldReactHookForm() {
  const { handleSubmit, reset, control } = useForm<FormValues>({
    defaultValues: {
      bio: "",
      address: "",
    },
    shouldFocusError: true,
  });

  const { field: bioField, fieldState: bioFieldState } = useController({
    name: "bio",
    control,
    rules: {
      required: "필수 입력 항목입니다",
    },
  });
  const {
    field: { onChange: addressOnChange, ...addressField },
    fieldState: addressFieldState,
  } = useController({
    name: "address",
    control,
    rules: {
      required: "필수 입력 항목입니다",
      pattern: { value: /^대한민국/, message: "대한민국으로 시작해주세요" },
    },
  });

  const onValid = useCallback((data: FormValues) => {
    window.alert(JSON.stringify(data, null, 2));
  }, []);

  const onReset = useCallback(
    (event: FormEvent) => {
      event.preventDefault();
      reset();
    },
    [reset],
  );

  const onMetaReturn = useCallback(
    (event: KeyboardEvent) => {
      if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault();
        handleSubmit(onValid)();
      }
    },
    [handleSubmit, onValid],
  );

  return (
    <VStack gap="x3" width="full" as="form" onSubmit={handleSubmit(onValid)} onReset={onReset}>
      <HStack gap="x2">
        <TextField
          label="자기소개"
          indicator="(필수)"
          description="자기소개를 써주세요"
          invalid={bioFieldState.invalid}
          errorMessage={bioFieldState.error?.message}
          required
          {...bioField}
        >
          <TextFieldTextarea placeholder="저는…" onKeyDown={onMetaReturn} />
        </TextField>
        <TextField
          label="주소"
          indicator="(필수)"
          description="주소를 써주세요"
          invalid={addressFieldState.invalid}
          errorMessage={addressFieldState.error?.message}
          maxGraphemeCount={30}
          onValueChange={({ slicedValue }) => addressOnChange(slicedValue)}
          required
          {...addressField}
        >
          <TextFieldTextarea placeholder="대한민국" onKeyDown={onMetaReturn} />
        </TextField>
      </HStack>
      <HStack gap="x2">
        <ActionButton type="reset" variant="neutralWeak">
          초기화
        </ActionButton>
        <ActionButton type="submit" flexGrow={1}>
          제출
        </ActionButton>
      </HStack>
    </VStack>
  );
}

Formatting

import { useMemo, useState } from "react";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldFormatting() {
  const [value, setValue] = useState("");

  const formattedValue = useMemo(
    () =>
      value
        .split("")
        .filter((char) => char !== " ")
        .join(""),
    [value],
  );

  return (
    <TextField
      label="레이블"
      description="공백을 입력할 수 없어요"
      value={formattedValue}
      onValueChange={({ value }) => setValue(value)}
    >
      <TextFieldTextarea placeholder="공백을 입력해보세요" />
    </TextField>
  );
}

Slicing

import { useState } from "react";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";

export default function MultilineTextFieldSlicing() {
  const [value, setValue] = useState("");

  return (
    <TextField
      label="라벨"
      description="6글자까지 입력 가능합니다"
      maxGraphemeCount={6}
      value={value}
      onValueChange={({ slicedValue }) => setValue(slicedValue)}
    >
      <TextFieldTextarea placeholder="플레이스홀더" />
    </TextField>
  );
}

Last updated on