@uniai-fe/uds-primitives 0.0.16 → 0.0.18

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.
@@ -138,6 +138,9 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
138
138
  if (resolvedState === "disabled") {
139
139
  return "disabled";
140
140
  }
141
+ if (resolvedState === "loading") {
142
+ return "loading";
143
+ }
141
144
  if (resolvedState === "error") {
142
145
  return "error";
143
146
  }
@@ -183,8 +186,12 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
183
186
  }, [visualState]);
184
187
 
185
188
  const effectiveClearIcon = clear ?? defaultClearIcon;
189
+ const shouldBlockClear =
190
+ resolvedState === "disabled" ||
191
+ resolvedState === "loading" ||
192
+ restProps.readOnly;
186
193
  const showClearIcon = Boolean(
187
- effectiveClearIcon && hasValue && resolvedState !== "disabled",
194
+ effectiveClearIcon && hasValue && !shouldBlockClear,
188
195
  );
189
196
  const isDisabled = resolvedState === "disabled";
190
197
  const labelFor = labelProps?.htmlFor ?? fieldId;
@@ -295,63 +302,69 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
295
302
  data-size={size}
296
303
  data-block={block ? "true" : undefined}
297
304
  >
298
- {shouldRenderInlineLabel ? (
299
- <div
300
- className="input-inline-label"
301
- aria-hidden="true"
302
- data-slot="inline-label"
303
- >
304
- {label}
305
- </div>
306
- ) : null}
307
- {left ? (
308
- <div
309
- className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--left`}
310
- data-slot="left"
311
- >
312
- {left}
313
- </div>
314
- ) : null}
315
- <input
316
- {...restProps}
317
- id={fieldId}
318
- ref={mergedRef}
319
- className={inputElementClassName}
320
- disabled={isDisabled}
321
- aria-invalid={resolvedState === "error" ? true : undefined}
322
- aria-describedby={helperElementId}
323
- type={type}
324
- value={value}
325
- defaultValue={defaultValue}
326
- name={inputName}
327
- onChange={handleChange}
328
- onFocus={handleFocus}
329
- onBlur={handleBlur}
330
- />
331
- {right ? (
332
- <div
333
- className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--right`}
334
- data-slot="right"
335
- >
336
- {right}
337
- </div>
338
- ) : null}
339
- {showClearIcon ? (
340
- <div
341
- className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--clear`}
342
- data-slot="clear"
343
- data-visible="true"
344
- >
345
- {effectiveClearIcon}
346
- </div>
347
- ) : null}
348
- {statusSlot ? (
349
- <div
350
- className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--status`}
351
- data-slot="status"
352
- data-state={resolvedState}
353
- >
354
- {statusSlot}
305
+ <div className="input-field__control">
306
+ {shouldRenderInlineLabel ? (
307
+ <div
308
+ className="input-inline-label"
309
+ aria-hidden="true"
310
+ data-slot="inline-label"
311
+ >
312
+ {label}
313
+ </div>
314
+ ) : null}
315
+ {left ? (
316
+ <div
317
+ className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--left`}
318
+ data-slot="left"
319
+ >
320
+ {left}
321
+ </div>
322
+ ) : null}
323
+ <input
324
+ {...restProps}
325
+ id={fieldId}
326
+ ref={mergedRef}
327
+ className={inputElementClassName}
328
+ disabled={isDisabled}
329
+ aria-invalid={resolvedState === "error" ? true : undefined}
330
+ aria-describedby={helperElementId}
331
+ type={type}
332
+ value={value}
333
+ defaultValue={defaultValue}
334
+ name={inputName}
335
+ onChange={handleChange}
336
+ onFocus={handleFocus}
337
+ onBlur={handleBlur}
338
+ />
339
+ </div>
340
+ {right || showClearIcon || statusSlot ? (
341
+ <div className="input-field__utilities">
342
+ {right ? (
343
+ <div
344
+ className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--right`}
345
+ data-slot="right"
346
+ >
347
+ {right}
348
+ </div>
349
+ ) : null}
350
+ {showClearIcon ? (
351
+ <div
352
+ className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--clear`}
353
+ data-slot="clear"
354
+ data-visible="true"
355
+ >
356
+ {effectiveClearIcon}
357
+ </div>
358
+ ) : null}
359
+ {statusSlot ? (
360
+ <div
361
+ className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--status`}
362
+ data-slot="status"
363
+ data-state={resolvedState}
364
+ >
365
+ {statusSlot}
366
+ </div>
367
+ ) : null}
355
368
  </div>
