jy-headless 0.3.14 → 0.3.16
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/README.md +6 -0
- package/dist/Autocomplete/Autocomplete.d.ts +103 -0
- package/dist/Autocomplete/Autocomplete.js +103 -0
- package/dist/Input/NumberInput.d.ts +53 -0
- package/dist/Input/NumberInput.js +53 -0
- package/dist/Input/TextInput.d.ts +95 -0
- package/dist/Input/TextInput.js +95 -0
- package/dist/cjs/Autocomplete/Autocomplete.d.ts +103 -0
- package/dist/cjs/Autocomplete/Autocomplete.js +103 -0
- package/dist/cjs/Input/NumberInput.d.ts +53 -0
- package/dist/cjs/Input/NumberInput.js +53 -0
- package/dist/cjs/Input/TextInput.d.ts +95 -0
- package/dist/cjs/Input/TextInput.js +95 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
|
|
3
|
+
/**
|
|
4
|
+
* 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
|
|
5
|
+
*
|
|
6
|
+
* - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
|
|
7
|
+
* - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
|
|
8
|
+
* - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
|
|
9
|
+
* - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
|
|
10
|
+
* - Options는 portal로 렌더링되며, outside click 시 닫힘
|
|
11
|
+
*
|
|
12
|
+
* ## 모드
|
|
13
|
+
* 1) items 모드 (권장)
|
|
14
|
+
* - <Autocomplete.Options items={...} renderItem={...} />
|
|
15
|
+
* - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
|
|
16
|
+
*
|
|
17
|
+
* 2) children 모드 (간단 리스트)
|
|
18
|
+
* - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
|
|
19
|
+
* - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // 1) items 모드 (기본 / 권장)
|
|
23
|
+
* const items = [
|
|
24
|
+
* { value: 'apple', label: 'Apple' },
|
|
25
|
+
* { value: 'banana', label: 'Banana' },
|
|
26
|
+
* { value: 'grape', label: 'Grape' },
|
|
27
|
+
* ];
|
|
28
|
+
*
|
|
29
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
30
|
+
*
|
|
31
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
32
|
+
* <Autocomplete.Input placeholder="과일을 검색하세요" />
|
|
33
|
+
* <Autocomplete.Options
|
|
34
|
+
* items={items}
|
|
35
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
36
|
+
* />
|
|
37
|
+
* </Autocomplete>
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
|
|
41
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
42
|
+
* const [inputValue, setInputValue] = useState('');
|
|
43
|
+
*
|
|
44
|
+
* <Autocomplete
|
|
45
|
+
* value={value}
|
|
46
|
+
* onChange={setValue}
|
|
47
|
+
* inputValue={inputValue}
|
|
48
|
+
* onInputChange={setInputValue}
|
|
49
|
+
* >
|
|
50
|
+
* <Autocomplete.Input placeholder="검색어 제어" />
|
|
51
|
+
* <Autocomplete.Options
|
|
52
|
+
* items={items}
|
|
53
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
54
|
+
* />
|
|
55
|
+
* </Autocomplete>
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // 3) filterFn 커스터마이징 (prefix match)
|
|
59
|
+
* const startsWithFilter = (item, query) =>
|
|
60
|
+
* item.label.toLowerCase().startsWith(query.trim().toLowerCase());
|
|
61
|
+
*
|
|
62
|
+
* <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
|
|
63
|
+
* <Autocomplete.Input placeholder="앞글자 매칭" />
|
|
64
|
+
* <Autocomplete.Options
|
|
65
|
+
* items={items}
|
|
66
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
67
|
+
* />
|
|
68
|
+
* </Autocomplete>
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // 4) 가상화 옵션 조정 (많은 데이터)
|
|
72
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
73
|
+
* <Autocomplete.Input placeholder="대량 데이터" />
|
|
74
|
+
* <Autocomplete.Options
|
|
75
|
+
* items={bigItems}
|
|
76
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
77
|
+
* itemHeight={36}
|
|
78
|
+
* maxVisibleItems={8}
|
|
79
|
+
* overscan={3}
|
|
80
|
+
* />
|
|
81
|
+
* </Autocomplete>
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // 5) disabled
|
|
85
|
+
* <Autocomplete value={value} onChange={setValue} disabled>
|
|
86
|
+
* <Autocomplete.Input placeholder="비활성화" />
|
|
87
|
+
* <Autocomplete.Options
|
|
88
|
+
* items={items}
|
|
89
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
90
|
+
* />
|
|
91
|
+
* </Autocomplete>
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // 6) children 모드 (소규모 리스트에만 권장)
|
|
95
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
96
|
+
*
|
|
97
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
98
|
+
* <Autocomplete.Input placeholder="직접 옵션 나열" />
|
|
99
|
+
* <Autocomplete.Options>
|
|
100
|
+
* <Autocomplete.Option value="seoul" label="Seoul" />
|
|
101
|
+
* <Autocomplete.Option value="busan" label="Busan" />
|
|
102
|
+
* <Autocomplete.Option value="jeju" label="Jeju" disabled />
|
|
103
|
+
* </Autocomplete.Options>
|
|
104
|
+
* </Autocomplete>
|
|
105
|
+
*/
|
|
3
106
|
export declare const Autocomplete: (({ value, onChange, inputValue, onInputChange, disabled, filterFn, children, }: AutocompleteProps) => import("react/jsx-runtime").JSX.Element) & {
|
|
4
107
|
Input: ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
108
|
Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
|
|
@@ -290,6 +290,109 @@ const Option = ({ value, label, disabled, children, ...props }) => {
|
|
|
290
290
|
setActiveIndex(-1);
|
|
291
291
|
}, ...props, children: children ?? label }));
|
|
292
292
|
};
|
|
293
|
+
/**
|
|
294
|
+
* 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
|
|
295
|
+
*
|
|
296
|
+
* - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
|
|
297
|
+
* - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
|
|
298
|
+
* - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
|
|
299
|
+
* - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
|
|
300
|
+
* - Options는 portal로 렌더링되며, outside click 시 닫힘
|
|
301
|
+
*
|
|
302
|
+
* ## 모드
|
|
303
|
+
* 1) items 모드 (권장)
|
|
304
|
+
* - <Autocomplete.Options items={...} renderItem={...} />
|
|
305
|
+
* - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
|
|
306
|
+
*
|
|
307
|
+
* 2) children 모드 (간단 리스트)
|
|
308
|
+
* - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
|
|
309
|
+
* - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* // 1) items 모드 (기본 / 권장)
|
|
313
|
+
* const items = [
|
|
314
|
+
* { value: 'apple', label: 'Apple' },
|
|
315
|
+
* { value: 'banana', label: 'Banana' },
|
|
316
|
+
* { value: 'grape', label: 'Grape' },
|
|
317
|
+
* ];
|
|
318
|
+
*
|
|
319
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
320
|
+
*
|
|
321
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
322
|
+
* <Autocomplete.Input placeholder="과일을 검색하세요" />
|
|
323
|
+
* <Autocomplete.Options
|
|
324
|
+
* items={items}
|
|
325
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
326
|
+
* />
|
|
327
|
+
* </Autocomplete>
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
|
|
331
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
332
|
+
* const [inputValue, setInputValue] = useState('');
|
|
333
|
+
*
|
|
334
|
+
* <Autocomplete
|
|
335
|
+
* value={value}
|
|
336
|
+
* onChange={setValue}
|
|
337
|
+
* inputValue={inputValue}
|
|
338
|
+
* onInputChange={setInputValue}
|
|
339
|
+
* >
|
|
340
|
+
* <Autocomplete.Input placeholder="검색어 제어" />
|
|
341
|
+
* <Autocomplete.Options
|
|
342
|
+
* items={items}
|
|
343
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
344
|
+
* />
|
|
345
|
+
* </Autocomplete>
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* // 3) filterFn 커스터마이징 (prefix match)
|
|
349
|
+
* const startsWithFilter = (item, query) =>
|
|
350
|
+
* item.label.toLowerCase().startsWith(query.trim().toLowerCase());
|
|
351
|
+
*
|
|
352
|
+
* <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
|
|
353
|
+
* <Autocomplete.Input placeholder="앞글자 매칭" />
|
|
354
|
+
* <Autocomplete.Options
|
|
355
|
+
* items={items}
|
|
356
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
357
|
+
* />
|
|
358
|
+
* </Autocomplete>
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* // 4) 가상화 옵션 조정 (많은 데이터)
|
|
362
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
363
|
+
* <Autocomplete.Input placeholder="대량 데이터" />
|
|
364
|
+
* <Autocomplete.Options
|
|
365
|
+
* items={bigItems}
|
|
366
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
367
|
+
* itemHeight={36}
|
|
368
|
+
* maxVisibleItems={8}
|
|
369
|
+
* overscan={3}
|
|
370
|
+
* />
|
|
371
|
+
* </Autocomplete>
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* // 5) disabled
|
|
375
|
+
* <Autocomplete value={value} onChange={setValue} disabled>
|
|
376
|
+
* <Autocomplete.Input placeholder="비활성화" />
|
|
377
|
+
* <Autocomplete.Options
|
|
378
|
+
* items={items}
|
|
379
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
380
|
+
* />
|
|
381
|
+
* </Autocomplete>
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* // 6) children 모드 (소규모 리스트에만 권장)
|
|
385
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
386
|
+
*
|
|
387
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
388
|
+
* <Autocomplete.Input placeholder="직접 옵션 나열" />
|
|
389
|
+
* <Autocomplete.Options>
|
|
390
|
+
* <Autocomplete.Option value="seoul" label="Seoul" />
|
|
391
|
+
* <Autocomplete.Option value="busan" label="Busan" />
|
|
392
|
+
* <Autocomplete.Option value="jeju" label="Jeju" disabled />
|
|
393
|
+
* </Autocomplete.Options>
|
|
394
|
+
* </Autocomplete>
|
|
395
|
+
*/
|
|
293
396
|
const Autocomplete = Object.assign(AutocompleteContainer, {
|
|
294
397
|
Input,
|
|
295
398
|
Options,
|
|
@@ -1,2 +1,55 @@
|
|
|
1
1
|
import { NumberInputProps } from './NumberInput.type';
|
|
2
|
+
/**
|
|
3
|
+
* 숫자 전용 Input 컴포넌트
|
|
4
|
+
*
|
|
5
|
+
* - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
|
|
6
|
+
* - max: 최대값 제한 (초과 시 자동으로 max로 보정)
|
|
7
|
+
* - useThousandsSeparator: 천단위 콤마 자동 포맷팅
|
|
8
|
+
* - 실제 onChange로 넘어가는 값은:
|
|
9
|
+
* - useThousandsSeparator=true → 콤마가 포함된 문자열
|
|
10
|
+
* - false → 순수 숫자 문자열
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // 1) 기본 숫자 입력
|
|
14
|
+
* <NumberInput
|
|
15
|
+
* placeholder="숫자 입력"
|
|
16
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 2) 최대값 제한
|
|
21
|
+
* <NumberInput
|
|
22
|
+
* max={100}
|
|
23
|
+
* placeholder="최대 100"
|
|
24
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
25
|
+
* />
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // 3) 천단위 콤마 포맷
|
|
29
|
+
* <NumberInput
|
|
30
|
+
* useThousandsSeparator
|
|
31
|
+
* placeholder="금액 입력"
|
|
32
|
+
* onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
|
|
33
|
+
* />
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // 4) max + 천단위 콤마 같이 사용
|
|
37
|
+
* <NumberInput
|
|
38
|
+
* max={1000000}
|
|
39
|
+
* useThousandsSeparator
|
|
40
|
+
* placeholder="최대 1,000,000"
|
|
41
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
42
|
+
* />
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // 5) React Hook Form 등과 함께 사용할 때
|
|
46
|
+
* // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
|
|
47
|
+
* <NumberInput
|
|
48
|
+
* useThousandsSeparator
|
|
49
|
+
* onChange={(e) => {
|
|
50
|
+
* const numeric = Number(e.target.value.replace(/,/g, ''));
|
|
51
|
+
* console.log(numeric);
|
|
52
|
+
* }}
|
|
53
|
+
* />
|
|
54
|
+
*/
|
|
2
55
|
export declare const NumberInput: ({ max, useThousandsSeparator, onChange, ...props }: NumberInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { TextInput } from './TextInput.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 숫자 전용 Input 컴포넌트
|
|
6
|
+
*
|
|
7
|
+
* - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
|
|
8
|
+
* - max: 최대값 제한 (초과 시 자동으로 max로 보정)
|
|
9
|
+
* - useThousandsSeparator: 천단위 콤마 자동 포맷팅
|
|
10
|
+
* - 실제 onChange로 넘어가는 값은:
|
|
11
|
+
* - useThousandsSeparator=true → 콤마가 포함된 문자열
|
|
12
|
+
* - false → 순수 숫자 문자열
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // 1) 기본 숫자 입력
|
|
16
|
+
* <NumberInput
|
|
17
|
+
* placeholder="숫자 입력"
|
|
18
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
19
|
+
* />
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // 2) 최대값 제한
|
|
23
|
+
* <NumberInput
|
|
24
|
+
* max={100}
|
|
25
|
+
* placeholder="최대 100"
|
|
26
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
27
|
+
* />
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // 3) 천단위 콤마 포맷
|
|
31
|
+
* <NumberInput
|
|
32
|
+
* useThousandsSeparator
|
|
33
|
+
* placeholder="금액 입력"
|
|
34
|
+
* onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
|
|
35
|
+
* />
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // 4) max + 천단위 콤마 같이 사용
|
|
39
|
+
* <NumberInput
|
|
40
|
+
* max={1000000}
|
|
41
|
+
* useThousandsSeparator
|
|
42
|
+
* placeholder="최대 1,000,000"
|
|
43
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
44
|
+
* />
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // 5) React Hook Form 등과 함께 사용할 때
|
|
48
|
+
* // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
|
|
49
|
+
* <NumberInput
|
|
50
|
+
* useThousandsSeparator
|
|
51
|
+
* onChange={(e) => {
|
|
52
|
+
* const numeric = Number(e.target.value.replace(/,/g, ''));
|
|
53
|
+
* console.log(numeric);
|
|
54
|
+
* }}
|
|
55
|
+
* />
|
|
56
|
+
*/
|
|
4
57
|
const NumberInput = ({ max, useThousandsSeparator, onChange, ...props }) => {
|
|
5
58
|
const handleChange = (e) => {
|
|
6
59
|
const rawValue = e.target.value.replace(/,/g, '');
|
|
@@ -1,3 +1,98 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TextInputProps } from './TextInput.type';
|
|
3
|
+
/**
|
|
4
|
+
* 범용 TextInput 컴포넌트
|
|
5
|
+
*
|
|
6
|
+
* - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
|
|
7
|
+
* - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
|
|
8
|
+
* - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
|
|
9
|
+
* - validator: 값 검증 (boolean | error message string 반환 가능)
|
|
10
|
+
* - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
|
|
11
|
+
* - debounceMs / onDebouncedChange: 디바운스된 값 콜백
|
|
12
|
+
* - throttleMs / onThrottledChange: 스로틀된 값 콜백
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // 1) 기본 사용
|
|
16
|
+
* <TextInput
|
|
17
|
+
* placeholder="이름을 입력하세요"
|
|
18
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
19
|
+
* />
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // 2) maxLength + trimWhitespace (blur 시 공백 제거)
|
|
23
|
+
* <TextInput
|
|
24
|
+
* maxLength={20}
|
|
25
|
+
* trimWhitespace
|
|
26
|
+
* placeholder="최대 20자, blur 시 공백 제거"
|
|
27
|
+
* onChange={(e) => console.log('change:', e.target.value)}
|
|
28
|
+
* />
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // 3) pattern(문자열)로 숫자만 허용
|
|
32
|
+
* // - 입력값이 정규식에 매번 매칭되어야 반영됨
|
|
33
|
+
* <TextInput
|
|
34
|
+
* pattern="^[0-9]*$"
|
|
35
|
+
* inputMode="numeric"
|
|
36
|
+
* placeholder="숫자만 입력"
|
|
37
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
38
|
+
* />
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
|
|
42
|
+
* // - test가 false면 반영되지 않음
|
|
43
|
+
* // - 예: 영문/숫자/언더스코어만 허용
|
|
44
|
+
* <TextInput
|
|
45
|
+
* disallowPattern={/^[a-zA-Z0-9_]*$/}
|
|
46
|
+
* placeholder="영문/숫자/_ 만"
|
|
47
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
48
|
+
* />
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // 5) validator + onValidate
|
|
52
|
+
* // - validator가 false면 onChange가 호출되지 않음
|
|
53
|
+
* // - 문자열 반환 시 그 문자열이 error로 넘어감
|
|
54
|
+
* <TextInput
|
|
55
|
+
* placeholder="이메일"
|
|
56
|
+
* validator={(v) => {
|
|
57
|
+
* if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
|
|
58
|
+
* const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
59
|
+
* return ok || '이메일 형식이 아니에요';
|
|
60
|
+
* }}
|
|
61
|
+
* onValidate={(isValid, error) => {
|
|
62
|
+
* console.log('valid:', isValid, 'error:', error);
|
|
63
|
+
* }}
|
|
64
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
65
|
+
* />
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
|
|
69
|
+
* <TextInput
|
|
70
|
+
* placeholder="검색어 입력"
|
|
71
|
+
* debounceMs={300}
|
|
72
|
+
* onDebouncedChange={(value) => {
|
|
73
|
+
* // fetch(`/api/search?q=${encodeURIComponent(value)}`)
|
|
74
|
+
* console.log('debounced:', value);
|
|
75
|
+
* }}
|
|
76
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
77
|
+
* />
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
|
|
81
|
+
* <TextInput
|
|
82
|
+
* placeholder="입력값 로깅(최대 200ms에 1번)"
|
|
83
|
+
* throttleMs={200}
|
|
84
|
+
* onThrottledChange={(value) => console.log('throttled:', value)}
|
|
85
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
86
|
+
* />
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // 8) IME(한글/일본어) 조합 이벤트 처리 예시
|
|
90
|
+
* <TextInput
|
|
91
|
+
* placeholder="한글 입력"
|
|
92
|
+
* maxLength={10}
|
|
93
|
+
* onCompositionStart={() => console.log('compose start')}
|
|
94
|
+
* onCompositionEnd={() => console.log('compose end')}
|
|
95
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
96
|
+
* />
|
|
97
|
+
*/
|
|
3
98
|
export declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
|
package/dist/Input/TextInput.js
CHANGED
|
@@ -16,6 +16,101 @@ function mergeRefs(...refs) {
|
|
|
16
16
|
});
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* 범용 TextInput 컴포넌트
|
|
21
|
+
*
|
|
22
|
+
* - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
|
|
23
|
+
* - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
|
|
24
|
+
* - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
|
|
25
|
+
* - validator: 값 검증 (boolean | error message string 반환 가능)
|
|
26
|
+
* - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
|
|
27
|
+
* - debounceMs / onDebouncedChange: 디바운스된 값 콜백
|
|
28
|
+
* - throttleMs / onThrottledChange: 스로틀된 값 콜백
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // 1) 기본 사용
|
|
32
|
+
* <TextInput
|
|
33
|
+
* placeholder="이름을 입력하세요"
|
|
34
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
35
|
+
* />
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // 2) maxLength + trimWhitespace (blur 시 공백 제거)
|
|
39
|
+
* <TextInput
|
|
40
|
+
* maxLength={20}
|
|
41
|
+
* trimWhitespace
|
|
42
|
+
* placeholder="최대 20자, blur 시 공백 제거"
|
|
43
|
+
* onChange={(e) => console.log('change:', e.target.value)}
|
|
44
|
+
* />
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // 3) pattern(문자열)로 숫자만 허용
|
|
48
|
+
* // - 입력값이 정규식에 매번 매칭되어야 반영됨
|
|
49
|
+
* <TextInput
|
|
50
|
+
* pattern="^[0-9]*$"
|
|
51
|
+
* inputMode="numeric"
|
|
52
|
+
* placeholder="숫자만 입력"
|
|
53
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
54
|
+
* />
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
|
|
58
|
+
* // - test가 false면 반영되지 않음
|
|
59
|
+
* // - 예: 영문/숫자/언더스코어만 허용
|
|
60
|
+
* <TextInput
|
|
61
|
+
* disallowPattern={/^[a-zA-Z0-9_]*$/}
|
|
62
|
+
* placeholder="영문/숫자/_ 만"
|
|
63
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
64
|
+
* />
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // 5) validator + onValidate
|
|
68
|
+
* // - validator가 false면 onChange가 호출되지 않음
|
|
69
|
+
* // - 문자열 반환 시 그 문자열이 error로 넘어감
|
|
70
|
+
* <TextInput
|
|
71
|
+
* placeholder="이메일"
|
|
72
|
+
* validator={(v) => {
|
|
73
|
+
* if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
|
|
74
|
+
* const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
75
|
+
* return ok || '이메일 형식이 아니에요';
|
|
76
|
+
* }}
|
|
77
|
+
* onValidate={(isValid, error) => {
|
|
78
|
+
* console.log('valid:', isValid, 'error:', error);
|
|
79
|
+
* }}
|
|
80
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
81
|
+
* />
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
|
|
85
|
+
* <TextInput
|
|
86
|
+
* placeholder="검색어 입력"
|
|
87
|
+
* debounceMs={300}
|
|
88
|
+
* onDebouncedChange={(value) => {
|
|
89
|
+
* // fetch(`/api/search?q=${encodeURIComponent(value)}`)
|
|
90
|
+
* console.log('debounced:', value);
|
|
91
|
+
* }}
|
|
92
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
93
|
+
* />
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
|
|
97
|
+
* <TextInput
|
|
98
|
+
* placeholder="입력값 로깅(최대 200ms에 1번)"
|
|
99
|
+
* throttleMs={200}
|
|
100
|
+
* onThrottledChange={(value) => console.log('throttled:', value)}
|
|
101
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
102
|
+
* />
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // 8) IME(한글/일본어) 조합 이벤트 처리 예시
|
|
106
|
+
* <TextInput
|
|
107
|
+
* placeholder="한글 입력"
|
|
108
|
+
* maxLength={10}
|
|
109
|
+
* onCompositionStart={() => console.log('compose start')}
|
|
110
|
+
* onCompositionEnd={() => console.log('compose end')}
|
|
111
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
112
|
+
* />
|
|
113
|
+
*/
|
|
19
114
|
const TextInput = forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
|
|
20
115
|
const [isComposing, setIsComposing] = useState(false);
|
|
21
116
|
const innerRef = useRef(null);
|
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
|
|
3
|
+
/**
|
|
4
|
+
* 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
|
|
5
|
+
*
|
|
6
|
+
* - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
|
|
7
|
+
* - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
|
|
8
|
+
* - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
|
|
9
|
+
* - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
|
|
10
|
+
* - Options는 portal로 렌더링되며, outside click 시 닫힘
|
|
11
|
+
*
|
|
12
|
+
* ## 모드
|
|
13
|
+
* 1) items 모드 (권장)
|
|
14
|
+
* - <Autocomplete.Options items={...} renderItem={...} />
|
|
15
|
+
* - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
|
|
16
|
+
*
|
|
17
|
+
* 2) children 모드 (간단 리스트)
|
|
18
|
+
* - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
|
|
19
|
+
* - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // 1) items 모드 (기본 / 권장)
|
|
23
|
+
* const items = [
|
|
24
|
+
* { value: 'apple', label: 'Apple' },
|
|
25
|
+
* { value: 'banana', label: 'Banana' },
|
|
26
|
+
* { value: 'grape', label: 'Grape' },
|
|
27
|
+
* ];
|
|
28
|
+
*
|
|
29
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
30
|
+
*
|
|
31
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
32
|
+
* <Autocomplete.Input placeholder="과일을 검색하세요" />
|
|
33
|
+
* <Autocomplete.Options
|
|
34
|
+
* items={items}
|
|
35
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
36
|
+
* />
|
|
37
|
+
* </Autocomplete>
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
|
|
41
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
42
|
+
* const [inputValue, setInputValue] = useState('');
|
|
43
|
+
*
|
|
44
|
+
* <Autocomplete
|
|
45
|
+
* value={value}
|
|
46
|
+
* onChange={setValue}
|
|
47
|
+
* inputValue={inputValue}
|
|
48
|
+
* onInputChange={setInputValue}
|
|
49
|
+
* >
|
|
50
|
+
* <Autocomplete.Input placeholder="검색어 제어" />
|
|
51
|
+
* <Autocomplete.Options
|
|
52
|
+
* items={items}
|
|
53
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
54
|
+
* />
|
|
55
|
+
* </Autocomplete>
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // 3) filterFn 커스터마이징 (prefix match)
|
|
59
|
+
* const startsWithFilter = (item, query) =>
|
|
60
|
+
* item.label.toLowerCase().startsWith(query.trim().toLowerCase());
|
|
61
|
+
*
|
|
62
|
+
* <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
|
|
63
|
+
* <Autocomplete.Input placeholder="앞글자 매칭" />
|
|
64
|
+
* <Autocomplete.Options
|
|
65
|
+
* items={items}
|
|
66
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
67
|
+
* />
|
|
68
|
+
* </Autocomplete>
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // 4) 가상화 옵션 조정 (많은 데이터)
|
|
72
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
73
|
+
* <Autocomplete.Input placeholder="대량 데이터" />
|
|
74
|
+
* <Autocomplete.Options
|
|
75
|
+
* items={bigItems}
|
|
76
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
77
|
+
* itemHeight={36}
|
|
78
|
+
* maxVisibleItems={8}
|
|
79
|
+
* overscan={3}
|
|
80
|
+
* />
|
|
81
|
+
* </Autocomplete>
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // 5) disabled
|
|
85
|
+
* <Autocomplete value={value} onChange={setValue} disabled>
|
|
86
|
+
* <Autocomplete.Input placeholder="비활성화" />
|
|
87
|
+
* <Autocomplete.Options
|
|
88
|
+
* items={items}
|
|
89
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
90
|
+
* />
|
|
91
|
+
* </Autocomplete>
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // 6) children 모드 (소규모 리스트에만 권장)
|
|
95
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
96
|
+
*
|
|
97
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
98
|
+
* <Autocomplete.Input placeholder="직접 옵션 나열" />
|
|
99
|
+
* <Autocomplete.Options>
|
|
100
|
+
* <Autocomplete.Option value="seoul" label="Seoul" />
|
|
101
|
+
* <Autocomplete.Option value="busan" label="Busan" />
|
|
102
|
+
* <Autocomplete.Option value="jeju" label="Jeju" disabled />
|
|
103
|
+
* </Autocomplete.Options>
|
|
104
|
+
* </Autocomplete>
|
|
105
|
+
*/
|
|
3
106
|
export declare const Autocomplete: (({ value, onChange, inputValue, onInputChange, disabled, filterFn, children, }: AutocompleteProps) => import("react/jsx-runtime").JSX.Element) & {
|
|
4
107
|
Input: ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
108
|
Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
|
|
@@ -292,6 +292,109 @@ const Option = ({ value, label, disabled, children, ...props }) => {
|
|
|
292
292
|
setActiveIndex(-1);
|
|
293
293
|
}, ...props, children: children ?? label }));
|
|
294
294
|
};
|
|
295
|
+
/**
|
|
296
|
+
* 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
|
|
297
|
+
*
|
|
298
|
+
* - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
|
|
299
|
+
* - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
|
|
300
|
+
* - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
|
|
301
|
+
* - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
|
|
302
|
+
* - Options는 portal로 렌더링되며, outside click 시 닫힘
|
|
303
|
+
*
|
|
304
|
+
* ## 모드
|
|
305
|
+
* 1) items 모드 (권장)
|
|
306
|
+
* - <Autocomplete.Options items={...} renderItem={...} />
|
|
307
|
+
* - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
|
|
308
|
+
*
|
|
309
|
+
* 2) children 모드 (간단 리스트)
|
|
310
|
+
* - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
|
|
311
|
+
* - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* // 1) items 모드 (기본 / 권장)
|
|
315
|
+
* const items = [
|
|
316
|
+
* { value: 'apple', label: 'Apple' },
|
|
317
|
+
* { value: 'banana', label: 'Banana' },
|
|
318
|
+
* { value: 'grape', label: 'Grape' },
|
|
319
|
+
* ];
|
|
320
|
+
*
|
|
321
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
322
|
+
*
|
|
323
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
324
|
+
* <Autocomplete.Input placeholder="과일을 검색하세요" />
|
|
325
|
+
* <Autocomplete.Options
|
|
326
|
+
* items={items}
|
|
327
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
328
|
+
* />
|
|
329
|
+
* </Autocomplete>
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
|
|
333
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
334
|
+
* const [inputValue, setInputValue] = useState('');
|
|
335
|
+
*
|
|
336
|
+
* <Autocomplete
|
|
337
|
+
* value={value}
|
|
338
|
+
* onChange={setValue}
|
|
339
|
+
* inputValue={inputValue}
|
|
340
|
+
* onInputChange={setInputValue}
|
|
341
|
+
* >
|
|
342
|
+
* <Autocomplete.Input placeholder="검색어 제어" />
|
|
343
|
+
* <Autocomplete.Options
|
|
344
|
+
* items={items}
|
|
345
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
346
|
+
* />
|
|
347
|
+
* </Autocomplete>
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* // 3) filterFn 커스터마이징 (prefix match)
|
|
351
|
+
* const startsWithFilter = (item, query) =>
|
|
352
|
+
* item.label.toLowerCase().startsWith(query.trim().toLowerCase());
|
|
353
|
+
*
|
|
354
|
+
* <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
|
|
355
|
+
* <Autocomplete.Input placeholder="앞글자 매칭" />
|
|
356
|
+
* <Autocomplete.Options
|
|
357
|
+
* items={items}
|
|
358
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
359
|
+
* />
|
|
360
|
+
* </Autocomplete>
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* // 4) 가상화 옵션 조정 (많은 데이터)
|
|
364
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
365
|
+
* <Autocomplete.Input placeholder="대량 데이터" />
|
|
366
|
+
* <Autocomplete.Options
|
|
367
|
+
* items={bigItems}
|
|
368
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
369
|
+
* itemHeight={36}
|
|
370
|
+
* maxVisibleItems={8}
|
|
371
|
+
* overscan={3}
|
|
372
|
+
* />
|
|
373
|
+
* </Autocomplete>
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* // 5) disabled
|
|
377
|
+
* <Autocomplete value={value} onChange={setValue} disabled>
|
|
378
|
+
* <Autocomplete.Input placeholder="비활성화" />
|
|
379
|
+
* <Autocomplete.Options
|
|
380
|
+
* items={items}
|
|
381
|
+
* renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
|
|
382
|
+
* />
|
|
383
|
+
* </Autocomplete>
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* // 6) children 모드 (소규모 리스트에만 권장)
|
|
387
|
+
* const [value, setValue] = useState<string | null>(null);
|
|
388
|
+
*
|
|
389
|
+
* <Autocomplete value={value} onChange={setValue}>
|
|
390
|
+
* <Autocomplete.Input placeholder="직접 옵션 나열" />
|
|
391
|
+
* <Autocomplete.Options>
|
|
392
|
+
* <Autocomplete.Option value="seoul" label="Seoul" />
|
|
393
|
+
* <Autocomplete.Option value="busan" label="Busan" />
|
|
394
|
+
* <Autocomplete.Option value="jeju" label="Jeju" disabled />
|
|
395
|
+
* </Autocomplete.Options>
|
|
396
|
+
* </Autocomplete>
|
|
397
|
+
*/
|
|
295
398
|
const Autocomplete = Object.assign(AutocompleteContainer, {
|
|
296
399
|
Input,
|
|
297
400
|
Options,
|
|
@@ -1,2 +1,55 @@
|
|
|
1
1
|
import { NumberInputProps } from './NumberInput.type';
|
|
2
|
+
/**
|
|
3
|
+
* 숫자 전용 Input 컴포넌트
|
|
4
|
+
*
|
|
5
|
+
* - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
|
|
6
|
+
* - max: 최대값 제한 (초과 시 자동으로 max로 보정)
|
|
7
|
+
* - useThousandsSeparator: 천단위 콤마 자동 포맷팅
|
|
8
|
+
* - 실제 onChange로 넘어가는 값은:
|
|
9
|
+
* - useThousandsSeparator=true → 콤마가 포함된 문자열
|
|
10
|
+
* - false → 순수 숫자 문자열
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // 1) 기본 숫자 입력
|
|
14
|
+
* <NumberInput
|
|
15
|
+
* placeholder="숫자 입력"
|
|
16
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 2) 최대값 제한
|
|
21
|
+
* <NumberInput
|
|
22
|
+
* max={100}
|
|
23
|
+
* placeholder="최대 100"
|
|
24
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
25
|
+
* />
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // 3) 천단위 콤마 포맷
|
|
29
|
+
* <NumberInput
|
|
30
|
+
* useThousandsSeparator
|
|
31
|
+
* placeholder="금액 입력"
|
|
32
|
+
* onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
|
|
33
|
+
* />
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // 4) max + 천단위 콤마 같이 사용
|
|
37
|
+
* <NumberInput
|
|
38
|
+
* max={1000000}
|
|
39
|
+
* useThousandsSeparator
|
|
40
|
+
* placeholder="최대 1,000,000"
|
|
41
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
42
|
+
* />
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // 5) React Hook Form 등과 함께 사용할 때
|
|
46
|
+
* // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
|
|
47
|
+
* <NumberInput
|
|
48
|
+
* useThousandsSeparator
|
|
49
|
+
* onChange={(e) => {
|
|
50
|
+
* const numeric = Number(e.target.value.replace(/,/g, ''));
|
|
51
|
+
* console.log(numeric);
|
|
52
|
+
* }}
|
|
53
|
+
* />
|
|
54
|
+
*/
|
|
2
55
|
export declare const NumberInput: ({ max, useThousandsSeparator, onChange, ...props }: NumberInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -3,6 +3,59 @@
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var TextInput = require('./TextInput.js');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* 숫자 전용 Input 컴포넌트
|
|
8
|
+
*
|
|
9
|
+
* - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
|
|
10
|
+
* - max: 최대값 제한 (초과 시 자동으로 max로 보정)
|
|
11
|
+
* - useThousandsSeparator: 천단위 콤마 자동 포맷팅
|
|
12
|
+
* - 실제 onChange로 넘어가는 값은:
|
|
13
|
+
* - useThousandsSeparator=true → 콤마가 포함된 문자열
|
|
14
|
+
* - false → 순수 숫자 문자열
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // 1) 기본 숫자 입력
|
|
18
|
+
* <NumberInput
|
|
19
|
+
* placeholder="숫자 입력"
|
|
20
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
21
|
+
* />
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 2) 최대값 제한
|
|
25
|
+
* <NumberInput
|
|
26
|
+
* max={100}
|
|
27
|
+
* placeholder="최대 100"
|
|
28
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
29
|
+
* />
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // 3) 천단위 콤마 포맷
|
|
33
|
+
* <NumberInput
|
|
34
|
+
* useThousandsSeparator
|
|
35
|
+
* placeholder="금액 입력"
|
|
36
|
+
* onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
|
|
37
|
+
* />
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 4) max + 천단위 콤마 같이 사용
|
|
41
|
+
* <NumberInput
|
|
42
|
+
* max={1000000}
|
|
43
|
+
* useThousandsSeparator
|
|
44
|
+
* placeholder="최대 1,000,000"
|
|
45
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
46
|
+
* />
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // 5) React Hook Form 등과 함께 사용할 때
|
|
50
|
+
* // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
|
|
51
|
+
* <NumberInput
|
|
52
|
+
* useThousandsSeparator
|
|
53
|
+
* onChange={(e) => {
|
|
54
|
+
* const numeric = Number(e.target.value.replace(/,/g, ''));
|
|
55
|
+
* console.log(numeric);
|
|
56
|
+
* }}
|
|
57
|
+
* />
|
|
58
|
+
*/
|
|
6
59
|
const NumberInput = ({ max, useThousandsSeparator, onChange, ...props }) => {
|
|
7
60
|
const handleChange = (e) => {
|
|
8
61
|
const rawValue = e.target.value.replace(/,/g, '');
|
|
@@ -1,3 +1,98 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TextInputProps } from './TextInput.type';
|
|
3
|
+
/**
|
|
4
|
+
* 범용 TextInput 컴포넌트
|
|
5
|
+
*
|
|
6
|
+
* - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
|
|
7
|
+
* - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
|
|
8
|
+
* - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
|
|
9
|
+
* - validator: 값 검증 (boolean | error message string 반환 가능)
|
|
10
|
+
* - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
|
|
11
|
+
* - debounceMs / onDebouncedChange: 디바운스된 값 콜백
|
|
12
|
+
* - throttleMs / onThrottledChange: 스로틀된 값 콜백
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // 1) 기본 사용
|
|
16
|
+
* <TextInput
|
|
17
|
+
* placeholder="이름을 입력하세요"
|
|
18
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
19
|
+
* />
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // 2) maxLength + trimWhitespace (blur 시 공백 제거)
|
|
23
|
+
* <TextInput
|
|
24
|
+
* maxLength={20}
|
|
25
|
+
* trimWhitespace
|
|
26
|
+
* placeholder="최대 20자, blur 시 공백 제거"
|
|
27
|
+
* onChange={(e) => console.log('change:', e.target.value)}
|
|
28
|
+
* />
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // 3) pattern(문자열)로 숫자만 허용
|
|
32
|
+
* // - 입력값이 정규식에 매번 매칭되어야 반영됨
|
|
33
|
+
* <TextInput
|
|
34
|
+
* pattern="^[0-9]*$"
|
|
35
|
+
* inputMode="numeric"
|
|
36
|
+
* placeholder="숫자만 입력"
|
|
37
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
38
|
+
* />
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
|
|
42
|
+
* // - test가 false면 반영되지 않음
|
|
43
|
+
* // - 예: 영문/숫자/언더스코어만 허용
|
|
44
|
+
* <TextInput
|
|
45
|
+
* disallowPattern={/^[a-zA-Z0-9_]*$/}
|
|
46
|
+
* placeholder="영문/숫자/_ 만"
|
|
47
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
48
|
+
* />
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // 5) validator + onValidate
|
|
52
|
+
* // - validator가 false면 onChange가 호출되지 않음
|
|
53
|
+
* // - 문자열 반환 시 그 문자열이 error로 넘어감
|
|
54
|
+
* <TextInput
|
|
55
|
+
* placeholder="이메일"
|
|
56
|
+
* validator={(v) => {
|
|
57
|
+
* if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
|
|
58
|
+
* const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
59
|
+
* return ok || '이메일 형식이 아니에요';
|
|
60
|
+
* }}
|
|
61
|
+
* onValidate={(isValid, error) => {
|
|
62
|
+
* console.log('valid:', isValid, 'error:', error);
|
|
63
|
+
* }}
|
|
64
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
65
|
+
* />
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
|
|
69
|
+
* <TextInput
|
|
70
|
+
* placeholder="검색어 입력"
|
|
71
|
+
* debounceMs={300}
|
|
72
|
+
* onDebouncedChange={(value) => {
|
|
73
|
+
* // fetch(`/api/search?q=${encodeURIComponent(value)}`)
|
|
74
|
+
* console.log('debounced:', value);
|
|
75
|
+
* }}
|
|
76
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
77
|
+
* />
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
|
|
81
|
+
* <TextInput
|
|
82
|
+
* placeholder="입력값 로깅(최대 200ms에 1번)"
|
|
83
|
+
* throttleMs={200}
|
|
84
|
+
* onThrottledChange={(value) => console.log('throttled:', value)}
|
|
85
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
86
|
+
* />
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // 8) IME(한글/일본어) 조합 이벤트 처리 예시
|
|
90
|
+
* <TextInput
|
|
91
|
+
* placeholder="한글 입력"
|
|
92
|
+
* maxLength={10}
|
|
93
|
+
* onCompositionStart={() => console.log('compose start')}
|
|
94
|
+
* onCompositionEnd={() => console.log('compose end')}
|
|
95
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
96
|
+
* />
|
|
97
|
+
*/
|
|
3
98
|
export declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -18,6 +18,101 @@ function mergeRefs(...refs) {
|
|
|
18
18
|
});
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 범용 TextInput 컴포넌트
|
|
23
|
+
*
|
|
24
|
+
* - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
|
|
25
|
+
* - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
|
|
26
|
+
* - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
|
|
27
|
+
* - validator: 값 검증 (boolean | error message string 반환 가능)
|
|
28
|
+
* - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
|
|
29
|
+
* - debounceMs / onDebouncedChange: 디바운스된 값 콜백
|
|
30
|
+
* - throttleMs / onThrottledChange: 스로틀된 값 콜백
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // 1) 기본 사용
|
|
34
|
+
* <TextInput
|
|
35
|
+
* placeholder="이름을 입력하세요"
|
|
36
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
37
|
+
* />
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 2) maxLength + trimWhitespace (blur 시 공백 제거)
|
|
41
|
+
* <TextInput
|
|
42
|
+
* maxLength={20}
|
|
43
|
+
* trimWhitespace
|
|
44
|
+
* placeholder="최대 20자, blur 시 공백 제거"
|
|
45
|
+
* onChange={(e) => console.log('change:', e.target.value)}
|
|
46
|
+
* />
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // 3) pattern(문자열)로 숫자만 허용
|
|
50
|
+
* // - 입력값이 정규식에 매번 매칭되어야 반영됨
|
|
51
|
+
* <TextInput
|
|
52
|
+
* pattern="^[0-9]*$"
|
|
53
|
+
* inputMode="numeric"
|
|
54
|
+
* placeholder="숫자만 입력"
|
|
55
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
56
|
+
* />
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
|
|
60
|
+
* // - test가 false면 반영되지 않음
|
|
61
|
+
* // - 예: 영문/숫자/언더스코어만 허용
|
|
62
|
+
* <TextInput
|
|
63
|
+
* disallowPattern={/^[a-zA-Z0-9_]*$/}
|
|
64
|
+
* placeholder="영문/숫자/_ 만"
|
|
65
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
66
|
+
* />
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // 5) validator + onValidate
|
|
70
|
+
* // - validator가 false면 onChange가 호출되지 않음
|
|
71
|
+
* // - 문자열 반환 시 그 문자열이 error로 넘어감
|
|
72
|
+
* <TextInput
|
|
73
|
+
* placeholder="이메일"
|
|
74
|
+
* validator={(v) => {
|
|
75
|
+
* if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
|
|
76
|
+
* const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
77
|
+
* return ok || '이메일 형식이 아니에요';
|
|
78
|
+
* }}
|
|
79
|
+
* onValidate={(isValid, error) => {
|
|
80
|
+
* console.log('valid:', isValid, 'error:', error);
|
|
81
|
+
* }}
|
|
82
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
83
|
+
* />
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
|
|
87
|
+
* <TextInput
|
|
88
|
+
* placeholder="검색어 입력"
|
|
89
|
+
* debounceMs={300}
|
|
90
|
+
* onDebouncedChange={(value) => {
|
|
91
|
+
* // fetch(`/api/search?q=${encodeURIComponent(value)}`)
|
|
92
|
+
* console.log('debounced:', value);
|
|
93
|
+
* }}
|
|
94
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
95
|
+
* />
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
|
|
99
|
+
* <TextInput
|
|
100
|
+
* placeholder="입력값 로깅(최대 200ms에 1번)"
|
|
101
|
+
* throttleMs={200}
|
|
102
|
+
* onThrottledChange={(value) => console.log('throttled:', value)}
|
|
103
|
+
* onChange={(e) => console.log('immediate:', e.target.value)}
|
|
104
|
+
* />
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // 8) IME(한글/일본어) 조합 이벤트 처리 예시
|
|
108
|
+
* <TextInput
|
|
109
|
+
* placeholder="한글 입력"
|
|
110
|
+
* maxLength={10}
|
|
111
|
+
* onCompositionStart={() => console.log('compose start')}
|
|
112
|
+
* onCompositionEnd={() => console.log('compose end')}
|
|
113
|
+
* onChange={(e) => console.log(e.target.value)}
|
|
114
|
+
* />
|
|
115
|
+
*/
|
|
21
116
|
const TextInput = react.forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
|
|
22
117
|
const [isComposing, setIsComposing] = react.useState(false);
|
|
23
118
|
const innerRef = react.useRef(null);
|