PlOtpField
한 글자짜리 칸이 늘어선 줄입니다. PIN이나 문자로 받은 인증 코드, 초대 키에 씁니다. 칸이 몇 개든 그 뒤에는 값 하나가 있고, 붙여넣기와 백스페이스, 휴대폰의 자동 완성이 모두 독자가 기대하는 대로 동작합니다.
import { PlOtpField } from 'plass-ui';
<PlOtpField label="Verification code" groupSize={3} onComplete={verify} />;import 'package:plass_ui/plass_ui.dart';
PlOtpField(
label: const Text('Verification code'),
groupSize: 3,
onCompleted: verify,
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | 칸의 재질. solid는 색이 든 판이 아니라 웰입니다 — PlTextField와 같은 이유입니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 칸의 상자와 그 안의 타입 스케일. 컨트롤 사다리가 아니라 칸 자신의 사다리입니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 칸 사이 간격만 바꿉니다 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음 |
| length | number | 6 | 코드의 글자 수. 2–12로 잘립니다 |
| charset | 'numeric' | 'alpha' | 'alphanumeric' | 'any' | 'numeric' | 입력할 수 있는 문자. 거부된 것은 버리고 onValueInvalid로 보고합니다 |
| mask | boolean | false | 비밀번호 필드처럼 글자를 가립니다 |
| groupSize | number | — | 이 칸 수마다 구분자로 줄을 나눕니다 |
| separator | ReactNode | '–' | 두 덩어리 사이에 그려지는 것 |
| value | string | — | 코드. onValueChange와 함께 controlled로 씁니다 |
| defaultValue | string | — | uncontrolled일 때 시작 값 |
| onValueChange | (value: string) => void | — | 코드가 바뀔 때 호출됩니다 |
| onComplete | (value: string) => void | — | 모든 칸이 채워지는 순간 호출됩니다 — 코드를 확인할 때 |
| onValueInvalid | (value: string) => void | — | 입력이나 붙여넣기에 charset이 거부하는 글자가 있었을 때 |
| autoSubmit | boolean | false | 코드가 완성되는 즉시 폼을 전송합니다 |
| label · description · error | ReactNode | — | 줄 위의 라벨, 아래의 보조 문구와 오류 메시지 |
| invalid | boolean | — | 메시지 없이 invalid 상태를 강제합니다. 기본값은 error가 있는지 여부입니다 |
| name | string | — | 폼 전송에서 필드를 식별합니다 |
| required | boolean | false | 코드가 완성되기 전까지 폼이 전송되지 않습니다 |
| disabled | boolean | false | 모든 칸이 반응하지 않습니다 |
| readOnly | boolean | false | 읽고 복사할 수는 있지만 입력할 수는 없습니다 |
| autoFocus | boolean | false | 마운트 시 첫 칸에 캐럿을 놓습니다 |
| hotKeys | Record<string, () => void> | — | 이 컨트롤이 답할 chord들. PlHotKeys가 그리는 것과 같은 철자입니다 — { 'Mod+Enter': save, Escape: cancel }. 맞는 chord는 **소비됩니다** |
| classNames | { label?, control?, description?, error?: string } | — | className이 닿지 않는 부분에 붙는 class. control은 실제로 조작하는 부분입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| controller | TextEditingController? | — | 입력 중인 코드. 주지 않으면 필드가 자기 것을 하나 듭니다 |
| onChanged | ValueChanged<String>? | — | 코드가 바뀔 때 호출됩니다 |
| onCompleted | ValueChanged<String>? | — | 모든 칸이 채워지는 순간 호출됩니다 — 코드를 확인할 때 |
| onRejected | ValueChanged<String>? | — | charset이 거부한 글자들로 호출됩니다 |
| variant공통 | PlassVariant | PlassVariant.glass | 칸의 재질. solid는 색이 든 판이 아니라 웰입니다 — PlTextField와 같은 이유입니다 |
| size공통 | PlassSize | PlassSize.md | 칸의 상자와 그 안의 타입 스케일. 컨트롤 사다리가 아니라 칸 자신의 사다리입니다 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 칸 사이 간격만 바꿉니다 |
| elevation공통 | int | 0 | 그림자 깊이. 0은 그림자 없음 |
| length | int | 6 | 코드의 글자 수. 2–12로 잘립니다 |
| charset | PlOtpCharset | PlOtpCharset.numeric | 입력할 수 있는 문자. 거부된 것은 버리고 onValueInvalid로 보고합니다 |
| mask | bool | false | 비밀번호 필드처럼 글자를 가립니다 |
| groupSize | int? | — | 이 칸 수마다 구분자로 줄을 나눕니다 |
| separator | String | '–' | 두 덩어리 사이에 그려지는 것 |
| label · description · error | Widget? | — | 줄 위의 라벨, 아래의 보조 문구와 오류 메시지 |
| invalid | bool? | — | 메시지 없이 invalid 상태를 강제합니다. 기본값은 error가 있는지 여부입니다 |
| disabled | bool | false | 모든 칸이 반응하지 않습니다 |
| readOnly | bool | false | 읽고 복사할 수는 있지만 입력할 수는 없습니다 |
| semanticLabel | String? | — | 스크린 리더가 줄을 부르는 이름 |
| focusNode · autofocus | FocusNode? · bool | — | 포커스를 밖에서 제어하거나, 트리에 들어가면서 캐럿을 놓습니다 |
| hotKeys | PlassHotKeys? | — | 이 컨트롤이 답할 chord들. PlHotKeys가 그리는 것과 같은 철자입니다 — { 'Mod+Enter': save, Escape: cancel }. 맞는 chord는 **소비됩니다** |
네이티브 <div> 속성은 바깥의 field가 아니라 칸이 늘어선 줄에 그대로 전달됩니다. color는 여기서 Plass의 prop이라, onChange는 이 컴포넌트가 onValueChange로 쓰기 때문에, children은 칸들이 곧 children이기 때문에 제외됩니다.
className은 label과 control, 그 아래 두 줄을 함께 담는 stack에 붙습니다. 그 안쪽 네 부분에 닿는 것이 classNames입니다: label, control(칸이 늘어선 줄), description, error.
값은 PlTextField에서와 마찬가지로 TextEditingController에 있습니다. 그래서 value와 defaultValue가 매개변수 하나로 합쳐지고, 코드를 지우고 싶은 호출자는 controller.text를 설정합니다.
라이브러리 전체에서 공유 축(variant size color density elevation)이 뜻하는 바는 prop 규칙에 있습니다.
Examples
length
2–12로 잘립니다. 상자 하나짜리는 PlTextField이고, 열둘을 넘기면 줄이 휴대폰에 들어가지 않습니다. 이런 코드는 대개 휴대폰에서 입력됩니다.
charset
입력할 수 있는 문자입니다. 거부된 것은 보여 주지 않고 버리되 onValueInvalid로 보고합니다. 키 입력을 조용히 삼키는 칸은 독자가 고장 났다고 여기는 칸입니다.
기본값이 numeric인 것은 문자로 오는 코드가 그렇기 때문이고, 휴대폰에 숫자 키패드를 띄우는 것도 그것이기 때문입니다.
groupSize와 separator
여섯 자리 코드에 groupSize={3}이면 익숙한 세 자리 두 덩어리가 됩니다. 구분자는 두 가지를 나누는 경계가 아니라 값 하나 안의 구두점이라서, 스크린 리더에서는 완전히 숨깁니다. 덩어리마다 그것을 읽어 주는 리더는 그 안의 코드가 아니라 상자의 생김새를 읽고 있는 셈입니다.
variant
PlTextField, PlSelect와 같은 field 껍데기입니다. 칸은 field 모양의 상자이고, 둘을 함께 담은 폼이 서로 다른 폼 키트를 쌓아 놓은 것처럼 보여서는 안 되기 때문입니다. solid는 색이 든 판이 아니라 웰입니다. 가장 불투명한 유리에 그림자가 안쪽으로 떨어지는 것. text field에서와 같은 이유입니다. 캐럿과 선택 영역이 그 위에서 읽혀야 합니다.
size
칸은 컨트롤 사다리가 아니라 자기 사다리를 씁니다. 체크박스의 틱과 같은 이유입니다. 칸은 컨트롤들 사이의 컨트롤이 아니라 홀로 선 글자 하나이고, md PlButton 높이의 md 칸은 책상 건너에서 코드를 읽기에 너무 작습니다. 모든 단계가 너비보다 높이가 커서, 이 줄이 작은 field의 행렬이 아니라 글자 하나씩의 자리로 읽힙니다.
타입 스케일도 함께 컨트롤 사다리에서 두 단계 위입니다. 인증 코드는 한 손에 든 휴대폰에서 읽어 다른 손으로 입력됩니다. 폼에서 위의 라벨보다 커야 하는 유일한 글입니다.
density는 칸 사이 간격만 건드립니다.
mask, readOnly, disabled, error
error는 메시지를 담는 동시에 필드를 invalid로 바꿉니다. 그러면 칸의 색 가족 전체가 danger로 다시 향해서, 가장자리 · 링 · 캐럿 · 메시지가 한꺼번에 넘어갑니다. invalid는 유효성을 폼 라이브러리가 쥐고 있을 때의 탈출구입니다.
구성
칸마다 <input> 하나씩이고, Base UI가 그 뒤에 값 하나를 유지합니다. 브라우저의 붙여넣기와 자동 완성이 기대하는 모양이 그것이고, 클릭이 포인터 아래의 상자가 아니라 첫 빈 칸에 떨어지게 만드는 것도 그것입니다.
줄 전체 뒤에 편집기 하나를 두고 칸으로 그립니다. Flutter의 텍스트 입력은 플랫폼과의 단일 연결이고, 그것을 여섯으로 쪼개면 코드 하나를 두고 키보드 여섯이 다투게 됩니다. 그래서 값은 TextEditingController에 있고, 상자는 거기서 그려지며, 줄 어디를 눌러도 캐럿은 첫 빈 칸으로 갑니다.
편집기는 화면 밖으로 치우지 않고 줄 위에 불투명도 0으로 배치됩니다. 텍스트 입력이 그 연결을 유지하려면 트리 안에 있고 측정되어야 해서 Offstage가 될 수 없습니다. 아무것도 편집기를 직접 건드리지 않고(제스처 하나가 모든 누름을 소유합니다) 독자가 보는 것은 상자입니다.
거부된 글자는 Flutter의 FilteringTextInputFormatter가 아니라 컴포넌트 자신의 포매터를 지납니다. 그쪽은 버리기만 하고 아무 말도 하지 않습니다. 조용히 사라지는 거부는 코드 필드가 저지를 수 있는 최악의 일입니다. 독자는 키를 누르고, 아무 일도 일어나지 않는 것을 보고, 필드가 고장 났다고 결론짓습니다.
Accessibility
- Base UI의 OTP Field 위에 있습니다. 보기보다 어려운 부분을 전부 그쪽이 맡습니다: 칸이 몇 개든 그 뒤의 값 하나, 캐럿이 있던 자리에서부터 칸에 흩뿌려지는 붙여넣기, 한 칸 뒤로 물러나는 백스페이스, 그리고 포인터 아래가 아니라 첫 빈 칸에 떨어지는 클릭.
- 모든 칸이
autocomplete="one-time-code"를 들고 있어서, 휴대폰이 메시지에서 코드를 바로 제안합니다. - 라벨 · 설명 · 오류는 Base UI의
Field가 줄에 연결합니다.for하나,aria-describedby하나, 호출자가 맞춰 두어야 할 id는 없습니다. - 구분자는
role="separator"가 아니라aria-hidden이 붙은<span>입니다. 두 가지 사이의 경계가 아니라 값 하나 안의 구두점입니다. - 칸의 포커스 링은
:focus-visible이 아니라:focus입니다. 라이브러리에서 그 구분을 의도적으로 내려놓는 유일한 자리입니다. 칸은 타이핑만큼이나 클릭으로 포커스를 받고, 다음 키가 어느 글자에 떨어지는지를 말해 주는 것은 그 링뿐입니다.
- 줄 전체가 코드를 값으로 들고 있는 텍스트 필드 시맨틱 노드 하나입니다. 상자는 그 값의 그림이고 시맨틱에서 완전히 빠지므로, 스크린 리더는 빈 사각형을 세는 대신 코드를 읽습니다.
- 편집기가
AutofillHints.oneTimeCode를 들고 있어서, 휴대폰이 메시지에서 코드를 바로 제안합니다. - 링은 다음 키가 떨어질 칸에 그려지고, focus-visible이 아니라 focus를 따릅니다. 다른 패키지에서와 같은 이유입니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
칸마다 <input> 하나 | 줄 뒤에 편집기 하나 | Flutter의 텍스트 입력은 플랫폼과의 단일 연결입니다. 여섯이면 코드 하나를 두고 키보드 여섯이 다툽니다. |
value / defaultValue / onValueChange | controller / onChanged | Flutter의 편집 가능한 위젯이 모두 가진 모양이고, PlTextField가 이미 쓰는 것입니다. |
onValueInvalid | onRejected | 살아남은 값이 아니라 거부된 글자를 건네줍니다. 둘 중 쓸모 있는 쪽입니다. |
name, required, autoSubmit | — | 셋 다 HTML 폼 전송에 관한 것이고, Flutter에는 대응물이 없습니다. |
autoFocus | autofocus | Flutter의 표기입니다. |
className, style | — | 전달할 클래스 목록도 style 속성도 없습니다. |