356
369
  ) : null}
357
370
  </div>
@@ -2,26 +2,34 @@ import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
2
2
  import { forwardRef, useCallback, useMemo } from "react";
3
3
  import type { InputProps, InputState } from "../../types";
4
4
  import { Text } from "./Base";
5
- import { IdentificationInput } from "./Identification";
5
+ import { AuthCodeInput } from "./AuthCode";
6
+ import type { AuthCodeInputProps } from "./AuthCode";
7
+ import { InputUtilityButton } from "./InputUtilityButton";
8
+ import type { InputUtilityButtonClickHandler } from "./InputUtilityButton";
6
9
 
7
10
  /**
8
- * EmailVerificationInput props. 이메일 입력 + 인증 요청/코드 입력 옵션을 정의한다.
11
+ * EmailInput props. 이메일 입력 + 인증 요청/코드 입력 옵션을 정의한다.
9
12
  * @property {string} [value] 제어형 값.
10
13
  * @property {string} [defaultValue] 비제어 초기값.
11
14
  * @property {(value: string) => void} [onValueChange] 값 변경 시 호출.
12
15
  * @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
13
- * @property {() => void} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
14
- * @property {string} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
16
+ * @property {InputUtilityButtonClickHandler} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
17
+ * @property {ReactNode} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
15
18
  * @property {boolean} [requestButtonDisabled] 인증 요청 버튼 disabled(optional).
16
19
  * @property {ReactNode} [countdownText] 인증 제한 시간 안내 텍스트(optional).
20
+ * @property {ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨(optional).
21
+ * @property {InputUtilityButtonClickHandler} [onCountdownAction] 제한 시간 연장 버튼 클릭 콜백(optional).
22
+ * @property {boolean} [countdownActionDisabled] 제한 시간 연장 버튼 disabled(optional).
17
23
  * @property {boolean} [codeVisible] 인증번호 입력 UI 노출 여부(optional).
18
24
  * @property {number} [codeLength=6] 인증번호 길이(optional).
19
25
  * @property {ReactNode} [codeLabel] 인증번호 입력 label(optional).
20
26
  * @property {ReactNode} [codeHelper] 인증번호 helper(optional).
21
27
  * @property {InputState} [codeState] 인증번호 입력 상태(optional).
22
28
  * @property {(code: string) => void} [onCodeComplete] 인증번호 입력 완료 시 호출(optional).
29
+ * @property {string} [codePlaceholder] 인증번호 입력 placeholder(optional).
30
+ * @property {Partial<AuthCodeInputProps>} [codeInputProps] 인증코드 입력 추가 설정(register 등) 전달용.
23
31
  */
24
- export interface EmailVerificationInputProps extends Omit<
32
+ export interface EmailInputProps extends Omit<
25
33
  InputProps,
26
34
  "type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
27
35
  > {
@@ -29,27 +37,32 @@ export interface EmailVerificationInputProps extends Omit<
29
37
  defaultValue?: string;
30
38
  onValueChange?: (value: string) => void;
31
39
  onChange?: ComponentPropsWithoutRef<"input">["onChange"];
32
- onRequestCode?: () => void;
33
- requestButtonLabel?: string;
40
+ onRequestCode?: InputUtilityButtonClickHandler;
41
+ requestButtonLabel?: ReactNode;
34
42
  requestButtonDisabled?: boolean;
35
43
  countdownText?: ReactNode;
44
+ countdownActionLabel?: ReactNode;
45
+ onCountdownAction?: InputUtilityButtonClickHandler;
46
+ countdownActionDisabled?: boolean;
36
47
  codeVisible?: boolean;
37
48
  codeLength?: number;
38
49
  codeLabel?: ReactNode;
39
50
  codeHelper?: ReactNode;
40
51
  codeState?: InputState;
41
52
  onCodeComplete?: (code: string) => void;
53
+ codePlaceholder?: string;
54
+ codeInputProps?: Partial<AuthCodeInputProps>;
42
55
  }
43
56
 
