PlNumberField
숫자만 담는 field입니다. 껍데기는 PlTextField와 픽셀 단위로 같고, 그 위에 진짜 숫자 컨트롤이 얹힙니다: 방향키, 스테퍼, 범위 고정, locale을 아는 서식.
import { PlNumberField } from 'plass-ui';
<PlNumberField label="Quantity" min={1} max={12} defaultValue={2} />;
<PlNumberField label="Budget" locale="en-US" format={{ style: 'currency', currency: 'USD' }} />;import 'package:plass_ui/plass_ui.dart';
PlNumberField(
label: const Text('Quantity'),
min: 1,
max: 12,
value: quantity,
onChanged: (double? next) => setState(() => quantity = next),
);
PlNumberField(
label: const Text('Budget'),
value: budget,
format: (double value) => '\$${value.toStringAsFixed(2)}',
onChanged: (double? next) => setState(() => budget = next),
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | 껍데기의 재질. PlTextField와 픽셀 단위로 같습니다 — solid는 시트에 파인 우물이지 색이 들어간 판이 아닙니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 컨트롤 높이와 타입 스케일. 같은 form의 다른 field와 같은 사다리 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음 |
| value | number | null | — | 값. controlled로 쓰려면 onValueChange와 함께 |
| defaultValue | number | — | uncontrolled일 때의 처음 값 |
| onValueChange | (value: number | null) => void | — | 타이핑, 스테퍼, 휠 — 바뀔 때마다 |
| onValueCommitted | (value: number | null) => void | — | 값이 가라앉을 때: 타이핑 후 blur, 누르고 뗐을 때, 키보드에서는 onValueChange와 함께 |
| min | number | — | 범위의 아래끝. 스테퍼가 여기서 멈춥니다 |
| max | number | — | 범위의 위끝 |
| step | number | 'any' | 1 | 한 걸음의 크기. any는 step 검증을 끕니다 |
| largeStep | number | 10 | Shift를 누른 채 밟는 걸음 |
| smallStep | number | 0.1 | Alt를 누른 채 밟는 걸음 |
| snapOnStep | boolean | false | 걸음이 step의 배수에 붙을지 |
| allowWheelScrub | boolean | false | focus된 상태에서 hover 중일 때 휠이 값을 바꿀지. 기본은 꺼짐 — 포인터 아래에서 스크롤되는 페이지와 값이 바뀌는 field는 같은 동작이고, 의도된 것은 둘 중 하나뿐입니다 |
| format | Intl.NumberFormatOptions | — | 숫자를 어떻게 쓸지 — 통화, 퍼센트, 소수 자릿수. Intl.NumberFormat으로 그대로 넘어가므로 화면에는 $1,240.00이 보이고 값은 1240입니다 |
| locale | Intl.LocalesArgument | — | 숫자를 쓰고 읽는 locale. 기본값은 런타임의 것 |
| steppers | 'end' | 'split' | 'none' | 'end' | 스테퍼가 놓이는 자리. 반높이 chevron을 위아래로 쌓는 형태는 일부러 없습니다 — xs에서 화살표 하나가 3px도 안 되고, 그만한 표적은 아무도 맞히지 못합니다 |
| incrementLabel | string | 'Increase' | 증가 버튼의 접근 가능한 이름 |
| decrementLabel | string | 'Decrease' | 감소 버튼의 접근 가능한 이름 |
| label | ReactNode | — | 컨트롤 위의 라벨. Base UI의 Field가 연결합니다. floating 형태는 일부러 없습니다 — floating label에는 transform이 필요합니다 |
| description | ReactNode | — | 컨트롤 아래의 도움말 |
| error | ReactNode | — | 컨트롤 아래의 오류 메시지. 이것이 있으면 field 자체가 invalid가 되고, 색 계열 전체가 danger를 가리킵니다 |
| invalid | boolean | — | 메시지 없이 invalid 상태만 강제합니다. 기본값은 !!error |
| startIcon | ReactNode | — | 숫자 앞에 놓이는 것 — 통화 기호, 단위, 아이콘 |
| endIcon | ReactNode | — | 숫자 뒤, 스테퍼 앞에 놓이는 것 |
| fullWidth공통 | boolean | false | 컨테이너 너비만큼 늘어납니다 |
| readOnly공통 | boolean | false | 값은 보이지만 바꿀 수 없습니다. 스테퍼도 그려지지 않습니다 |
| disabled공통 | boolean | false | 사용할 수 없음 |
| hotKeys | Record<string, () => void> | — | 이 컨트롤이 답할 chord들. PlHotKeys가 그리는 것과 같은 철자입니다 — { 'Mod+Enter': save, Escape: cancel }. 맞는 chord는 **소비됩니다** |
| classNames | { label?, control?, description?, error?: string } | — | className이 닿지 않는 부분에 붙는 class. control은 실제로 조작하는 부분입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | double? | — | 값. controlled로 쓰려면 onValueChange와 함께 |
| onChanged | ValueChanged<double?>? | — | 키를 누를 때마다, 걸음마다, 휠마다. 정착한 값이 아니라 입력된 값을 보고합니다 |
| onCommitted | ValueChanged<double?>? | — | 값이 가라앉을 때: 타이핑 후 blur, 누르고 뗐을 때, 키보드에서는 onValueChange와 함께 |
| min | double? | — | 범위의 아래끝. 스테퍼가 여기서 멈춥니다 |
| max | double? | — | 범위의 위끝 |
| step | double | 1 | 한 걸음의 크기. any는 step 검증을 끕니다 |
| largeStep | double | 10 | Shift를 누른 채 밟는 걸음 |
| smallStep | double | 0.1 | Alt를 누른 채 밟는 걸음 |
| snapOnStep | bool | false | 걸음이 step의 배수에 붙을지 |
| allowWheelScrub | bool | false | focus된 상태에서 hover 중일 때 휠이 값을 바꿀지. 기본은 꺼짐 — 포인터 아래에서 스크롤되는 페이지와 값이 바뀌는 field는 같은 동작이고, 의도된 것은 둘 중 하나뿐입니다 |
| format | String Function(double value)? | — | 정착한 값을 어떻게 쓸지. Dart SDK에는 Intl.NumberFormat이 없으니 서식은 함수입니다 |
| parse | double? Function(String text)? | — | 입력된 글자를 어떻게 되읽을지. 생략하면 숫자와 부호, 소수점을 뺀 나머지를 버립니다 |
| steppers | PlNumberFieldSteppers | PlNumberFieldSteppers.end | 스테퍼가 놓이는 자리. 반높이 chevron을 위아래로 쌓는 형태는 일부러 없습니다 — xs에서 화살표 하나가 3px도 안 되고, 그만한 표적은 아무도 맞히지 못합니다 |
| incrementLabel | String | 'Increase' | 증가 버튼의 접근 가능한 이름 |
| decrementLabel | String | 'Decrease' | 감소 버튼의 접근 가능한 이름 |
| variant공통 | PlassVariant | PlassVariant.glass | 껍데기의 재질. PlTextField와 픽셀 단위로 같습니다 — solid는 시트에 파인 우물이지 색이 들어간 판이 아닙니다 |
| size공통 | PlassSize | PlassSize.md | 컨트롤 높이와 타입 스케일. 같은 form의 다른 field와 같은 사다리 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | int | 0 | 그림자 깊이. 0은 그림자 없음 |
| label | Widget? | — | 컨트롤 위의 라벨. floating 형태는 일부러 없습니다 — floating label은 움직이는 글자입니다 |
| description | Widget? | — | 컨트롤 아래의 도움말 |
| error | Widget? | — | 컨트롤 아래의 오류 메시지. 이것이 있으면 field 자체가 invalid가 되고, 색 계열 전체가 danger를 가리킵니다 |
| invalid | bool? | — | 메시지 없이 invalid 상태만 강제합니다. 기본값은 !!error |
| placeholder | String? | — | 비어 있는 동안 보이는 글자 |
| startIcon | Widget? | — | 숫자 앞에 놓이는 것 — 통화 기호, 단위, 아이콘 |
| endIcon | Widget? | — | 숫자 뒤, 스테퍼 앞에 놓이는 것 |
| fullWidth공통 | bool | false | 컨테이너 너비만큼 늘어납니다 |
| readOnly공통 | bool | false | 값은 보이지만 바꿀 수 없습니다. 스테퍼도 그려지지 않습니다 |
| disabled공통 | bool | false | 사용할 수 없음 |
| semanticLabel | String? | — | field를 스크린 리더가 부를 이름 |
| focusNode | FocusNode? | — | 바깥에서 focus를 몹니다 |
| autofocus | bool | false | 트리에 들어가는 순간 focus를 가져갑니다 |
| hotKeys | PlassHotKeys? | — | 이 컨트롤이 답할 chord들. PlHotKeys가 그리는 것과 같은 철자입니다 — { 'Mod+Enter': save, Escape: cancel }. 맞는 chord는 **소비됩니다** |
네이티브 <div> 속성은 field를 감싸는 요소에 그대로 전달됩니다. color, defaultValue, children은 셋 다 여기서는 Plass의 prop이라 전달 대상에서 제외됩니다.
className은 label과 control, 그 아래 두 줄을 함께 담는 stack에 붙습니다. 그 안쪽 네 부분에 닿는 것이 classNames입니다: label, control(stepper까지 포함한 껍데기), description, error.
패키지의 다른 입력들과 마찬가지로 controlled입니다. value를 받고 값이 무엇이 되어야 하는지를 보고합니다. defaultValue는 없고, value는 double?입니다. null이 빈 상자입니다.
콜백이 하나가 아니라 둘이고, 그 차이가 라이브러리의 다른 어디보다 여기서 중요합니다. onChanged는 키를 누를 때마다 입력된 것과 함께 불리고, onCommitted는 field가 정착할 때 정착한 값과 함께 불립니다. 50으로 가는 길의 5는 10에서 시작하는 범위 밖에 있는 것이 아니라 아직 덜 쓰인 것이라, 범위 고정은 field가 정착할 때까지 기다립니다.
라이브러리 전체에서 공유 축(variant size color density elevation)이 뜻하는 바는 prop 규칙에 있습니다.
Examples
steppers
end는 두 버튼을 뒤쪽 가장자리에 둡니다. spinner가 늘 그래 온 모양입니다. split은 빼기를 앞에, 더하기를 뒤에 두고 숫자를 그 사이에 놓습니다. 타이핑하기보다 툭툭 밀어 올리는 수량을 위한 것입니다. none은 버튼을 빼지만 field는 여전히 숫자 field입니다. 방향키도, 범위 고정도, 서식도 그대로입니다.
반높이 chevron을 위아래로 쌓는 형태는 일부러 없습니다. xs에서 화살표 하나는 3px도 되지 않고, 그만한 표적은 아무도 맞히지 못합니다.
format
Intl.NumberFormat으로 그대로 넘어갑니다. 그래서 화면에는 $1,240.00이나 18.5%가 보이고 value는 평범한 숫자로 남습니다. 입력된 것도 같은 locale로 되읽히는데, 그것이 쉼표가 소수점이어야 할 곳에서 소수점이 되게 하는 이유입니다.
옵션 객체 하나가 아니라 함수 둘입니다. format은 정착한 값을 쓰고 parse는 입력된 글자를 되읽습니다. Dart SDK에는 Intl.NumberFormat이 없고 이 패키지에는 의존성이 없으니, locale을 아는 field는 앱이 자기 formatter로 만드는 것입니다. 어차피 다른 화면에도 필요해서 이미 갖고 있을 그 formatter를 씁니다.
둘 다 생략하면 format은 정수를 소수점 없이 쓰고, parse는 숫자와 부호, 소수점을 뺀 나머지를 전부 버립니다. 통화가 보이는 field에 $1,240.50을 그대로 칠 수 있는 것이 이 기본 짝 덕분입니다.
step, largeStep, smallStep
방향키와 스테퍼는 둘 다 step만큼 움직이고, Shift는 largeStep을, Alt는 smallStep을 씁니다. 수정 키는 눌린 키에도, 눌린 스테퍼에도 똑같이 셉니다. snapOnStep은 한 걸음이 하나만큼 움직이는 대신 배수에 내려앉게 합니다.
Page Up과 Page Down도 largeStep을 쓰고, Home과 End는 min과 max가 있으면 그리로 갑니다. 스테퍼를 누르고 있으면 짧은 정지 뒤에 반복되고, 눌렀다 뗀 스테퍼는 정확히 한 걸음입니다. 라이브러리의 다른 모든 버튼과 같습니다.
variant
껍데기는 PlTextField와 픽셀 단위로 같습니다. 수량 상자만 주변 상자들과 높이나 모서리가 다른 form은 설계된 것이 아니라 조립된 것처럼 보이는 form입니다. 그래서 여기서도 solid는 색이 들어간 판이 아니라 시트에 파인 우물입니다.
상태
readOnly는 숫자를 읽을 수 있게 두고 스테퍼를 없앱니다. 바뀔 수 없는 값에는 누를 것이 없습니다. error는 field 자체를 invalid로 만들고, 그것이 색 계열 전체를 danger로 돌려세워 가장자리와 ring, 캐럿, 메시지가 함께 넘어가게 합니다.
size
Accessibility
- 어려운 부분은 Base UI의 NumberField가 가집니다. locale에 맞춰 입력을 해석하는 것,
min/max로 고정하는 것, 스테퍼를 누르고 있을 때의 반복, form과 함께 제출되는 숨은 input. - 라벨과 설명, 오류는 Base UI의 Field가 컨트롤에 연결하므로 어느 것도 호출하는 쪽의
id를 필요로 하지 않습니다. - 두 스테퍼에는 이미 접근 가능한 이름이 있습니다.
incrementLabel과decrementLabel이 그것을 바꿉니다. - 범위 끝에 닿은 스테퍼는 흐려지기만 하는 것이 아니라 진짜로
disabled입니다. allowWheelScrub은 기본적으로 꺼져 있습니다. 포인터 아래에서 스크롤되는 페이지와 값이 바뀌는 field는 같은 동작이고, 의도된 것은 둘 중 하나뿐입니다.
- field는 보이는 것을 담은 텍스트 field로 읽힙니다. 그래서 스크린리더는
1240이 아니라$1,240.00을 읽습니다. 그려진 것이 읽히는 것입니다. - 두 스테퍼에는 이미 이름이 있습니다.
incrementLabel과decrementLabel이 그것을 바꿉니다. 각각은 숫자 다음의 자기 focus stop이 있습니다. - 범위 끝에 닿은 스테퍼는 흐려지기만 하는 것이 아니라 사용할 수 없다고 읽힙니다.
- 방향키는 field 안쪽에, 앱 자신의 텍스트 편집 단축키보다 편집기에 가까이 묶여 있습니다. 위 방향키가 캐럿이 아니라 숫자를 움직이게 하는 것이 이것입니다.
allowWheelScrub은 기본적으로 꺼져 있고, 켜도 field가 focus가 붙은 동시에 포인터가 그 위에 있어야 합니다. 포인터 아래에서 스크롤되는 페이지와 값이 바뀌는 field는 같은 동작이고, 의도된 것은 둘 중 하나뿐입니다.- 라벨과 설명, 오류는 컴포넌트의 일부라, 연결할
id도 없고 연결하는 것을 잊을 일도 없습니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
value / defaultValue / onValueChange | value / onChanged | Flutter의 컨트롤은 controlled이고, 콜백 이름도 Flutter의 것입니다. |
onValueCommitted | onCommitted | 같은 생각, 더 짧은 이름. 범위 고정이 일어나는 자리입니다. |
Intl.NumberFormatOptions인 format | 함수 둘인 format과 parse | Dart SDK에는 Intl.NumberFormat이 없고 이 패키지에는 의존성이 없습니다. 되읽지 못하는 서식은 입력할 수 없는 field이므로, 양쪽 다 호출하는 쪽의 몫입니다. |
locale | — | 앱이 넘기는 formatter의 것입니다. 그 formatter는 이미 자기가 어느 locale로 쓰는지 압니다. |
number | null 값 | double? | Dart의 부동소수점 타입입니다. 정수 field는 step: 1에 소수를 쓰지 않는 format입니다. |
숨은 input, name, required | — | 함께 제출될 네이티브 form이 없습니다. |
id | — | 여기서는 무엇도 id로 다른 것을 가리키지 않습니다. 라벨과 메시지는 컴포넌트의 일부입니다. |
className, style, 네이티브 속성 | — | 전달할 클래스 목록도 style 속성도 없습니다. |