ObjectInput | CoUI
LogoCoUI

ObjectInput

구분자로 연결된 키-값 구조화 입력 컴포넌트

ObjectInput#

연/월/일처럼 여러 키-값 필드를 구분자로 연결해 한 줄로 입력받는 구조화 입력 컴포넌트입니다. 각 필드의 값이 Map<String, String> (필드 key → 값) 으로 방출됩니다. 단일 문자열을 마스킹하는 FormattedInput 의 구조화 데이터 버전입니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 연/월/일, 시/분/초처럼 의미가 다른 여러 필드를 한 줄로 입력받는 경우
  • 각 필드 값을 키별로 구분해 다뤄야 하는 경우

대신 다른 컴포넌트를 사용하세요:

  • FormattedInput: 단일 문자열을 마스킹하는 경우
  • TextField: 단일 자유 입력

기본 사용법 (Basic Usage)#

ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2),
  ],
  separator: '/',
  onChanged: (values) => print(values),
)
ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2),
  ],
  separator: '/',
  onChanged: (values) => print(values),
)

빠른 오버라이드 (Chain)#

이미 만든 ObjectInput 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class ObjectInputChainExample extends StatelessWidget {
  const ObjectInputChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => debugPrint('$values'),
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => debugPrint('$values'),
        ).withStyle(
          const CoreObjectInputStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class ObjectInputChainExample extends StatelessComponent {
  const ObjectInputChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => print('$values'),
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => print('$values'),
        ).withStyle(
          const CoreObjectInputStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Props / Parameters#

속성타입기본값설명
fields List<CoreObjectInputField> 키-값 필드 정의 목록 (필수)
values Map<String, String>? null 초기 값 (필드 key → 값)
separator String? null 필드 사이에 표시할 구분자 텍스트
enabledbooltrue상호작용 가능 여부
onChanged void Function(Map<String, String>)? null 값 변경 콜백 (전체 맵 전달)
leading Widget? / Component? null 필드 앞 위젯
trailing Widget? / Component? null 필드 뒤 위젯
objectInputStyle CoreObjectInputStyle? null 컨테이너 색상 / 테두리 / padding 오버라이드

CoreObjectInputField#

속성타입설명
keyString값 맵에서의 고유 키
placeholderString빈 필드 placeholder
maxLengthint?최대 입력 길이

스타일 시스템 (Style System)#

ObjectInput 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreObjectInputStyle 단일 슬롯으로 흐릅니다.

CoreObjectInputStyle 필드#

필드타입설명
backgroundColor CoreColor? Container background colour override.
borderColor CoreColor? Container border colour override.
focusBorderColor CoreColor? Container focus-ring border colour override.
borderRadius CoreBorderRadius? Container border radius override.
borderWidth double? Container border width override (logical px).
padding CoreEdgeInsets? Container padding override.
separatorTextStyle CoreTextStyle? Field separator text style override (typography role / font / colour).
fieldInputStyle CoreTextFieldStyle? Per-field TextField style override (chrome + value / placeholder typography). Nested child-component slot forwarded to the composed field TextField on both platforms.
shortFieldMaxWidth double? Max-width override for short fields (1–2 chars) (logical px).
mediumFieldMaxWidth double? Max-width override for medium fields (3–4 chars) (logical px).
longFieldMaxWidth double? Max-width override for long fields (5+ chars) (logical px).
shortFieldMaxChars int? Override for the short-field maxLength bucket upper bound (inclusive).
mediumFieldMaxChars int? Override for the medium-field maxLength bucket upper bound (inclusive).
separatorPadding CoreEdgeInsets? Custom padding around the field separator glyph (pre-scaling). null → [defaultSeparatorPadding].
charWidth double? Per-character width used to size a field to its maxChars bucket (logical px, pre-scaling). null → [defaultCharWidth].

동작 스펙 (Behavior)#

인터랙션#

  • 입력: 각 필드 입력 시 onChanged에 전체 값 맵 전달
  • 포커스: 컨테이너가 포커스되면 테두리 색이 강조됨
  • enabledfalse이면 비활성

사용 가이드라인 (Usage Guidelines)#

✅ Do#

pattern으로 필드별 허용 문자를 제한하기

ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4, pattern: r'\d'),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2, pattern: r'\d'),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2, pattern: r'\d'),
  ],
  separator: '/',
  onChanged: handleDateChanged,
)

ObjectInputkey로 필드 종류를 추론하지 않습니다. pattern이 없으면 어떤 문자든 maxLength까지 입력되고, 매칭 실패로 거부되는 문자는 양 플랫폼 모두 스크린 리더에 알려지지 않으므로 애초에 잘못된 문자를 못 치게 막는 편이 안전합니다.


❌ Don't#