44
57
  /**
45
58
  * 이메일 인증 입력 컴포넌트; 이메일 입력 + 인증요청 버튼 + OneTimeCode 입력을 옵션으로 제공한다.
46
59
  * @component
47
- * @param {EmailVerificationInputProps} props 이메일 인증 props
60
+ * @param {EmailInputProps} props 이메일 인증 props
48
61
  * @param {string} [props.value] 제어형 이메일 값
49
62
  * @param {string} [props.defaultValue] 비제어 이메일 초기값
50
63
  * @param {(value: string) => void} [props.onValueChange] 이메일 변경 콜백
51
64
  * @param {ComponentPropsWithoutRef<"input">["onChange"]} [props.onChange] native onChange override
52
- * @param {() => void} [props.onRequestCode] 인증요청 버튼 클릭 시 호출
65
+ * @param {InputUtilityButtonClickHandler} [props.onRequestCode] 인증요청 버튼 클릭 시 호출
53
66
  * @param {string} [props.requestButtonLabel] 인증요청 버튼 라벨
54
67
  * @param {boolean} [props.requestButtonDisabled] 인증요청 버튼 disabled
55
68
  * @param {ReactNode} [props.countdownText] 제한 시간 안내 텍스트
@@ -60,10 +73,7 @@ export interface EmailVerificationInputProps extends Omit<
60
73
  * @param {InputState} [props.codeState] 인증번호 입력 상태
61
74
  * @param {(code: string) => void} [props.onCodeComplete] 인증번호 입력 완료 시 호출
62
75
  */
63
- const EmailVerificationInput = forwardRef<
64
- HTMLInputElement,
65
- EmailVerificationInputProps
66
- >(
76
+ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
67
77
  (
68
78
  {
69
79
  value,
@@ -74,13 +84,18 @@ const EmailVerificationInput = forwardRef<
74
84
  requestButtonLabel = "인증번호 요청",
75
85
  requestButtonDisabled,
76
86
  countdownText,
87
+ countdownActionLabel = "시간연장",
88
+ onCountdownAction,
89
+ countdownActionDisabled,
77
90
  codeVisible,
78
91
  codeLength = 6,
79
92
  codeLabel,
80
93
  codeHelper,
81
94
  codeState,
82
95
  onCodeComplete,
96
+ codePlaceholder = "인증코드 입력",
83
97
  right,
98
+ codeInputProps,
84
99
  ...restProps
85
100
  },
86
101
  forwardedRef,
@@ -99,17 +114,32 @@ const EmailVerificationInput = forwardRef<
99
114
  }
100
115
 
101
116
  return (
102
- <button
103
- type="button"
104
- className="input-action-button"
117
+ <InputUtilityButton
118
+ priority="primary"
105
119
  onClick={onRequestCode}
106
120
  disabled={requestButtonDisabled}
107
121
  >
108
122
  {requestButtonLabel}
109
- </button>
123
+ </InputUtilityButton>
110
124
  );
111
125
  }, [onRequestCode, requestButtonDisabled, requestButtonLabel]);
112
126
 
127
+ const resolvedCodeProps: AuthCodeInputProps = {
128
+ ...(codeInputProps ?? {}),
129
+ length: codeLength ?? codeInputProps?.length,
130
+ label: codeLabel ?? codeInputProps?.label,
131
+ helper: codeHelper ?? codeInputProps?.helper,
132
+ state: codeState ?? codeInputProps?.state,
133
+ placeholder: codePlaceholder ?? codeInputProps?.placeholder,
134
+ countdownText: countdownText ?? codeInputProps?.countdownText,
135
+ countdownActionLabel:
136
+ countdownActionLabel ?? codeInputProps?.countdownActionLabel,
137
+ onCountdownAction: onCountdownAction ?? codeInputProps?.onCountdownAction,
138
+ countdownActionDisabled:
139
+ countdownActionDisabled ?? codeInputProps?.countdownActionDisabled,
140
+ onComplete: onCodeComplete ?? codeInputProps?.onComplete,
141
+ };
142
+
113
143
  return (
114
144
  <div className="email-verification">
115
145
  <Text
@@ -122,23 +152,12 @@ const EmailVerificationInput = forwardRef<
122
152
  onChange={handleChange}
123
153
  right={right ?? actionButton}
124
154
  />
125
- {countdownText ? (
126
- <div className="email-verification__countdown">{countdownText}</div>
127
- ) : null}
128
- {codeVisible ? (
129
- <IdentificationInput
130
- length={codeLength}
131
- label={codeLabel}
132
- helper={codeHelper}
133
- state={codeState}
134
- onComplete={onCodeComplete}
135
- />
136
- ) : null}
155
+ {codeVisible ? <AuthCodeInput {...resolvedCodeProps} /> : null}
137
156
  </div>
138
157
  );
