ObjectInput#
연/월/일처럼 여러 키-값 필드를 구분자로 연결해 한 줄로 입력받는 구조화 입력 컴포넌트입니다. 각 필드의 값이 Map<String, String>
(필드 key → 값) 으로 방출됩니다. 단일 문자열을 마스킹하는 FormattedInput 의 구조화 데이터 버전입니다.
Live Preview#
class ObjectInputDefaultExample extends StatelessComponent {
const ObjectInputDefaultExample({super.key});
@override
Component build(BuildContext context) {
return 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'),
);
}
}
class ObjectInputDefaultExample extends StatelessWidget {
const ObjectInputDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return 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'),
);
}
}
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',
);
}
}
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,
),
),
],
);
}
}
사용 시기 (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 |
필드 사이에 표시할 구분자 텍스트 |
enabled | bool | true | 상호작용 가능 여부 |
onChanged |
void Function(Map<String, String>)? |
null |
값 변경 콜백 (전체 맵 전달) |
leading |
Widget? / Component? |
null |
필드 앞 위젯 |
trailing |
Widget? / Component? |
null |
필드 뒤 위젯 |
objectInputStyle |
CoreObjectInputStyle? |
null |
컨테이너 색상 / 테두리 / padding 오버라이드 |
CoreObjectInputField#
| 속성 | 타입 | 설명 |
|---|---|---|
key | String | 값 맵에서의 고유 키 |
placeholder | String | 빈 필드 placeholder |
maxLength | int? | 최대 입력 길이 |
스타일 시스템 (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에 전체 값 맵 전달 - 포커스: 컨테이너가 포커스되면 테두리 색이 강조됨
enabled가false이면 비활성
사용 가이드라인 (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,
)
ObjectInput은 key로 필드 종류를 추론하지 않습니다. 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)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | ObjectInput | ObjectInput |
| 필드 | TextField (chrome 제거) | native <input> |
| 포커스 링 | Focus + 테두리 색 변경 |
focus / blur 이벤트 |
관련 컴포넌트 (Related Components)#
- FormattedInput: 단일 문자열 마스크 입력
- TextField: 자유 형식 텍스트 입력