필드 묶음이 하나의 의미 있는 입력으로 안내될 거라 기대하지 않기

// ❌ "생년월일" 같은 그룹 의미가 스크린 리더에 저절로 전달될 거라 기대
ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2),
  ],
  separator: '/',
  onChanged: handleDateChanged,
)

ObjectInput은 role도 접근 가능한 이름도, 필드 개수·순서 안내도, 자동 이동/Backspace 복귀도 제공하지 않습니다 — 각 필드는 서로 무관한 입력 세 개로 읽힙니다. 그룹 이름과 안내가 필요하면 소비자가 직접 부여해야 합니다.

접근성 (Accessibility)#

역할 / Semantics#

ObjectInput 자체는 role 도 그룹 semantics 도 내보내지 않습니다. Flutter 구현에는 Semantics 호출이 하나도 없고 루트는 Focus > Container > Row 이며, Web 루트는 호출자 passthrough 속성만 실은 맨 <div>role / aria-* / <fieldset> / <legend> / <label> 어느 것도 붙지 않습니다. 필드별 wrapper 도 평범한 <div>, 구분자도 평범한 <span>(Web) / Text(Flutter) 입니다.

실제 semantics 는 전부 합성된 TextField 각각에서 나옵니다 — Web 은 native <input>, Flutter 는 TextField/EditableText 의 기본 textField semantics. role 이나 aria 를 얹으려면 Web 은 attributes: passthrough 로 직접 주입해야 하고, Flutter 에는 대응하는 통로가 없어 소비자가 바깥에서 Semantics 로 감싸야 합니다.

키보드#

ObjectInput 이 직접 처리하는 키가 없습니다. 타이핑·캐럿·선택은 모두 각 TextField 의 몫이고, 컴포넌트 차원의 maxLength 자동 이동, Backspace 로 이전 필드 복귀, 화살표 키 필드 이동은 어느 플랫폼에도 없습니다. Web 의 onKeyDown / onKeyUp 은 호출자 passthrough 프로퍼티일 뿐 소비자가 넘기지 않으면 아무 동작도 하지 않습니다.

포커스#

포커스 링에 해당하는 시각 표현은 테두리 색 변경 하나이며, 그 상태는 보조기술에 노출되지 않습니다.

  • Flutter: Focus(onFocusChange:) 가 행 전체를 감싸고 그 결과로 테두리 색만 고릅니다. focusNode / canRequestFocus: false / skipTraversal: true 를 넘기지 않아 이 내부 노드는 Flutter 기본값(포커스 가능 + traversal 대상)으로 만들어지므로, 컨테이너가 필드들보다 앞선 탭 정지점 하나를 더 차지합니다.
  • Web: 루트 <div>tabindex 가 없어 포커스 대상이 아닙니다. 테두리 색을 위해 focusin / focusout 만 듣고, 그 리스너는 enabled == false 일 때 아예 등록되지 않습니다.

포커스 트랩, 포커스 복원, autofocus 는 양쪽 모두 없습니다.

스크린 리더#

합성된 TextField 각각이 자기 hintText/placeholder 와 native input semantics 를 안내하고, 그 사이의 구분자 문자가 텍스트 노드로 함께 읽힙니다. ObjectInput 은 여기에 그룹 이름도, 필드 개수·위치("3개 중 2번째")도, 필드 간 관계도 더하지 않으며, 포커스 링이 바뀌어도 아무것도 안내되지 않습니다.

알려진 제약#

  • 필드 묶음에 role 도 접근 가능한 이름도 없습니다 — 연/월/일 세 필드가 서로 무관한 입력 세 개로 읽힙니다. 하나의 입력으로 인지시키려면 소비자가 직접 이름과 그룹을 부여해야 합니다.
  • 다중 파트 입력의 관례적 키보드 모델이 없습니다 — 자동 이동, Backspace 복귀, 화살표 이동 중 어느 것도 구현되어 있지 않습니다.
  • 구분자 텍스트가 리더에서 감춰지지 않습니다aria-hidden 이 붙지 않아 필드 사이에서 그대로 읽힙니다.
  • traversal 이 플랫폼마다 다릅니다 — Flutter 의 Focus 가 만드는 컨테이너 탭 정지점이 Web 에는 없어 탭 정지점 개수가 어긋납니다.
  • Web 은 enabled: false 일 때 자체 포커스 추적을 중단합니다.
  • pattern / maxLength 로 입력이 거부되어도 리더에 알려지지 않습니다 — 양 플랫폼 공통입니다.

포커스 링 색 대비 등 전역 축은 전역 접근성 축 을 참고하세요.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명ObjectInputObjectInput
필드TextField (chrome 제거)native <input>
포커스 링 Focus + 테두리 색 변경 focus / blur 이벤트