139
158
  },
140
159
  );
141
160
 
142
- EmailVerificationInput.displayName = "EmailVerificationInput";
161
+ EmailInput.displayName = "EmailInput";
143
162
 
144
- export { EmailVerificationInput };
163
+ export { EmailInput };
@@ -0,0 +1,46 @@
1
+ import type { MouseEvent, ReactNode } from "react";
2
+ import { Button } from "../../../button";
3
+ import type { ButtonPriority, ButtonProps } from "../../../button/types";
4
+
5
+ export type InputUtilityButtonClickHandler = (
6
+ event?: MouseEvent<HTMLButtonElement> | MouseEvent<HTMLAnchorElement>,
7
+ ) => void;
8
+
9
+ interface InputUtilityButtonProps {
10
+ children: ReactNode;
11
+ priority?: ButtonPriority;
12
+ disabled?: boolean;
13
+ onClick?: InputUtilityButtonClickHandler;
14
+ }
15
+
16
+ /**
17
+ * 입력 필드 우측에 배치되는 유틸리티 버튼; outlined-small scale을 공통으로 사용한다.
18
+ * @component
19
+ * @param {InputUtilityButtonProps} props 유틸 버튼 props
20
+ * @param {React.ReactNode} props.children 라벨
21
+ * @param {ButtonPriority} [props.priority="primary"] 색상 우선순위
22
+ * @param {boolean} [props.disabled] disabled 여부
23
+ * @param {InputUtilityButtonClickHandler} [props.onClick] onClick 핸들러
24
+ */
25
+ export function InputUtilityButton({
26
+ children,
27
+ priority = "primary",
28
+ disabled,
29
+ onClick,
30
+ }: InputUtilityButtonProps) {
31
+ const handleButtonClick: ButtonProps["onClick"] = event => {
32
+ onClick?.(event);
33
+ };
34
+
35
+ return (
36
+ <Button.Default
37
+ scale="outlined-small"
38
+ priority={priority}
39
+ className="input-utility-button"
40
+ onClick={handleButtonClick}
41
+ disabled={disabled}
42
+ >
43
+ {children}
44
+ </Button.Default>
45
+ );
46
+ }
@@ -1,8 +1,10 @@
1
1
  import { maskPhone } from "@uniai-fe/util-functions";
2
- import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
2
+ import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
3
3
  import { forwardRef, useCallback, useMemo, useState } from "react";
4
- import type { InputProps } from "../../types";
4
+ import type { InputProps, InputState } from "../../types";
5
5
  import { Text } from "./Base";
6
+ import { AuthCodeInput } from "./AuthCode";
7
+ import { InputUtilityButton } from "./InputUtilityButton";
6
8
 
7
9
  /**
8
10
  * PhoneInput 전용 props. Text Input 중 전화번호 UX에 필요한 필드를 정의한다.
@@ -13,6 +15,17 @@ import { Text } from "./Base";
13
15
  * @property {() => void} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
14
16
  * @property {string} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
15
17
  * @property {boolean} [requestButtonDisabled] 인증 요청 버튼 disabled(optional).
18
+ * @property {React.ReactNode} [countdownText] 인증 제한 시간 안내 텍스트(optional).
19
+ * @property {React.ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼(optional).
20
+ * @property {() => void} [onCountdownAction] 제한 시간 연장 핸들러(optional).
21
+ * @property {boolean} [countdownActionDisabled] 제한 시간 연장 disabled(optional).
22
+ * @property {boolean} [codeVisible] 인증번호 입력 UI 노출 여부(optional).
23
+ * @property {number} [codeLength=6] 인증번호 길이(optional).
24
+ * @property {React.ReactNode} [codeLabel] 인증번호 label(optional).
25
+ * @property {React.ReactNode} [codeHelper] 인증번호 helper 텍스트(optional).
26
+ * @property {InputState} [codeState] 인증번호 입력 상태(optional).
27
+ * @property {(code: string) => void} [onCodeComplete] 인증번호 입력 완료 콜백(optional).
28
+ * @property {string} [codePlaceholder="인증코드 입력"] 인증번호 placeholder(optional).
16
29
  */
