@ttdk-design-system/library-react 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_GUIDE.md +6518 -0
- package/README.md +106 -2
- package/llms.txt +6518 -0
- package/package.json +1099 -4
- package/src/core/index.ts +2 -0
- package/src/core/tailwind-preset.cjs +62 -0
- package/src/core/tailwind-preset.ts +85 -0
- package/src/core/theme/tokens.css +663 -0
- package/src/core/types.ts +836 -0
- package/src/index.ts +17 -0
- package/src/tailwind-preset.cjs +62 -0
- package/src/tailwind-preset.ts +85 -0
- package/src/types.ts +1 -0
- package/src/web/accordion/AI_GUIDE.md +149 -0
- package/src/web/accordion/accordion.css +1 -0
- package/src/web/accordion/accordion.tsx +430 -0
- package/src/web/accordion/index.ts +3 -0
- package/src/web/accordion/manifest.json +351 -0
- package/src/web/alert/AI_GUIDE.md +139 -0
- package/src/web/alert/alert.css +1 -0
- package/src/web/alert/alert.tsx +318 -0
- package/src/web/alert/index.ts +3 -0
- package/src/web/alert/manifest.json +240 -0
- package/src/web/avatar/AI_GUIDE.md +118 -0
- package/src/web/avatar/avatar.css +1 -0
- package/src/web/avatar/avatar.tsx +233 -0
- package/src/web/avatar/index.ts +3 -0
- package/src/web/avatar/manifest.json +195 -0
- package/src/web/badge/AI_GUIDE.md +94 -0
- package/src/web/badge/badge.css +1 -0
- package/src/web/badge/badge.tsx +140 -0
- package/src/web/badge/index.ts +2 -0
- package/src/web/badge/manifest.json +141 -0
- package/src/web/banner/AI_GUIDE.md +130 -0
- package/src/web/banner/banner.css +1 -0
- package/src/web/banner/banner.tsx +334 -0
- package/src/web/banner/index.ts +3 -0
- package/src/web/banner/manifest.json +240 -0
- package/src/web/breadcrumb/AI_GUIDE.md +120 -0
- package/src/web/breadcrumb/breadcrumb.css +1 -0
- package/src/web/breadcrumb/breadcrumb.tsx +306 -0
- package/src/web/breadcrumb/index.ts +3 -0
- package/src/web/breadcrumb/manifest.json +225 -0
- package/src/web/button/AI_GUIDE.md +121 -0
- package/src/web/button/button.css +1 -0
- package/src/web/button/button.tsx +287 -0
- package/src/web/button/index.ts +2 -0
- package/src/web/button/manifest.json +177 -0
- package/src/web/calendar/AI_GUIDE.md +151 -0
- package/src/web/calendar/calendar.css +1 -0
- package/src/web/calendar/calendar.tsx +1611 -0
- package/src/web/calendar/index.ts +3 -0
- package/src/web/calendar/manifest.json +470 -0
- package/src/web/card/AI_GUIDE.md +133 -0
- package/src/web/card/card.css +1 -0
- package/src/web/card/card.tsx +408 -0
- package/src/web/card/index.ts +4 -0
- package/src/web/card/manifest.json +357 -0
- package/src/web/checkbox/AI_GUIDE.md +146 -0
- package/src/web/checkbox/checkbox.css +1 -0
- package/src/web/checkbox/checkbox.tsx +307 -0
- package/src/web/checkbox/index.ts +2 -0
- package/src/web/checkbox/manifest.json +294 -0
- package/src/web/chip/AI_GUIDE.md +186 -0
- package/src/web/chip/chip.css +1 -0
- package/src/web/chip/chip.tsx +230 -0
- package/src/web/chip/index.ts +2 -0
- package/src/web/chip/manifest.json +186 -0
- package/src/web/dropdown/AI_GUIDE.md +167 -0
- package/src/web/dropdown/dropdown.css +1 -0
- package/src/web/dropdown/dropdown.tsx +1274 -0
- package/src/web/dropdown/index.ts +3 -0
- package/src/web/dropdown/manifest.json +576 -0
- package/src/web/flex/AI_GUIDE.md +113 -0
- package/src/web/flex/flex-item.tsx +97 -0
- package/src/web/flex/flex.css +1 -0
- package/src/web/flex/flex.tsx +108 -0
- package/src/web/flex/index.ts +5 -0
- package/src/web/flex/manifest.json +162 -0
- package/src/web/form/index.ts +7 -0
- package/src/web/grid/AI_GUIDE.md +112 -0
- package/src/web/grid/grid-item.tsx +112 -0
- package/src/web/grid/grid.css +1 -0
- package/src/web/grid/grid.tsx +99 -0
- package/src/web/grid/index.ts +5 -0
- package/src/web/grid/manifest.json +153 -0
- package/src/web/index.ts +86 -0
- package/src/web/input/AI_GUIDE.md +159 -0
- package/src/web/input/index.ts +3 -0
- package/src/web/input/input.css +1 -0
- package/src/web/input/input.tsx +537 -0
- package/src/web/input/manifest.json +465 -0
- package/src/web/input-number/AI_GUIDE.md +164 -0
- package/src/web/input-number/index.ts +3 -0
- package/src/web/input-number/input-number.css +1 -0
- package/src/web/input-number/input-number.tsx +1193 -0
- package/src/web/input-number/manifest.json +575 -0
- package/src/web/modal/AI_GUIDE.md +171 -0
- package/src/web/modal/index.ts +3 -0
- package/src/web/modal/manifest.json +375 -0
- package/src/web/modal/modal.css +1 -0
- package/src/web/modal/modal.tsx +571 -0
- package/src/web/pagination/AI_GUIDE.md +148 -0
- package/src/web/pagination/index.ts +21 -0
- package/src/web/pagination/manifest.json +432 -0
- package/src/web/pagination/pagination-content.tsx +45 -0
- package/src/web/pagination/pagination-ellipsis.tsx +54 -0
- package/src/web/pagination/pagination-first.tsx +155 -0
- package/src/web/pagination/pagination-item.tsx +45 -0
- package/src/web/pagination/pagination-last.tsx +155 -0
- package/src/web/pagination/pagination-link.tsx +121 -0
- package/src/web/pagination/pagination-next.tsx +158 -0
- package/src/web/pagination/pagination-previous.tsx +158 -0
- package/src/web/pagination/pagination.css +1 -0
- package/src/web/pagination/pagination.tsx +687 -0
- package/src/web/popover/AI_GUIDE.md +163 -0
- package/src/web/popover/index.ts +3 -0
- package/src/web/popover/manifest.json +396 -0
- package/src/web/popover/popover.css +1 -0
- package/src/web/popover/popover.tsx +702 -0
- package/src/web/radio/AI_GUIDE.md +155 -0
- package/src/web/radio/index.ts +2 -0
- package/src/web/radio/manifest.json +294 -0
- package/src/web/radio/radio.css +1 -0
- package/src/web/radio/radio.tsx +290 -0
- package/src/web/selectbox/AI_GUIDE.md +216 -0
- package/src/web/selectbox/index.ts +3 -0
- package/src/web/selectbox/manifest.json +477 -0
- package/src/web/selectbox/selectbox.css +1 -0
- package/src/web/selectbox/selectbox.tsx +1048 -0
- package/src/web/sonner/AI_GUIDE.md +98 -0
- package/src/web/sonner/index.ts +114 -0
- package/src/web/sonner/manifest.json +132 -0
- package/src/web/sonner/sonner.css +1 -0
- package/src/web/sonner/sonner.tsx +177 -0
- package/src/web/table/AI_GUIDE.md +145 -0
- package/src/web/table/index.ts +35 -0
- package/src/web/table/manifest.json +168 -0
- package/src/web/table/table-body.tsx +45 -0
- package/src/web/table/table-caption.tsx +45 -0
- package/src/web/table/table-cell.tsx +74 -0
- package/src/web/table/table-empty.tsx +108 -0
- package/src/web/table/table-footer.tsx +45 -0
- package/src/web/table/table-head.tsx +80 -0
- package/src/web/table/table-header.tsx +45 -0
- package/src/web/table/table-row.tsx +69 -0
- package/src/web/table/table.css +1 -0
- package/src/web/table/table.tsx +305 -0
- package/src/web/tabs/AI_GUIDE.md +150 -0
- package/src/web/tabs/index.ts +3 -0
- package/src/web/tabs/manifest.json +315 -0
- package/src/web/tabs/tabs.css +1 -0
- package/src/web/tabs/tabs.tsx +332 -0
- package/src/web/text/AI_GUIDE.md +129 -0
- package/src/web/text/index.ts +3 -0
- package/src/web/text/manifest.json +195 -0
- package/src/web/text/text.css +1 -0
- package/src/web/text/text.tsx +689 -0
- package/src/web/textarea/AI_GUIDE.md +141 -0
- package/src/web/textarea/index.ts +3 -0
- package/src/web/textarea/manifest.json +438 -0
- package/src/web/textarea/textarea.css +1 -0
- package/src/web/textarea/textarea.tsx +467 -0
- package/src/web/tooltip/AI_GUIDE.md +110 -0
- package/src/web/tooltip/index.ts +3 -0
- package/src/web/tooltip/manifest.json +222 -0
- package/src/web/tooltip/tooltip.css +1 -0
- package/src/web/tooltip/tooltip.tsx +423 -0
- package/src/webview/bottomsheet/AI_GUIDE.md +141 -0
- package/src/webview/bottomsheet/bottomsheet.css +1 -0
- package/src/webview/bottomsheet/bottomsheet.tsx +603 -0
- package/src/webview/bottomsheet/index.ts +4 -0
- package/src/webview/bottomsheet/manifest.json +377 -0
- package/src/webview/button/AI_GUIDE.md +79 -0
- package/src/webview/button/button-ios.css +1 -0
- package/src/webview/button/button-ios.tsx +138 -0
- package/src/webview/button/button-material.css +1 -0
- package/src/webview/button/button-material.tsx +138 -0
- package/src/webview/button/index.ts +7 -0
- package/src/webview/button/manifest.json +148 -0
- package/src/webview/index.ts +7 -0
- package/tailwind-preset.cjs +62 -0
- package/tailwind-preset.js +62 -0
- package/ttdk-manifest.json +10188 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState, forwardRef } from "react";
|
|
4
|
+
|
|
5
|
+
export type InputNumberUiProps = UiProps;
|
|
6
|
+
export interface InputNumberProps {
|
|
7
|
+
/**
|
|
8
|
+
* Teks label judul field yang ditampilkan di atas input (14px Semibold, #141313).
|
|
9
|
+
*/
|
|
10
|
+
label?: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Style kustom untuk elemen label judul field (UiProps).
|
|
14
|
+
*/
|
|
15
|
+
labelStyle?: UiProps;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Class CSS kustom untuk elemen label judul field.
|
|
19
|
+
*/
|
|
20
|
+
labelClass?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Style kustom untuk elemen kontainer input wrapper (UiProps).
|
|
24
|
+
*/
|
|
25
|
+
wrapperStyle?: UiProps;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Class CSS kustom untuk elemen kontainer input wrapper.
|
|
29
|
+
*/
|
|
30
|
+
wrapperClass?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Style kustom untuk elemen input control HTML (UiProps).
|
|
34
|
+
*/
|
|
35
|
+
inputStyle?: UiProps;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Class CSS kustom untuk elemen input control HTML.
|
|
39
|
+
*/
|
|
40
|
+
inputClass?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Style kustom untuk elemen stepper kontrol tombol (UiProps).
|
|
44
|
+
*/
|
|
45
|
+
controlsStyle?: UiProps;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Class CSS kustom untuk elemen stepper kontrol tombol.
|
|
49
|
+
*/
|
|
50
|
+
controlsClass?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Style kustom untuk elemen pesan error validasi (UiProps).
|
|
54
|
+
*/
|
|
55
|
+
errorMessageStyle?: UiProps;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Class CSS kustom untuk elemen pesan error validasi.
|
|
59
|
+
*/
|
|
60
|
+
errorMessageClass?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Style kustom untuk elemen helper text (UiProps).
|
|
64
|
+
*/
|
|
65
|
+
helperTextStyle?: UiProps;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Class CSS kustom untuk elemen helper text.
|
|
69
|
+
*/
|
|
70
|
+
helperTextClass?: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Menandai field sebagai opsi opsional dengan menambahkan teks '(Opsional)' (12px Regular, #9698A2).
|
|
74
|
+
*/
|
|
75
|
+
optional?: boolean;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Kustomisasi teks keterangan opsional.
|
|
79
|
+
* @default '(Opsional)'
|
|
80
|
+
*/
|
|
81
|
+
optionalText?: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Nilai teks placeholder saat input kosong (#9698A2).
|
|
85
|
+
*/
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Nilai terkontrol (controlled value) pada input number.
|
|
90
|
+
*/
|
|
91
|
+
value?: number | string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Nilai awal tidak terkontrol (uncontrolled initial value).
|
|
95
|
+
*/
|
|
96
|
+
defaultValue?: number | string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Nama form field HTML untuk integrasi formulir.
|
|
100
|
+
*/
|
|
101
|
+
name?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Identifier unik elemen input.
|
|
105
|
+
*/
|
|
106
|
+
id?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Status apakah input sedang dalam kondisi error / tidak valid.
|
|
110
|
+
* @default false
|
|
111
|
+
*/
|
|
112
|
+
error?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pesan teks penjelasan kesalahan validasi di bawah field.
|
|
116
|
+
*/
|
|
117
|
+
errorMessage?: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Teks petunjuk atau keterangan bantuan di bawah field (12px Regular, #3C4245).
|
|
121
|
+
*/
|
|
122
|
+
helperText?: string;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Posisi teks helper
|
|
126
|
+
*/
|
|
127
|
+
helperTextPosition?: "top" | "bottom";
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Skala ukuran input field.
|
|
131
|
+
* - `'sm'`: 38px
|
|
132
|
+
* - `'md'`: 48px (standar)
|
|
133
|
+
* - `'lg'`: 56px
|
|
134
|
+
* @default 'md'
|
|
135
|
+
*/
|
|
136
|
+
size?: "sm" | "md" | "lg";
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Bentuk sudut kelengkungan input.
|
|
140
|
+
* - `'none'`: 0px
|
|
141
|
+
* - `'sm'`: 4px
|
|
142
|
+
* - `'md'`: 8px (standar desain)
|
|
143
|
+
* - `'lg'`: 12px
|
|
144
|
+
* - `'full'`: 9999px (kapsul)
|
|
145
|
+
* @default 'md'
|
|
146
|
+
*/
|
|
147
|
+
rounded?: "none" | "sm" | "md" | "lg" | "full";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Menjadikan lebar field input memenuhi kontainer penuh (full-width 100%).
|
|
151
|
+
* @default false
|
|
152
|
+
*/
|
|
153
|
+
block?: boolean;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Status apakah field dinonaktifkan (disabled).
|
|
157
|
+
* @default false
|
|
158
|
+
*/
|
|
159
|
+
disabled?: boolean;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Status apakah field bersifat hanya-baca (readonly).
|
|
163
|
+
* @default false
|
|
164
|
+
*/
|
|
165
|
+
readOnly?: boolean;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Menandai field sebagai input wajib diisi (menampilkan tanda bintang merah *).
|
|
169
|
+
* @default false
|
|
170
|
+
*/
|
|
171
|
+
required?: boolean;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ikon di sebelah kiri dalam input (ukuran 20-24px, misal: Hash, Calculator, Currency).
|
|
175
|
+
*/
|
|
176
|
+
iconLeft?: any;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Ikon di sebelah kanan dalam input (ukuran 20-24px).
|
|
180
|
+
*/
|
|
181
|
+
iconRight?: any;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Addon / teks prefix di sebelah kiri luar input (misal: 'Rp', '+62').
|
|
185
|
+
*/
|
|
186
|
+
addonLeft?: string | any;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Addon / teks suffix di sebelah kanan luar input (misal: 'kg', 'pcs', '%').
|
|
190
|
+
*/
|
|
191
|
+
addonRight?: string | any;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Konfigurasi atribut autocomplete HTML.
|
|
195
|
+
*/
|
|
196
|
+
autoComplete?: string;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Nilai minimum yang diizinkan untuk input number.
|
|
200
|
+
*/
|
|
201
|
+
min?: number;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Nilai maksimum yang diizinkan untuk input number.
|
|
205
|
+
*/
|
|
206
|
+
max?: number;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Nilai selisih kenaikan atau penurunan setiap kali stepper ditekan.
|
|
210
|
+
* @default 1
|
|
211
|
+
*/
|
|
212
|
+
step?: number;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Jumlah angka di belakang koma (presisi desimal).
|
|
216
|
+
*/
|
|
217
|
+
precision?: number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mengaktifkan format pemisah ribuan otomatis dengan titik '.' (contoh: 1.000 atau 10.000).
|
|
221
|
+
* Nilai yang diteruskan ke callback form (onChange / onValueChange) tetap berupa number murni standar (contoh: 1000).
|
|
222
|
+
* @default false
|
|
223
|
+
*/
|
|
224
|
+
thousandSeparator?: boolean;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Mengizinkan tanda eksponensial ilmiah 'e' / 'E'.
|
|
228
|
+
* @default false
|
|
229
|
+
*/
|
|
230
|
+
allowExponential?: boolean;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Tipe input HTML. Secara default 'text' dengan inputMode 'numeric'/'decimal' agar mendukung formatting dan pembatasan karakter tanpa bug bawaan browser.
|
|
234
|
+
* @default 'text'
|
|
235
|
+
*/
|
|
236
|
+
type?: string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Menampilkan tombol stepper panah kontrol atas/bawah.
|
|
240
|
+
* @default true
|
|
241
|
+
*/
|
|
242
|
+
controls?: boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Menghilangkan tombol kontrol increment dan decrement sehingga tampil seperti input teks biasa.
|
|
246
|
+
* @default false
|
|
247
|
+
*/
|
|
248
|
+
withoutButton?: boolean;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Posisi letak tombol kontrol stepper:
|
|
252
|
+
* - `'right'`: tombol stepper ditumpuk di sisi kanan (standar)
|
|
253
|
+
* - `'split'`: tombol minus di sisi kiri dan tombol plus di sisi kanan (mode counter)
|
|
254
|
+
* @default 'right'
|
|
255
|
+
*/
|
|
256
|
+
controlsPosition?: "right" | "split";
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Batas maksimum panjang karakter.
|
|
260
|
+
*/
|
|
261
|
+
maxLength?: number;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Batas minimum panjang karakter.
|
|
265
|
+
*/
|
|
266
|
+
minLength?: number;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Inline CSS style object native.
|
|
270
|
+
*/
|
|
271
|
+
style?: React.CSSProperties;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Kustomisasi visual granular root / form-item (UiProps).
|
|
275
|
+
*/
|
|
276
|
+
ui?: UiProps;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Callback event saat nilai input berubah (SyntheticEvent standar HTML).
|
|
280
|
+
*/
|
|
281
|
+
onChange?: (event: any) => void;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Callback event saat nilai numerik angka berubah.
|
|
285
|
+
*/
|
|
286
|
+
onValueChange?: (value: number | undefined) => void;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Callback event saat pengguna mengetik (input event).
|
|
290
|
+
*/
|
|
291
|
+
onInput?: (event: any) => void;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Callback event saat field input kehilangan fokus.
|
|
295
|
+
*/
|
|
296
|
+
onBlur?: (event: any) => void;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Callback event saat field input mendapatkan fokus.
|
|
300
|
+
*/
|
|
301
|
+
onFocus?: (event: any) => void;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Ref instance elemen input HTML untuk akses kontrol DOM langsung (focus, blur, select).
|
|
305
|
+
*/
|
|
306
|
+
inputRef?: any;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Class CSS kustom.
|
|
310
|
+
*/
|
|
311
|
+
className?: string;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Callback event tombol keyboard ditekan.
|
|
315
|
+
*/
|
|
316
|
+
onKeyDown?: (event: any) => void;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Callback event tombol keyboard dilepaskan.
|
|
320
|
+
*/
|
|
321
|
+
onKeyUp?: (event: any) => void;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Callback event saat teks di-copy.
|
|
325
|
+
*/
|
|
326
|
+
onCopy?: (event: any) => void;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Callback event saat teks di-cut.
|
|
330
|
+
*/
|
|
331
|
+
onCut?: (event: any) => void;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Menampilkan spinner dan menonaktifkan interaksi field sementara menunggu proses async.
|
|
335
|
+
* @default false
|
|
336
|
+
*/
|
|
337
|
+
loading?: boolean;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Komponen InputNumber TTDK sesuai spesifikasi Anatomy Desain Form & Input:
|
|
342
|
+
* 1. Field title / label (14px Semibold, #141313)
|
|
343
|
+
* 2. (Opsional) label (12px Regular, #9698A2)
|
|
344
|
+
* 3. Border radius 8px default (rounded)
|
|
345
|
+
* 4. Stepper controls (+/- spin buttons) dengan mode right-stacked atau split-counter
|
|
346
|
+
* 5. Input control numerik terproteksi min, max, step, & precision
|
|
347
|
+
* 6. Additional information / Helper text & error message
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
import { buildUiVars, UiProps } from "../../core/types";
|
|
351
|
+
|
|
352
|
+
const InputNumber = forwardRef<any, InputNumberProps>(
|
|
353
|
+
function InputNumber(props: InputNumberProps, inputRef) {
|
|
354
|
+
const [innerValue, setInnerValue] = useState(() =>
|
|
355
|
+
props.value !== undefined
|
|
356
|
+
? props.value
|
|
357
|
+
: props.defaultValue !== undefined
|
|
358
|
+
? props.defaultValue
|
|
359
|
+
: ""
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
function shouldShowControls() {
|
|
363
|
+
if (props.withoutButton) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return props.controls !== false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function getStep() {
|
|
370
|
+
return props.step !== undefined ? props.step : 1;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getPrecision() {
|
|
374
|
+
if (props.precision !== undefined) {
|
|
375
|
+
return props.precision;
|
|
376
|
+
}
|
|
377
|
+
if (props.step !== undefined) {
|
|
378
|
+
const stepVal = props.step;
|
|
379
|
+
const stepStr = String(stepVal);
|
|
380
|
+
const dotIdx = stepStr.indexOf(".");
|
|
381
|
+
return dotIdx === -1 ? 0 : stepStr.length - dotIdx - 1;
|
|
382
|
+
}
|
|
383
|
+
return -1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function getThousandSep() {
|
|
387
|
+
return props.thousandSeparator ? "." : "";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getInputType() {
|
|
391
|
+
if (props.type) return props.type;
|
|
392
|
+
return "text";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getInputMode() {
|
|
396
|
+
return getPrecision() === 0 ? "numeric" : "decimal";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseToRawNumber(displayVal: any) {
|
|
400
|
+
if (
|
|
401
|
+
displayVal === undefined ||
|
|
402
|
+
displayVal === null ||
|
|
403
|
+
displayVal === ""
|
|
404
|
+
) {
|
|
405
|
+
return "";
|
|
406
|
+
}
|
|
407
|
+
if (typeof displayVal === "number") {
|
|
408
|
+
return String(displayVal);
|
|
409
|
+
}
|
|
410
|
+
let s = String(displayVal).trim();
|
|
411
|
+
if (s === "-" || s === "") return s;
|
|
412
|
+
let isNegative = false;
|
|
413
|
+
if (s.startsWith("-")) {
|
|
414
|
+
isNegative = true;
|
|
415
|
+
s = s.slice(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Hapus titik pemisah ribuan
|
|
419
|
+
s = s.replace(/\./g, "");
|
|
420
|
+
|
|
421
|
+
// Ubah koma desimal menjadi titik desimal standar
|
|
422
|
+
s = s.replace(/,/g, ".");
|
|
423
|
+
|
|
424
|
+
// Hapus karakter selain digit dan titik
|
|
425
|
+
s = s.replace(/[^0-9.]/g, "");
|
|
426
|
+
|
|
427
|
+
// Pastikan maksimal 1 titik desimal
|
|
428
|
+
const dotIndex = s.indexOf(".");
|
|
429
|
+
if (dotIndex !== -1) {
|
|
430
|
+
s = s.slice(0, dotIndex + 1) + s.slice(dotIndex + 1).replace(/\./g, "");
|
|
431
|
+
}
|
|
432
|
+
return (isNegative ? "-" : "") + s;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function formatValue(rawVal: any) {
|
|
436
|
+
if (rawVal === undefined || rawVal === null || rawVal === "") {
|
|
437
|
+
return "";
|
|
438
|
+
}
|
|
439
|
+
const strVal = String(rawVal);
|
|
440
|
+
if (strVal === "-") return "-";
|
|
441
|
+
const thSep = getThousandSep();
|
|
442
|
+
let isNegative = false;
|
|
443
|
+
let clean = strVal;
|
|
444
|
+
if (clean.startsWith("-")) {
|
|
445
|
+
isNegative = true;
|
|
446
|
+
clean = clean.slice(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Pisahkan bagian integer dan desimal
|
|
450
|
+
let intPart = clean;
|
|
451
|
+
let decPart = "";
|
|
452
|
+
let hasDecimal = false;
|
|
453
|
+
if (clean.includes(".")) {
|
|
454
|
+
hasDecimal = true;
|
|
455
|
+
const parts = clean.split(".");
|
|
456
|
+
intPart = parts[0];
|
|
457
|
+
decPart = parts.slice(1).join("");
|
|
458
|
+
} else if (clean.includes(",")) {
|
|
459
|
+
hasDecimal = true;
|
|
460
|
+
const parts = clean.split(",");
|
|
461
|
+
intPart = parts[0];
|
|
462
|
+
decPart = parts.slice(1).join("");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Format ribuan pada integer part dengan titik jika thousandSeparator aktif
|
|
466
|
+
const formattedInt = thSep
|
|
467
|
+
? intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thSep)
|
|
468
|
+
: intPart;
|
|
469
|
+
let result =
|
|
470
|
+
(isNegative ? "-" : "") + (formattedInt || (hasDecimal ? "0" : ""));
|
|
471
|
+
if (hasDecimal) {
|
|
472
|
+
result += "," + decPart;
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getRawValue() {
|
|
478
|
+
// 1. Controlled mode: jika prop value diberikan ke komponen
|
|
479
|
+
if (props.value !== undefined) {
|
|
480
|
+
// Jika user sedang mengetik tanda minus murni ('-'), pertahankan!
|
|
481
|
+
if (innerValue === "-") {
|
|
482
|
+
return "-";
|
|
483
|
+
}
|
|
484
|
+
if (
|
|
485
|
+
props.value === undefined ||
|
|
486
|
+
props.value === null ||
|
|
487
|
+
props.value === ""
|
|
488
|
+
) {
|
|
489
|
+
return "";
|
|
490
|
+
}
|
|
491
|
+
// Pertahankan tanda minus atau titik desimal saat sedang diketik (misal '12,' atau '12,0' atau '-')
|
|
492
|
+
if (
|
|
493
|
+
typeof innerValue === "string" &&
|
|
494
|
+
(innerValue.includes(".") ||
|
|
495
|
+
innerValue.includes(",") ||
|
|
496
|
+
innerValue === "-")
|
|
497
|
+
) {
|
|
498
|
+
const innerNum = Number(parseToRawNumber(innerValue));
|
|
499
|
+
const propNum = Number(props.value);
|
|
500
|
+
if (innerNum === propNum) {
|
|
501
|
+
return innerValue;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return props.value;
|
|
505
|
+
}
|
|
506
|
+
// 2. Uncontrolled mode: gunakan state.innerValue
|
|
507
|
+
return innerValue !== undefined && innerValue !== null ? innerValue : "";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getDisplayValue() {
|
|
511
|
+
const raw = getRawValue();
|
|
512
|
+
if (raw === undefined || raw === null || raw === "") return "";
|
|
513
|
+
if (raw === "-") return "-";
|
|
514
|
+
return formatValue(raw);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function getCurrentNumber() {
|
|
518
|
+
const raw = getRawValue();
|
|
519
|
+
if (raw === undefined || raw === null || raw === "") return undefined;
|
|
520
|
+
const parsed = parseToRawNumber(raw);
|
|
521
|
+
if (parsed === "" || parsed === "-") return undefined;
|
|
522
|
+
const num = Number(parsed);
|
|
523
|
+
return isNaN(num) ? undefined : num;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function canIncrement() {
|
|
527
|
+
if (props.disabled || props.readOnly || props.loading) return false;
|
|
528
|
+
const num = getCurrentNumber();
|
|
529
|
+
if (props.max !== undefined && num !== undefined) {
|
|
530
|
+
return num < props.max;
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function canDecrement() {
|
|
536
|
+
if (props.disabled || props.readOnly || props.loading) return false;
|
|
537
|
+
const num = getCurrentNumber();
|
|
538
|
+
if (props.min !== undefined && num !== undefined) {
|
|
539
|
+
return num > props.min;
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function clamp(val: number) {
|
|
545
|
+
let result = val;
|
|
546
|
+
if (props.min !== undefined && result < props.min) {
|
|
547
|
+
result = props.min;
|
|
548
|
+
}
|
|
549
|
+
if (props.max !== undefined && result > props.max) {
|
|
550
|
+
result = props.max;
|
|
551
|
+
}
|
|
552
|
+
const prec = getPrecision();
|
|
553
|
+
if (prec > 0) {
|
|
554
|
+
const factor = Math.pow(10, prec);
|
|
555
|
+
result = Math.round(result * factor) / factor;
|
|
556
|
+
} else if (prec === 0 && props.precision === 0) {
|
|
557
|
+
result = Math.round(result);
|
|
558
|
+
}
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function emitValue(nextVal: number | string) {
|
|
563
|
+
setInnerValue(nextVal);
|
|
564
|
+
const rawParsed =
|
|
565
|
+
typeof nextVal === "string" ? parseToRawNumber(nextVal) : nextVal;
|
|
566
|
+
const numVal =
|
|
567
|
+
typeof rawParsed === "number"
|
|
568
|
+
? rawParsed
|
|
569
|
+
: rawParsed === "" || rawParsed === "-"
|
|
570
|
+
? undefined
|
|
571
|
+
: Number(rawParsed);
|
|
572
|
+
const safeNumVal =
|
|
573
|
+
numVal !== undefined && isNaN(numVal) ? undefined : numVal;
|
|
574
|
+
if (props.onValueChange) {
|
|
575
|
+
props.onValueChange(safeNumVal);
|
|
576
|
+
}
|
|
577
|
+
if (props.onChange) {
|
|
578
|
+
const syntheticEvent = {
|
|
579
|
+
target: {
|
|
580
|
+
value: rawParsed,
|
|
581
|
+
valueAsNumber: safeNumVal,
|
|
582
|
+
name: props.name,
|
|
583
|
+
id: props.id,
|
|
584
|
+
},
|
|
585
|
+
currentTarget: {
|
|
586
|
+
value: rawParsed,
|
|
587
|
+
valueAsNumber: safeNumVal,
|
|
588
|
+
},
|
|
589
|
+
type: "change",
|
|
590
|
+
value: rawParsed,
|
|
591
|
+
};
|
|
592
|
+
props.onChange(syntheticEvent);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function stepUp() {
|
|
597
|
+
if (!canIncrement()) return;
|
|
598
|
+
const current = getCurrentNumber();
|
|
599
|
+
const step = getStep();
|
|
600
|
+
const base =
|
|
601
|
+
current !== undefined
|
|
602
|
+
? current
|
|
603
|
+
: props.min !== undefined
|
|
604
|
+
? props.min
|
|
605
|
+
: 0;
|
|
606
|
+
const next = clamp(base + step);
|
|
607
|
+
emitValue(next);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function stepDown() {
|
|
611
|
+
if (!canDecrement()) return;
|
|
612
|
+
const current = getCurrentNumber();
|
|
613
|
+
const step = getStep();
|
|
614
|
+
const base =
|
|
615
|
+
current !== undefined
|
|
616
|
+
? current
|
|
617
|
+
: props.min !== undefined
|
|
618
|
+
? props.min
|
|
619
|
+
: 0;
|
|
620
|
+
const next = clamp(base - step);
|
|
621
|
+
emitValue(next);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function handleInput(e: any) {
|
|
625
|
+
const input = e.target;
|
|
626
|
+
const inputValue = input ? input.value : "";
|
|
627
|
+
const raw = parseToRawNumber(inputValue);
|
|
628
|
+
if (getThousandSep()) {
|
|
629
|
+
const cursorPos = (input && input.selectionEnd) || inputValue.length;
|
|
630
|
+
let validCharsBeforeCursor = 0;
|
|
631
|
+
for (let i = 0; i < cursorPos; i++) {
|
|
632
|
+
const ch = inputValue[i];
|
|
633
|
+
if (/\d/.test(ch) || ch === "-" || ch === ",") {
|
|
634
|
+
validCharsBeforeCursor++;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const formatted = formatValue(raw);
|
|
638
|
+
setInnerValue(raw);
|
|
639
|
+
emitValue(raw);
|
|
640
|
+
if (input && typeof input.setSelectionRange === "function") {
|
|
641
|
+
let newCursorPos = 0;
|
|
642
|
+
let count = 0;
|
|
643
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
644
|
+
const ch = formatted[i];
|
|
645
|
+
if (/\d/.test(ch) || ch === "-" || ch === ",") {
|
|
646
|
+
count++;
|
|
647
|
+
}
|
|
648
|
+
if (count >= validCharsBeforeCursor) {
|
|
649
|
+
newCursorPos = i + 1;
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (count < validCharsBeforeCursor) {
|
|
654
|
+
newCursorPos = formatted.length;
|
|
655
|
+
}
|
|
656
|
+
input.value = formatted;
|
|
657
|
+
try {
|
|
658
|
+
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
// ignore
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
setInnerValue(raw);
|
|
665
|
+
emitValue(raw);
|
|
666
|
+
}
|
|
667
|
+
if (props.onInput) {
|
|
668
|
+
props.onInput(e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function handleChange(e: any) {
|
|
673
|
+
handleInput(e);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function handleBlur(e: any) {
|
|
677
|
+
const current = getCurrentNumber();
|
|
678
|
+
if (current !== undefined) {
|
|
679
|
+
const clamped = clamp(current);
|
|
680
|
+
if (clamped !== current) {
|
|
681
|
+
emitValue(clamped);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (props.onBlur) {
|
|
685
|
+
props.onBlur(e);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function handleCopy(e: any) {
|
|
690
|
+
const input = e.target;
|
|
691
|
+
if (!input) return;
|
|
692
|
+
const start = input.selectionStart;
|
|
693
|
+
const end = input.selectionEnd;
|
|
694
|
+
if (start === null || end === null || start === end) return;
|
|
695
|
+
const selectedText = input.value.substring(start, end);
|
|
696
|
+
if (!selectedText) return;
|
|
697
|
+
|
|
698
|
+
// Hilangkan titik separator ribuan, desimal koma dipertahankan
|
|
699
|
+
const cleanText = selectedText.replace(/\./g, "");
|
|
700
|
+
if (e.clipboardData) {
|
|
701
|
+
e.clipboardData.setData("text/plain", cleanText);
|
|
702
|
+
e.preventDefault();
|
|
703
|
+
}
|
|
704
|
+
if (props.onCopy) {
|
|
705
|
+
props.onCopy(e);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function handleCut(e: any) {
|
|
710
|
+
if (props.disabled || props.readOnly) {
|
|
711
|
+
e.preventDefault();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const input = e.target;
|
|
715
|
+
if (!input) return;
|
|
716
|
+
const start = input.selectionStart;
|
|
717
|
+
const end = input.selectionEnd;
|
|
718
|
+
if (start === null || end === null || start === end) return;
|
|
719
|
+
const selectedText = input.value.substring(start, end);
|
|
720
|
+
if (!selectedText) return;
|
|
721
|
+
|
|
722
|
+
// Hilangkan titik separator ribuan, desimal koma dipertahankan
|
|
723
|
+
const cleanText = selectedText.replace(/\./g, "");
|
|
724
|
+
if (e.clipboardData) {
|
|
725
|
+
e.clipboardData.setData("text/plain", cleanText);
|
|
726
|
+
e.preventDefault();
|
|
727
|
+
}
|
|
728
|
+
const val = input.value || "";
|
|
729
|
+
const nextVal = val.slice(0, start) + val.slice(end);
|
|
730
|
+
const raw = parseToRawNumber(nextVal);
|
|
731
|
+
setInnerValue(raw);
|
|
732
|
+
emitValue(raw);
|
|
733
|
+
const formatted = formatValue(raw);
|
|
734
|
+
input.value = formatted;
|
|
735
|
+
try {
|
|
736
|
+
input.setSelectionRange(start, start);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
// ignore
|
|
739
|
+
}
|
|
740
|
+
if (props.onCut) {
|
|
741
|
+
props.onCut(e);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function handleKeyDown(e: any) {
|
|
746
|
+
// 1. Izinkan navigasi, editing, copy-paste, dan shortcut keyboard
|
|
747
|
+
if (
|
|
748
|
+
e.key === "Backspace" ||
|
|
749
|
+
e.key === "Delete" ||
|
|
750
|
+
e.key === "Tab" ||
|
|
751
|
+
e.key === "Escape" ||
|
|
752
|
+
e.key === "Enter" ||
|
|
753
|
+
e.key === "ArrowLeft" ||
|
|
754
|
+
e.key === "ArrowRight" ||
|
|
755
|
+
e.key === "Home" ||
|
|
756
|
+
e.key === "End" ||
|
|
757
|
+
e.key === "PageUp" ||
|
|
758
|
+
e.key === "PageDown" ||
|
|
759
|
+
e.ctrlKey ||
|
|
760
|
+
e.metaKey ||
|
|
761
|
+
e.altKey ||
|
|
762
|
+
e.key.length > 1
|
|
763
|
+
) {
|
|
764
|
+
if (props.onKeyDown) {
|
|
765
|
+
props.onKeyDown(e);
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 2. Stepper Up / Down via panah keyboard
|
|
771
|
+
if (e.key === "ArrowUp") {
|
|
772
|
+
e.preventDefault();
|
|
773
|
+
stepUp();
|
|
774
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (e.key === "ArrowDown") {
|
|
778
|
+
e.preventDefault();
|
|
779
|
+
stepDown();
|
|
780
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 3. Karakter Eksponensial ('e', 'E')
|
|
785
|
+
if (e.key === "e" || e.key === "E") {
|
|
786
|
+
if (!props.allowExponential) {
|
|
787
|
+
e.preventDefault();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 4. Blokir tanda plus '+'
|
|
793
|
+
if (e.key === "+") {
|
|
794
|
+
e.preventDefault();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 5. Pembatasan tanda minus '-'
|
|
799
|
+
if (e.key === "-") {
|
|
800
|
+
// Jika nilai minimum >= 0, dilarang tanda minus
|
|
801
|
+
if (props.min !== undefined && props.min >= 0) {
|
|
802
|
+
e.preventDefault();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const input = e.target;
|
|
806
|
+
const val = input ? input.value : "";
|
|
807
|
+
|
|
808
|
+
// Jika sudah ada tanda minus di depan, toggle/hapus tanda minus
|
|
809
|
+
if (val.startsWith("-")) {
|
|
810
|
+
e.preventDefault();
|
|
811
|
+
const positiveVal = val.slice(1);
|
|
812
|
+
const raw = parseToRawNumber(positiveVal);
|
|
813
|
+
setInnerValue(raw);
|
|
814
|
+
emitValue(raw);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Jika ada teks dan kursor tidak di posisi 0, jadikan negatif (tambahkan '-' di depan)
|
|
819
|
+
if (
|
|
820
|
+
val &&
|
|
821
|
+
input &&
|
|
822
|
+
(input.selectionStart === null || input.selectionStart > 0)
|
|
823
|
+
) {
|
|
824
|
+
e.preventDefault();
|
|
825
|
+
const negativeVal = "-" + val;
|
|
826
|
+
const raw = parseToRawNumber(negativeVal);
|
|
827
|
+
setInnerValue(raw);
|
|
828
|
+
emitValue(raw);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// 6. Pemisah desimal (koma ',')
|
|
836
|
+
if (e.key === ",") {
|
|
837
|
+
// Hanya tolak jika developer secara eksplisit menyetel precision={0}
|
|
838
|
+
if (props.precision === 0) {
|
|
839
|
+
e.preventDefault();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const input = e.target;
|
|
843
|
+
const val = input ? input.value : "";
|
|
844
|
+
|
|
845
|
+
// Hanya izinkan 1 koma desimal
|
|
846
|
+
if (val.includes(",")) {
|
|
847
|
+
e.preventDefault();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// 6b. Pemisah ribuan (titik '.')
|
|
855
|
+
if (e.key === ".") {
|
|
856
|
+
// Titik sebagai pemisah ribuan diizinkan
|
|
857
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// 7. Jika bukan digit 0-9, blokir karakter lainnya (termasuk aritmatika *, /, %, =, dll.)
|
|
862
|
+
if (!/^[0-9]$/.test(e.key)) {
|
|
863
|
+
e.preventDefault();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (props.onKeyDown) {
|
|
867
|
+
props.onKeyDown(e);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<div
|
|
873
|
+
className={`ttdk-form-item ${props.className || ""}`.trim()}
|
|
874
|
+
data-disabled={props.disabled}
|
|
875
|
+
data-error={Boolean(props.error || props.errorMessage)}
|
|
876
|
+
data-size={props.size || "md"}
|
|
877
|
+
data-block={props.block ? "true" : undefined}
|
|
878
|
+
style={{
|
|
879
|
+
...buildUiVars(props.ui),
|
|
880
|
+
...((props.ui as any) || {}),
|
|
881
|
+
...((props as any)["style"] || {}),
|
|
882
|
+
...props.style,
|
|
883
|
+
}}
|
|
884
|
+
>
|
|
885
|
+
{props.label ? (
|
|
886
|
+
<label
|
|
887
|
+
className={`ttdk-form-label ${props.labelClass || ""}`.trim()}
|
|
888
|
+
htmlFor={props.id}
|
|
889
|
+
style={{
|
|
890
|
+
...buildUiVars(props.labelStyle),
|
|
891
|
+
...((props.labelStyle as any) || {}),
|
|
892
|
+
}}
|
|
893
|
+
>
|
|
894
|
+
<span>{props.label}</span>
|
|
895
|
+
{props.required ? (
|
|
896
|
+
<span className="ttdk-form-required" aria-hidden="true">
|
|
897
|
+
*
|
|
898
|
+
</span>
|
|
899
|
+
) : null}
|
|
900
|
+
{props.optional ? (
|
|
901
|
+
<span className="ttdk-form-optional">
|
|
902
|
+
{props.optionalText || "(Opsional)"}
|
|
903
|
+
</span>
|
|
904
|
+
) : null}
|
|
905
|
+
</label>
|
|
906
|
+
) : null}
|
|
907
|
+
{props.helperText && props.helperTextPosition === "top" ? (
|
|
908
|
+
<div
|
|
909
|
+
className={`ttdk-form-helper-text ${
|
|
910
|
+
props.helperTextClass || ""
|
|
911
|
+
}`.trim()}
|
|
912
|
+
style={{
|
|
913
|
+
...buildUiVars(props.helperTextStyle),
|
|
914
|
+
...((props.helperTextStyle as any) || {}),
|
|
915
|
+
}}
|
|
916
|
+
>
|
|
917
|
+
{props.helperText}
|
|
918
|
+
</div>
|
|
919
|
+
) : null}
|
|
920
|
+
<div
|
|
921
|
+
className={`ttdk-input-wrapper ttdk-input-number-wrapper ${
|
|
922
|
+
props.wrapperClass || ""
|
|
923
|
+
}`.trim()}
|
|
924
|
+
data-filled={Boolean(String(getRawValue()) !== "")}
|
|
925
|
+
data-error={Boolean(props.error || props.errorMessage)}
|
|
926
|
+
data-disabled={Boolean(props.disabled)}
|
|
927
|
+
data-size={props.size || "md"}
|
|
928
|
+
data-rounded={props.rounded || "md"}
|
|
929
|
+
data-split={
|
|
930
|
+
shouldShowControls() && props.controlsPosition === "split"
|
|
931
|
+
? "true"
|
|
932
|
+
: undefined
|
|
933
|
+
}
|
|
934
|
+
style={{
|
|
935
|
+
...buildUiVars(props.wrapperStyle),
|
|
936
|
+
...((props.wrapperStyle as any) || {}),
|
|
937
|
+
}}
|
|
938
|
+
>
|
|
939
|
+
{shouldShowControls() && props.controlsPosition === "split" ? (
|
|
940
|
+
<button
|
|
941
|
+
type="button"
|
|
942
|
+
aria-label="Kurangi nilai"
|
|
943
|
+
className="ttdk-input-number-split-btn ttdk-input-number-split-down"
|
|
944
|
+
tabIndex={-1}
|
|
945
|
+
disabled={!canDecrement()}
|
|
946
|
+
data-disabled={!canDecrement() ? "true" : "false"}
|
|
947
|
+
onClick={(event) => stepDown()}
|
|
948
|
+
/>
|
|
949
|
+
) : null}
|
|
950
|
+
{props.addonLeft ? (
|
|
951
|
+
<div className="ttdk-input-addon ttdk-input-addon-left">
|
|
952
|
+
{props.addonLeft}
|
|
953
|
+
</div>
|
|
954
|
+
) : null}
|
|
955
|
+
<div className="ttdk-input-inner">
|
|
956
|
+
{props.iconLeft ? (
|
|
957
|
+
<span
|
|
958
|
+
className="ttdk-input-icon ttdk-input-icon-left"
|
|
959
|
+
aria-hidden="true"
|
|
960
|
+
>
|
|
961
|
+
{props.iconLeft}
|
|
962
|
+
</span>
|
|
963
|
+
) : null}
|
|
964
|
+
<input
|
|
965
|
+
ref={inputRef}
|
|
966
|
+
className={`ttdk-input-control ttdk-input-number-control ${
|
|
967
|
+
props.inputClass || ""
|
|
968
|
+
}`.trim()}
|
|
969
|
+
type={getInputType()}
|
|
970
|
+
inputMode={getInputMode()}
|
|
971
|
+
name={props.name}
|
|
972
|
+
id={props.id}
|
|
973
|
+
value={getDisplayValue()}
|
|
974
|
+
placeholder={props.placeholder}
|
|
975
|
+
disabled={props.disabled}
|
|
976
|
+
readOnly={props.readOnly}
|
|
977
|
+
required={props.required}
|
|
978
|
+
autoComplete={props.autoComplete}
|
|
979
|
+
maxLength={props.maxLength}
|
|
980
|
+
minLength={props.minLength}
|
|
981
|
+
min={props.min}
|
|
982
|
+
max={props.max}
|
|
983
|
+
step={props.step !== undefined ? props.step : 1}
|
|
984
|
+
aria-invalid={Boolean(props.error || props.errorMessage)}
|
|
985
|
+
style={{
|
|
986
|
+
...buildUiVars(props.inputStyle),
|
|
987
|
+
...((props.inputStyle as any) || {}),
|
|
988
|
+
}}
|
|
989
|
+
onChange={(e) => handleChange(e)}
|
|
990
|
+
onInput={(e) => handleInput(e)}
|
|
991
|
+
onBlur={(e) => handleBlur(e)}
|
|
992
|
+
onFocus={(e) => props.onFocus && props.onFocus(e)}
|
|
993
|
+
onCopy={(e) => handleCopy(e)}
|
|
994
|
+
onCut={(e) => handleCut(e)}
|
|
995
|
+
onKeyDown={(e) => handleKeyDown(e)}
|
|
996
|
+
onKeyUp={(e) => props.onKeyUp && props.onKeyUp(e)}
|
|
997
|
+
/>
|
|
998
|
+
{props.loading ? (
|
|
999
|
+
<span
|
|
1000
|
+
className="ttdk-input-icon ttdk-input-icon-right ttdk-input-spinner"
|
|
1001
|
+
aria-hidden="true"
|
|
1002
|
+
>
|
|
1003
|
+
<svg viewBox="0 0 24 24">
|
|
1004
|
+
<line
|
|
1005
|
+
x1="12"
|
|
1006
|
+
y1="2.5"
|
|
1007
|
+
x2="12"
|
|
1008
|
+
y2="6.5"
|
|
1009
|
+
opacity="0.175"
|
|
1010
|
+
transform="rotate(0 12 12)"
|
|
1011
|
+
/>
|
|
1012
|
+
<line
|
|
1013
|
+
x1="12"
|
|
1014
|
+
y1="2.5"
|
|
1015
|
+
x2="12"
|
|
1016
|
+
y2="6.5"
|
|
1017
|
+
opacity="0.25"
|
|
1018
|
+
transform="rotate(30 12 12)"
|
|
1019
|
+
/>
|
|
1020
|
+
<line
|
|
1021
|
+
x1="12"
|
|
1022
|
+
y1="2.5"
|
|
1023
|
+
x2="12"
|
|
1024
|
+
y2="6.5"
|
|
1025
|
+
opacity="0.325"
|
|
1026
|
+
transform="rotate(60 12 12)"
|
|
1027
|
+
/>
|
|
1028
|
+
<line
|
|
1029
|
+
x1="12"
|
|
1030
|
+
y1="2.5"
|
|
1031
|
+
x2="12"
|
|
1032
|
+
y2="6.5"
|
|
1033
|
+
opacity="0.4"
|
|
1034
|
+
transform="rotate(90 12 12)"
|
|
1035
|
+
/>
|
|
1036
|
+
<line
|
|
1037
|
+
x1="12"
|
|
1038
|
+
y1="2.5"
|
|
1039
|
+
x2="12"
|
|
1040
|
+
y2="6.5"
|
|
1041
|
+
opacity="0.475"
|
|
1042
|
+
transform="rotate(120 12 12)"
|
|
1043
|
+
/>
|
|
1044
|
+
<line
|
|
1045
|
+
x1="12"
|
|
1046
|
+
y1="2.5"
|
|
1047
|
+
x2="12"
|
|
1048
|
+
y2="6.5"
|
|
1049
|
+
opacity="0.55"
|
|
1050
|
+
transform="rotate(150 12 12)"
|
|
1051
|
+
/>
|
|
1052
|
+
<line
|
|
1053
|
+
x1="12"
|
|
1054
|
+
y1="2.5"
|
|
1055
|
+
x2="12"
|
|
1056
|
+
y2="6.5"
|
|
1057
|
+
opacity="0.625"
|
|
1058
|
+
transform="rotate(180 12 12)"
|
|
1059
|
+
/>
|
|
1060
|
+
<line
|
|
1061
|
+
x1="12"
|
|
1062
|
+
y1="2.5"
|
|
1063
|
+
x2="12"
|
|
1064
|
+
y2="6.5"
|
|
1065
|
+
opacity="0.7"
|
|
1066
|
+
transform="rotate(210 12 12)"
|
|
1067
|
+
/>
|
|
1068
|
+
<line
|
|
1069
|
+
x1="12"
|
|
1070
|
+
y1="2.5"
|
|
1071
|
+
x2="12"
|
|
1072
|
+
y2="6.5"
|
|
1073
|
+
opacity="0.775"
|
|
1074
|
+
transform="rotate(240 12 12)"
|
|
1075
|
+
/>
|
|
1076
|
+
<line
|
|
1077
|
+
x1="12"
|
|
1078
|
+
y1="2.5"
|
|
1079
|
+
x2="12"
|
|
1080
|
+
y2="6.5"
|
|
1081
|
+
opacity="0.85"
|
|
1082
|
+
transform="rotate(270 12 12)"
|
|
1083
|
+
/>
|
|
1084
|
+
<line
|
|
1085
|
+
x1="12"
|
|
1086
|
+
y1="2.5"
|
|
1087
|
+
x2="12"
|
|
1088
|
+
y2="6.5"
|
|
1089
|
+
opacity="0.925"
|
|
1090
|
+
transform="rotate(300 12 12)"
|
|
1091
|
+
/>
|
|
1092
|
+
<line
|
|
1093
|
+
x1="12"
|
|
1094
|
+
y1="2.5"
|
|
1095
|
+
x2="12"
|
|
1096
|
+
y2="6.5"
|
|
1097
|
+
opacity="1"
|
|
1098
|
+
transform="rotate(330 12 12)"
|
|
1099
|
+
/>
|
|
1100
|
+
</svg>
|
|
1101
|
+
</span>
|
|
1102
|
+
) : null}
|
|
1103
|
+
{!props.loading && props.iconRight ? (
|
|
1104
|
+
<span
|
|
1105
|
+
className="ttdk-input-icon ttdk-input-icon-right"
|
|
1106
|
+
aria-hidden="true"
|
|
1107
|
+
>
|
|
1108
|
+
{props.iconRight}
|
|
1109
|
+
</span>
|
|
1110
|
+
) : null}
|
|
1111
|
+
{shouldShowControls() && props.controlsPosition !== "split" ? (
|
|
1112
|
+
<div
|
|
1113
|
+
className={`ttdk-input-number-stepper ${
|
|
1114
|
+
props.controlsClass || ""
|
|
1115
|
+
}`.trim()}
|
|
1116
|
+
style={{
|
|
1117
|
+
...buildUiVars(props.controlsStyle),
|
|
1118
|
+
...((props.controlsStyle as any) || {}),
|
|
1119
|
+
}}
|
|
1120
|
+
>
|
|
1121
|
+
<button
|
|
1122
|
+
type="button"
|
|
1123
|
+
aria-label="Tingkatkan nilai"
|
|
1124
|
+
className="ttdk-input-number-btn ttdk-input-number-btn-up"
|
|
1125
|
+
tabIndex={-1}
|
|
1126
|
+
disabled={!canIncrement()}
|
|
1127
|
+
data-disabled={!canIncrement() ? "true" : "false"}
|
|
1128
|
+
onClick={(event) => stepUp()}
|
|
1129
|
+
/>
|
|
1130
|
+
<button
|
|
1131
|
+
type="button"
|
|
1132
|
+
aria-label="Kurangi nilai"
|
|
1133
|
+
className="ttdk-input-number-btn ttdk-input-number-btn-down"
|
|
1134
|
+
tabIndex={-1}
|
|
1135
|
+
disabled={!canDecrement()}
|
|
1136
|
+
data-disabled={!canDecrement() ? "true" : "false"}
|
|
1137
|
+
onClick={(event) => stepDown()}
|
|
1138
|
+
/>
|
|
1139
|
+
</div>
|
|
1140
|
+
) : null}
|
|
1141
|
+
</div>
|
|
1142
|
+
{props.addonRight ? (
|
|
1143
|
+
<div className="ttdk-input-addon ttdk-input-addon-right">
|
|
1144
|
+
{props.addonRight}
|
|
1145
|
+
</div>
|
|
1146
|
+
) : null}
|
|
1147
|
+
{shouldShowControls() && props.controlsPosition === "split" ? (
|
|
1148
|
+
<button
|
|
1149
|
+
type="button"
|
|
1150
|
+
aria-label="Tingkatkan nilai"
|
|
1151
|
+
className="ttdk-input-number-split-btn ttdk-input-number-split-up"
|
|
1152
|
+
tabIndex={-1}
|
|
1153
|
+
disabled={!canIncrement()}
|
|
1154
|
+
data-disabled={!canIncrement() ? "true" : "false"}
|
|
1155
|
+
onClick={(event) => stepUp()}
|
|
1156
|
+
/>
|
|
1157
|
+
) : null}
|
|
1158
|
+
</div>
|
|
1159
|
+
{props.helperText && props.helperTextPosition !== "top" ? (
|
|
1160
|
+
<div
|
|
1161
|
+
className={`ttdk-form-helper-text ${
|
|
1162
|
+
props.helperTextClass || ""
|
|
1163
|
+
}`.trim()}
|
|
1164
|
+
style={{
|
|
1165
|
+
...buildUiVars(props.helperTextStyle),
|
|
1166
|
+
...((props.helperTextStyle as any) || {}),
|
|
1167
|
+
}}
|
|
1168
|
+
>
|
|
1169
|
+
{props.helperText}
|
|
1170
|
+
</div>
|
|
1171
|
+
) : null}
|
|
1172
|
+
{props.errorMessage && (props.error || props.errorMessage) ? (
|
|
1173
|
+
<div
|
|
1174
|
+
role="alert"
|
|
1175
|
+
className={`ttdk-form-error-msg ${
|
|
1176
|
+
props.errorMessageClass || ""
|
|
1177
|
+
}`.trim()}
|
|
1178
|
+
style={{
|
|
1179
|
+
...buildUiVars(props.errorMessageStyle),
|
|
1180
|
+
...((props.errorMessageStyle as any) || {}),
|
|
1181
|
+
}}
|
|
1182
|
+
>
|
|
1183
|
+
<span>{props.errorMessage}</span>
|
|
1184
|
+
</div>
|
|
1185
|
+
) : null}
|
|
1186
|
+
</div>
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
export default InputNumber;
|
|
1192
|
+
|
|
1193
|
+
export { InputNumber };
|