17
30
  export interface PhoneInputProps extends Omit<
18
31
  InputProps,
@@ -25,6 +38,17 @@ export interface PhoneInputProps extends Omit<
25
38
  onRequestCode?: () => void;
26
39
  requestButtonLabel?: string;
27
40
  requestButtonDisabled?: boolean;
41
+ countdownText?: ReactNode;
42
+ countdownActionLabel?: ReactNode;
43
+ onCountdownAction?: () => void;
44
+ countdownActionDisabled?: boolean;
45
+ codeVisible?: boolean;
46
+ codeLength?: number;
47
+ codeLabel?: ReactNode;
48
+ codeHelper?: ReactNode;
49
+ codeState?: InputState;
50
+ onCodeComplete?: (code: string) => void;
51
+ codePlaceholder?: string;
28
52
  }
29
53
 
30
54
  const MAX_DIGITS = 11;
@@ -56,6 +80,17 @@ const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
56
80
  onRequestCode,
57
81
  requestButtonLabel = "인증번호 요청",
58
82
  requestButtonDisabled,
83
+ countdownText,
84
+ countdownActionLabel = "시간연장",
85
+ onCountdownAction,
86
+ countdownActionDisabled,
87
+ codeVisible,
88
+ codeLength = 6,
89
+ codeLabel,
90
+ codeHelper,
91
+ codeState,
92
+ onCodeComplete,
93
+ codePlaceholder = "인증코드 입력",
59
94
  right,
60
95
  ...restProps
61
96
  },
@@ -96,28 +131,51 @@ const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
96
131
  }
97
132
  // 휴대폰 인증 입력은 우측 right 슬롯에 인증 버튼을 둔다.
98
133
  return (
99
- <button
100
- type="button"
101
- className="input-action-button"
134
+ <InputUtilityButton
135
+ priority="primary"
102
136
  onClick={onRequestCode}
103
137
  disabled={requestButtonDisabled}
104
138
  >
105
139
  {requestButtonLabel}
106
- </button>
140
+ </InputUtilityButton>
107
141
  );
108
142
  }, [onRequestCode, requestButtonDisabled, requestButtonLabel]);
109
143
 
110
- return (
144
+ const phoneField = (
111
145
  <Text
112
146
  {...restProps}
113
147
  ref={forwardedRef}
114
148
  type="tel"
115
149
  inputMode="tel"
150
+ placeholder="000-0000-0000"
116
151
  value={formattedValue}
117
152
  onChange={handleChange}
118
153
  right={right ?? actionButton}
119
154
  />
120
155
  );
156
+
157
+ // 인증번호 UI가 비활성화된 경우에는 기존 PhoneInput DOM 구조를 그대로 유지한다.
158
+ if (!codeVisible) {
159
+ return phoneField;
160
+ }
161
+
162
+ return (
163
+ <div className="phone-verification">
164
+ {phoneField}
165
+ <AuthCodeInput
166
+ length={codeLength}
167
+ label={codeLabel}
168
+ helper={codeHelper}
169
+ state={codeState}
170
+ placeholder={codePlaceholder}
171
+ countdownText={countdownText}
172
+ countdownActionLabel={countdownActionLabel}
173
+ onCountdownAction={onCountdownAction}
174
+ countdownActionDisabled={countdownActionDisabled}
175
+ onComplete={onCodeComplete}
176
+ />
177
+ </div>
178
+ );
121
179
  },
122
180
  );
123
181
 
@@ -2,11 +2,11 @@ export { Text } from "./Base";
2
2
  export { Text as Input } from "./Base";
3
3
  export { PasswordInput } from "./Password";
4
4
  export { PhoneInput } from "./Phone";
5
- export { EmailVerificationInput } from "./EmailVerification";
5
+ export { EmailInput } from "./Email";
6
6
  export { SearchInput } from "./Search";
7
- export { IdentificationInput } from "./Identification";
7
+ export { AuthCodeInput } from "./AuthCode";
8
8
  export type { InputPasswordProps } from "./Password";
9
9
  export type { PhoneInputProps } from "./Phone";
10
- export type { EmailVerificationInputProps } from "./EmailVerification";
10
+ export type { EmailInputProps } from "./Email";
11
11
  export type { SearchInputProps } from "./Search";
12
- export type { IdentificationInputProps } from "./Identification";
12
+ export type { AuthCodeInputProps } from "./AuthCode";