@uniai-fe/uds-primitives 0.1.13 → 0.2.0
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 +2 -2
- package/dist/styles.css +1112 -385
- package/package.json +12 -15
- package/src/components/button/index.scss +1 -0
- package/src/components/button/markup/{ButtonRounded.tsx → Rounded.tsx} +1 -1
- package/src/components/button/markup/{ButtonText.tsx → Text.tsx} +1 -1
- package/src/components/button/markup/index.ts +3 -3
- package/src/components/button/styles/button.scss +113 -229
- package/src/components/button/styles/round-button.scss +11 -14
- package/src/components/button/styles/text-button.scss +23 -23
- package/src/components/button/styles/variables.scss +145 -0
- package/src/components/dropdown/index.tsx +3 -3
- package/src/components/dropdown/markup/Template.tsx +61 -0
- package/src/components/dropdown/markup/foundation/Container.tsx +97 -0
- package/src/components/dropdown/markup/foundation/MenuItem.tsx +107 -0
- package/src/components/dropdown/markup/foundation/MenuList.tsx +27 -0
- package/src/components/dropdown/markup/foundation/Provider.tsx +46 -0
- package/src/components/dropdown/markup/foundation/Root.tsx +30 -0
- package/src/components/dropdown/markup/foundation/Trigger.tsx +34 -0
- package/src/components/dropdown/markup/foundation/index.tsx +25 -0
- package/src/components/dropdown/markup/index.tsx +8 -2
- package/src/components/dropdown/styles/dropdown.scss +166 -0
- package/src/components/dropdown/styles/index.scss +2 -0
- package/src/components/dropdown/styles/variables.scss +40 -0
- package/src/components/dropdown/types/base.ts +18 -0
- package/src/components/dropdown/types/index.ts +2 -4
- package/src/components/dropdown/types/props.ts +174 -0
- package/src/components/dropdown/utils/index.ts +1 -4
- package/src/components/dropdown/utils/refs.ts +20 -0
- package/src/components/form/index.scss +1 -0
- package/src/components/form/index.tsx +18 -2
- package/src/components/form/markup/form-field/Body.tsx +18 -0
- package/src/components/form/markup/form-field/Container.tsx +58 -0
- package/src/components/form/markup/form-field/Footer.tsx +21 -0
- package/src/components/form/markup/form-field/Header.tsx +39 -0
- package/src/components/form/markup/form-field/Template.tsx +56 -0
- package/src/components/form/markup/form-field/index.tsx +22 -0
- package/src/components/form/styles/form-field/layout.scss +67 -0
- package/src/components/form/styles/form-field/variables.scss +17 -0
- package/src/components/form/styles/index.scss +2 -0
- package/src/components/form/types/index.ts +1 -0
- package/src/components/form/types/props.ts +125 -0
- package/src/components/form/utils/form-field.ts +42 -0
- package/src/components/input/hooks/index.ts +1 -4
- package/src/components/input/hooks/useDigitField.ts +63 -0
- package/src/components/input/img/calendar/calendar.svg +7 -0
- package/src/components/input/img/calendar/chevron-down.svg +3 -0
- package/src/components/input/img/calendar/chevron-left.svg +3 -0
- package/src/components/input/img/calendar/chevron-right.svg +3 -0
- package/src/components/input/img/calendar/chevron-up.svg +3 -0
- package/src/components/input/index.tsx +2 -1
- package/src/components/input/markup/calendar/Base.tsx +329 -0
- package/src/components/input/markup/calendar/index.tsx +8 -0
- package/src/components/input/markup/{text/InputUtilityButton.tsx → foundation/Button.tsx} +5 -15
- package/src/components/input/markup/foundation/Input.tsx +245 -0
- package/src/components/input/markup/foundation/SideSlot.tsx +30 -0
- package/src/components/input/markup/foundation/StatusIcon.tsx +21 -0
- package/src/components/input/markup/foundation/Utility.tsx +103 -0
- package/src/components/input/markup/foundation/index.tsx +15 -0
- package/src/components/input/markup/index.tsx +11 -1
- package/src/components/input/markup/text/AuthCode.tsx +41 -59
- package/src/components/input/markup/text/Email.tsx +25 -115
- package/src/components/input/markup/text/Password.tsx +30 -39
- package/src/components/input/markup/text/Phone.tsx +35 -122
- package/src/components/input/markup/text/Search.tsx +17 -18
- package/src/components/input/markup/text/index.ts +15 -12
- package/src/components/input/styles/calendar.scss +110 -0
- package/src/components/input/styles/foundation.scss +345 -0
- package/src/components/input/styles/index.scss +4 -476
- package/src/components/input/styles/text.scss +89 -0
- package/src/components/input/styles/variables.scss +41 -0
- package/src/components/input/types/calendar.ts +208 -0
- package/src/components/input/types/foundation.ts +194 -0
- package/src/components/input/types/hooks.ts +43 -0
- package/src/components/input/types/index.ts +5 -87
- package/src/components/input/types/text.ts +203 -0
- package/src/components/input/types/verification.ts +23 -0
- package/src/components/input/utils/index.tsx +1 -0
- package/src/components/input/utils/verification.tsx +35 -0
- package/src/components/select/hooks/index.ts +43 -2
- package/src/components/select/img/chevron/primary/large.svg +3 -0
- package/src/components/select/img/chevron/primary/medium.svg +3 -0
- package/src/components/select/img/chevron/primary/small.svg +3 -0
- package/src/components/select/img/chevron/secondary/large.svg +3 -0
- package/src/components/select/img/chevron/secondary/medium.svg +3 -0
- package/src/components/select/img/chevron/secondary/small.svg +3 -0
- package/src/components/select/img/remove.svg +3 -0
- package/src/components/select/index.scss +2 -1
- package/src/components/select/index.tsx +5 -0
- package/src/components/select/markup/Default.tsx +154 -0
- package/src/components/select/markup/foundation/Base.tsx +90 -0
- package/src/components/select/markup/foundation/Container.tsx +30 -0
- package/src/components/select/markup/foundation/Icon.tsx +78 -0
- package/src/components/select/markup/foundation/Selected.tsx +34 -0
- package/src/components/select/markup/foundation/index.ts +2 -0
- package/src/components/select/markup/index.tsx +36 -2
- package/src/components/select/markup/multiple/Multiple.tsx +205 -0
- package/src/components/select/markup/multiple/SelectedChip.tsx +58 -0
- package/src/components/select/markup/multiple/index.ts +2 -0
- package/src/components/select/styles/select.scss +316 -0
- package/src/components/select/styles/variables.scss +91 -0
- package/src/components/select/types/base.ts +34 -0
- package/src/components/select/types/icon.ts +45 -0
- package/src/components/select/types/index.ts +5 -4
- package/src/components/select/types/multiple.ts +57 -0
- package/src/components/select/types/props.ts +208 -0
- package/src/components/select/types/trigger.ts +196 -0
- package/src/index.scss +3 -2
- package/src/components/input/markup/text/Base.tsx +0 -454
- package/src/components/input/utils/index.ts +0 -60
- package/src/components/select/styles/index.scss +0 -0
- /package/src/components/button/markup/{ButtonDefault.tsx → Base.tsx} +0 -0
- /package/src/components/form/{Provider.tsx → markup/Provider.tsx} +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { InputUtilityProps } from "../../types";
|
|
6
|
+
import { InputStatusIcon } from "./StatusIcon";
|
|
7
|
+
import InputBaseSideSlot from "./SideSlot";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Input; 오른쪽 유틸리티 영역(wrapper)
|
|
11
|
+
* @component
|
|
12
|
+
* @param {InputUtilProps} props
|
|
13
|
+
* @param {React.ReactNode} [props.right] 오른쪽 슬롯 콘텐츠
|
|
14
|
+
* @param {React.ReactNode} [props.clear] clear 버튼 아이콘
|
|
15
|
+
* @param {React.ReactNode} [props.success] success 상태 아이콘 override
|
|
16
|
+
* @param {React.ReactNode} [props.error] error 상태 아이콘 override
|
|
17
|
+
* @param {string} props.state 현재 input 상태
|
|
18
|
+
* @param {boolean} props.isDisabled disable 여부
|
|
19
|
+
* @param {boolean} props.isFocused focus 여부
|
|
20
|
+
* @param {boolean} props.hasValue 입력값 존재 여부
|
|
21
|
+
* @param {boolean} [props.readOnly] readOnly 여부
|
|
22
|
+
* @param {Function} [props.onClear] clear 버튼 클릭 핸들러
|
|
23
|
+
*/
|
|
24
|
+
export default function InputBaseUtil({
|
|
25
|
+
children: right,
|
|
26
|
+
clear,
|
|
27
|
+
success,
|
|
28
|
+
error,
|
|
29
|
+
state,
|
|
30
|
+
isDisabled,
|
|
31
|
+
isFocused,
|
|
32
|
+
hasValue,
|
|
33
|
+
readOnly,
|
|
34
|
+
onClear,
|
|
35
|
+
}: InputUtilityProps) {
|
|
36
|
+
const [isClearInteracting, setIsClearInteracting] = useState(false);
|
|
37
|
+
|
|
38
|
+
const baseStatusIcon = InputStatusIcon[state] ?? null;
|
|
39
|
+
const statusIcon =
|
|
40
|
+
state === "success"
|
|
41
|
+
? (success ?? baseStatusIcon)
|
|
42
|
+
: state === "error"
|
|
43
|
+
? (error ?? baseStatusIcon)
|
|
44
|
+
: null;
|
|
45
|
+
const clearIconNode = clear ?? InputStatusIcon.reset;
|
|
46
|
+
const showClearIcon = Boolean(
|
|
47
|
+
clearIconNode &&
|
|
48
|
+
hasValue &&
|
|
49
|
+
!(isDisabled || readOnly) &&
|
|
50
|
+
(isFocused || isClearInteracting),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!right && !showClearIcon && !statusIcon) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleClearPointerDown = () => {
|
|
58
|
+
setIsClearInteracting(true);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleClearPointerLeave = () => {
|
|
62
|
+
setIsClearInteracting(false);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleClearPointerUp = (
|
|
66
|
+
event: React.PointerEvent<HTMLButtonElement>,
|
|
67
|
+
) => {
|
|
68
|
+
setIsClearInteracting(false);
|
|
69
|
+
onClear?.(event);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
// 유틸리티 영역; 오른쪽 슬롯/clear/status 정렬
|
|
74
|
+
<div className="input-field-utilities">
|
|
75
|
+
{right && <InputBaseSideSlot type="right">{right}</InputBaseSideSlot>}
|
|
76
|
+
{showClearIcon ? (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
className="input-affix input-affix-clear"
|
|
80
|
+
data-slot="clear"
|
|
81
|
+
data-visible="true"
|
|
82
|
+
onClick={onClear}
|
|
83
|
+
onPointerDown={handleClearPointerDown}
|
|
84
|
+
onPointerLeave={handleClearPointerLeave}
|
|
85
|
+
onPointerUp={handleClearPointerUp}
|
|
86
|
+
onPointerCancel={handleClearPointerLeave}
|
|
87
|
+
aria-label="입력 내용 지우기"
|
|
88
|
+
>
|
|
89
|
+
{clearIconNode}
|
|
90
|
+
</button>
|
|
91
|
+
) : null}
|
|
92
|
+
{statusIcon ? (
|
|
93
|
+
<div
|
|
94
|
+
className="input-affix input-affix-status"
|
|
95
|
+
data-slot="status"
|
|
96
|
+
data-state={state}
|
|
97
|
+
>
|
|
98
|
+
{statusIcon}
|
|
99
|
+
</div>
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import InputBase from "./Input";
|
|
4
|
+
import InputBaseSideSlot from "./SideSlot";
|
|
5
|
+
import InputBaseUtil from "./Utility";
|
|
6
|
+
import InputBaseUtilityButton from "./Button";
|
|
7
|
+
import { InputStatusIcon } from "./StatusIcon";
|
|
8
|
+
|
|
9
|
+
export const InputFoundation = {
|
|
10
|
+
Base: InputBase,
|
|
11
|
+
Util: InputBaseUtil,
|
|
12
|
+
SideSlot: InputBaseSideSlot,
|
|
13
|
+
Icon: InputStatusIcon,
|
|
14
|
+
Button: InputBaseUtilityButton,
|
|
15
|
+
};
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { InputFoundation } from "./foundation";
|
|
4
|
+
import { InputText } from "./text";
|
|
5
|
+
import { InputCalendar } from "./calendar";
|
|
6
|
+
|
|
7
|
+
export const Input = {
|
|
8
|
+
...InputFoundation,
|
|
9
|
+
Text: InputText,
|
|
10
|
+
Calendar: InputCalendar,
|
|
11
|
+
};
|
|
@@ -1,51 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
import { forwardRef, useCallback, useMemo, useState } from "react";
|
|
3
|
-
import { Text } from "./Base";
|
|
4
|
-
import { InputUtilityButton } from "./InputUtilityButton";
|
|
5
|
-
import type { InputUtilityButtonClickHandler } from "./InputUtilityButton";
|
|
6
|
-
import type { InputProps } from "../../types";
|
|
1
|
+
"use client";
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import type { ChangeEvent } from "react";
|
|
4
|
+
import { forwardRef, useCallback, useMemo } from "react";
|
|
5
|
+
import InputBase from "../foundation/Input";
|
|
6
|
+
import InputBaseUtilityButton from "../foundation/Button";
|
|
7
|
+
import { useDigitField } from "../../hooks/useDigitField";
|
|
8
|
+
import type { AuthCodeInputProps } from "../../types";
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @property {string} [value] 제어형 값.
|
|
14
|
-
* @property {string} [defaultValue] 비제어 초기값.
|
|
15
|
-
* @property {(value: string) => void} [onValueChange] 값 변경 시 호출.
|
|
16
|
-
* @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
|
|
17
|
-
* @property {number} [length=6] 허용 자리수(4~8 사이로 보정).
|
|
18
|
-
* @property {(code: string) => void} [onComplete] length만큼 입력되면 호출.
|
|
19
|
-
* @property {string} [placeholder="인증코드 입력"] placeholder 텍스트.
|
|
20
|
-
* @property {ReactNode} [countdownText] 제한 시간 텍스트.
|
|
21
|
-
* @property {ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨.
|
|
22
|
-
* @property {InputUtilityButtonClickHandler} [onCountdownAction] 제한 시간 연장 핸들러.
|
|
23
|
-
* @property {boolean} [countdownActionDisabled] 제한 시간 연장 버튼 disabled.
|
|
11
|
+
* 인증코드 길이를 4~8자 범위로 보정한다.
|
|
24
12
|
*/
|
|
25
|
-
|
|
26
|
-
InputProps,
|
|
27
|
-
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
28
|
-
> {
|
|
29
|
-
value?: string;
|
|
30
|
-
defaultValue?: string;
|
|
31
|
-
onValueChange?: (value: string) => void;
|
|
32
|
-
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
33
|
-
length?: number;
|
|
34
|
-
onComplete?: (code: string) => void;
|
|
35
|
-
placeholder?: string;
|
|
36
|
-
countdownText?: ReactNode;
|
|
37
|
-
countdownActionLabel?: ReactNode;
|
|
38
|
-
onCountdownAction?: InputUtilityButtonClickHandler;
|
|
39
|
-
countdownActionDisabled?: boolean;
|
|
40
|
-
}
|
|
13
|
+
const clampLength = (length?: number) => Math.max(4, Math.min(8, length ?? 6));
|
|
41
14
|
|
|
15
|
+
/**
|
|
16
|
+
* AuthCode 입력 필드 기본 placeholder.
|
|
17
|
+
*/
|
|
42
18
|
const DEFAULT_PLACEHOLDER = "인증코드 입력";
|
|
19
|
+
/**
|
|
20
|
+
* 제한 시간 연장 버튼 기본 라벨.
|
|
21
|
+
*/
|
|
43
22
|
const DEFAULT_COUNTDOWN_ACTION_LABEL = "시간연장";
|
|
44
23
|
|
|
45
24
|
/**
|
|
46
25
|
* AuthCodeInput — Text Input priority secondary 스타일로 구성된 인증번호 입력.
|
|
47
26
|
* @component
|
|
48
27
|
* @param {AuthCodeInputProps} props 인증코드 입력 props
|
|
28
|
+
* @property {string} [props.value] 제어형 인증코드 값
|
|
29
|
+
* @property {string} [props.defaultValue] 비제어 초기값
|
|
30
|
+
* @property {(value: string) => void} [props.onValueChange] 값 변경 콜백
|
|
31
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] native onChange override
|
|
32
|
+
* @property {number} [props.length=6] 허용 자리수(4~8로 보정)
|
|
33
|
+
* @property {(code: string) => void} [props.onComplete] 자리수 충족 시 호출
|
|
34
|
+
* @property {string} [props.placeholder="인증코드 입력"] placeholder 텍스트
|
|
35
|
+
* @property {ReactNode} [props.countdownText] 제한 시간 표시
|
|
36
|
+
* @property {ReactNode} [props.countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨
|
|
37
|
+
* @property {InputUtilityButtonClickHandler} [props.onCountdownAction] 연장 버튼 클릭 핸들러
|
|
38
|
+
* @property {boolean} [props.countdownActionDisabled] 연장 버튼 disabled 여부
|
|
49
39
|
*/
|
|
50
40
|
const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
|
|
51
41
|
(
|
|
@@ -68,31 +58,22 @@ const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
|
|
|
68
58
|
forwardedRef,
|
|
69
59
|
) => {
|
|
70
60
|
const safeLength = clampLength(length);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
const { digits: resolvedValue, handleDigitsChange } = useDigitField({
|
|
62
|
+
value,
|
|
63
|
+
defaultValue,
|
|
64
|
+
maxLength: safeLength,
|
|
65
|
+
});
|
|
76
66
|
|
|
77
67
|
const handleChange = useCallback(
|
|
78
68
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
79
|
-
const digits =
|
|
80
|
-
|
|
81
|
-
safeLength,
|
|
82
|
-
);
|
|
83
|
-
if (!isControlled) {
|
|
84
|
-
setInnerValue(digits);
|
|
85
|
-
}
|
|
86
|
-
if (event.currentTarget.value !== digits) {
|
|
69
|
+
const digits = handleDigitsChange(event);
|
|
70
|
+
if (event.currentTarget.value !== digits)
|
|
87
71
|
event.currentTarget.value = digits;
|
|
88
|
-
}
|
|
89
72
|
onValueChange?.(digits);
|
|
90
|
-
if (digits.length === safeLength)
|
|
91
|
-
onComplete?.(digits);
|
|
92
|
-
}
|
|
73
|
+
if (digits.length === safeLength) onComplete?.(digits);
|
|
93
74
|
onChange?.(event);
|
|
94
75
|
},
|
|
95
|
-
[
|
|
76
|
+
[handleDigitsChange, onChange, onComplete, onValueChange, safeLength],
|
|
96
77
|
);
|
|
97
78
|
|
|
98
79
|
const countdownActions = useMemo(() => {
|
|
@@ -101,18 +82,19 @@ const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
|
|
|
101
82
|
}
|
|
102
83
|
|
|
103
84
|
return (
|
|
104
|
-
|
|
85
|
+
// 인증 타이머/액션 영역
|
|
86
|
+
<div className="auth-code-input-actions">
|
|
105
87
|
{countdownText ? (
|
|
106
|
-
<span className="auth-code-
|
|
88
|
+
<span className="auth-code-input-countdown">{countdownText}</span>
|
|
107
89
|
) : null}
|
|
108
90
|
{onCountdownAction ? (
|
|
109
|
-
<
|
|
91
|
+
<InputBaseUtilityButton
|
|
110
92
|
priority="tertiary"
|
|
111
93
|
onClick={onCountdownAction}
|
|
112
94
|
disabled={countdownActionDisabled}
|
|
113
95
|
>
|
|
114
96
|
{countdownActionLabel}
|
|
115
|
-
</
|
|
97
|
+
</InputBaseUtilityButton>
|
|
116
98
|
) : null}
|
|
117
99
|
</div>
|
|
118
100
|
);
|
|
@@ -124,7 +106,7 @@ const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
|
|
|
124
106
|
]);
|
|
125
107
|
|
|
126
108
|
return (
|
|
127
|
-
<
|
|
109
|
+
<InputBase
|
|
128
110
|
{...restProps}
|
|
129
111
|
ref={forwardedRef}
|
|
130
112
|
priority={priority}
|
|
@@ -1,77 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
import { forwardRef, useCallback, useMemo } from "react";
|
|
3
|
-
import type { InputProps, InputState } from "../../types";
|
|
4
|
-
import { Text } from "./Base";
|
|
5
|
-
import { AuthCodeInput } from "./AuthCode";
|
|
6
|
-
import type { AuthCodeInputProps } from "./AuthCode";
|
|
7
|
-
import { InputUtilityButton } from "./InputUtilityButton";
|
|
8
|
-
import type { InputUtilityButtonClickHandler } from "./InputUtilityButton";
|
|
1
|
+
"use client";
|
|
9
2
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @property {InputUtilityButtonClickHandler} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
|
|
17
|
-
* @property {ReactNode} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
|
|
18
|
-
* @property {boolean} [requestButtonDisabled] 인증 요청 버튼 disabled(optional).
|
|
19
|
-
* @property {ReactNode} [countdownText] 인증 제한 시간 안내 텍스트(optional).
|
|
20
|
-
* @property {ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨(optional).
|
|
21
|
-
* @property {InputUtilityButtonClickHandler} [onCountdownAction] 제한 시간 연장 버튼 클릭 콜백(optional).
|
|
22
|
-
* @property {boolean} [countdownActionDisabled] 제한 시간 연장 버튼 disabled(optional).
|
|
23
|
-
* @property {boolean} [codeVisible] 인증번호 입력 UI 노출 여부(optional).
|
|
24
|
-
* @property {number} [codeLength=6] 인증번호 길이(optional).
|
|
25
|
-
* @property {ReactNode} [codeLabel] 인증번호 입력 label(optional).
|
|
26
|
-
* @property {ReactNode} [codeHelper] 인증번호 helper(optional).
|
|
27
|
-
* @property {InputState} [codeState] 인증번호 입력 상태(optional).
|
|
28
|
-
* @property {(code: string) => void} [onCodeComplete] 인증번호 입력 완료 시 호출(optional).
|
|
29
|
-
* @property {string} [codePlaceholder] 인증번호 입력 placeholder(optional).
|
|
30
|
-
* @property {Partial<AuthCodeInputProps>} [codeInputProps] 인증코드 입력 추가 설정(register 등) 전달용.
|
|
31
|
-
*/
|
|
32
|
-
export interface EmailInputProps extends Omit<
|
|
33
|
-
InputProps,
|
|
34
|
-
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
35
|
-
> {
|
|
36
|
-
value?: string;
|
|
37
|
-
defaultValue?: string;
|
|
38
|
-
onValueChange?: (value: string) => void;
|
|
39
|
-
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
40
|
-
onRequestCode?: InputUtilityButtonClickHandler;
|
|
41
|
-
requestButtonLabel?: ReactNode;
|
|
42
|
-
requestButtonDisabled?: boolean;
|
|
43
|
-
countdownText?: ReactNode;
|
|
44
|
-
countdownActionLabel?: ReactNode;
|
|
45
|
-
onCountdownAction?: InputUtilityButtonClickHandler;
|
|
46
|
-
countdownActionDisabled?: boolean;
|
|
47
|
-
codeVisible?: boolean;
|
|
48
|
-
codeLength?: number;
|
|
49
|
-
codeLabel?: ReactNode;
|
|
50
|
-
codeHelper?: ReactNode;
|
|
51
|
-
codeState?: InputState;
|
|
52
|
-
onCodeComplete?: (code: string) => void;
|
|
53
|
-
codePlaceholder?: string;
|
|
54
|
-
codeInputProps?: Partial<AuthCodeInputProps>;
|
|
55
|
-
}
|
|
3
|
+
import type { ChangeEvent } from "react";
|
|
4
|
+
import { forwardRef, useCallback } from "react";
|
|
5
|
+
|
|
6
|
+
import type { EmailInputProps } from "../../types";
|
|
7
|
+
import InputBase from "../foundation/Input";
|
|
8
|
+
import { renderVerificationRequestButton } from "../../utils/verification";
|
|
56
9
|
|
|
57
10
|
/**
|
|
58
|
-
*
|
|
11
|
+
* EmailInput — 이메일 입력과 인증요청 버튼만 제공한다.
|
|
59
12
|
* @component
|
|
60
|
-
* @param {EmailInputProps} props 이메일
|
|
61
|
-
* @
|
|
62
|
-
* @
|
|
63
|
-
* @
|
|
64
|
-
* @
|
|
65
|
-
* @
|
|
66
|
-
* @
|
|
67
|
-
* @
|
|
68
|
-
* @
|
|
69
|
-
* @param {boolean} [props.codeVisible] 인증번호 입력 UI 노출 여부
|
|
70
|
-
* @param {number} [props.codeLength] 인증번호 길이
|
|
71
|
-
* @param {ReactNode} [props.codeLabel] 인증번호 label
|
|
72
|
-
* @param {ReactNode} [props.codeHelper] 인증번호 helper
|
|
73
|
-
* @param {InputState} [props.codeState] 인증번호 입력 상태
|
|
74
|
-
* @param {(code: string) => void} [props.onCodeComplete] 인증번호 입력 완료 시 호출
|
|
13
|
+
* @param {EmailInputProps} props 이메일 입력 props
|
|
14
|
+
* @property {string} [props.value] 제어형 이메일 값
|
|
15
|
+
* @property {string} [props.defaultValue] 비제어 초기값
|
|
16
|
+
* @property {(value: string) => void} [props.onValueChange] 값 변경 콜백
|
|
17
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] native onChange override
|
|
18
|
+
* @property {InputUtilityButtonClickHandler} [props.onRequestCode] 인증요청 버튼 핸들러
|
|
19
|
+
* @property {ReactNode} [props.requestButtonLabel] 버튼 라벨
|
|
20
|
+
* @property {boolean} [props.requestButtonDisabled] 버튼 disabled 상태
|
|
21
|
+
* @property {ReactNode} [props.right] 기본 오른쪽 슬롯 override
|
|
75
22
|
*/
|
|
76
23
|
const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
77
24
|
(
|
|
@@ -83,23 +30,12 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
|
83
30
|
onRequestCode,
|
|
84
31
|
requestButtonLabel = "인증번호 요청",
|
|
85
32
|
requestButtonDisabled,
|
|
86
|
-
countdownText,
|
|
87
|
-
countdownActionLabel = "시간연장",
|
|
88
|
-
onCountdownAction,
|
|
89
|
-
countdownActionDisabled,
|
|
90
|
-
codeVisible,
|
|
91
|
-
codeLength = 6,
|
|
92
|
-
codeLabel,
|
|
93
|
-
codeHelper,
|
|
94
|
-
codeState,
|
|
95
|
-
onCodeComplete,
|
|
96
|
-
codePlaceholder = "인증코드 입력",
|
|
97
33
|
right,
|
|
98
|
-
codeInputProps,
|
|
99
34
|
...restProps
|
|
100
35
|
},
|
|
101
36
|
forwardedRef,
|
|
102
37
|
) => {
|
|
38
|
+
// 이메일 값 변경 시 상위 상태(onValueChange)와 native onChange를 모두 호출한다.
|
|
103
39
|
const handleChange = useCallback(
|
|
104
40
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
105
41
|
onValueChange?.(event.currentTarget.value);
|
|
@@ -108,41 +44,16 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
|
108
44
|
[onChange, onValueChange],
|
|
109
45
|
);
|
|
110
46
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<InputUtilityButton
|
|
118
|
-
priority="primary"
|
|
119
|
-
onClick={onRequestCode}
|
|
120
|
-
disabled={requestButtonDisabled}
|
|
121
|
-
>
|
|
122
|
-
{requestButtonLabel}
|
|
123
|
-
</InputUtilityButton>
|
|
124
|
-
);
|
|
125
|
-
}, [onRequestCode, requestButtonDisabled, requestButtonLabel]);
|
|
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
|
-
};
|
|
47
|
+
// 인증 요청 옵션이 있을 때만 우측 액션 버튼을 만든다.
|
|
48
|
+
const actionButton = renderVerificationRequestButton({
|
|
49
|
+
onRequestCode,
|
|
50
|
+
requestButtonDisabled,
|
|
51
|
+
requestButtonLabel,
|
|
52
|
+
});
|
|
142
53
|
|
|
143
54
|
return (
|
|
144
55
|
<div className="email-verification">
|
|
145
|
-
<
|
|
56
|
+
<InputBase
|
|
146
57
|
{...restProps}
|
|
147
58
|
ref={forwardedRef}
|
|
148
59
|
type="email"
|
|
@@ -152,7 +63,6 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
|
152
63
|
onChange={handleChange}
|
|
153
64
|
right={right ?? actionButton}
|
|
154
65
|
/>
|
|
155
|
-
{codeVisible ? <AuthCodeInput {...resolvedCodeProps} /> : null}
|
|
156
66
|
</div>
|
|
157
67
|
);
|
|
158
68
|
},
|
|
@@ -1,29 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback, useState } from "react";
|
|
4
|
+
import type { InputPasswordProps } from "../../types";
|
|
5
|
+
import InputBase from "../foundation/Input";
|
|
4
6
|
import HideOffIcon from "../../img/hide-off.svg";
|
|
5
7
|
import HideOnIcon from "../../img/hide-on.svg";
|
|
6
8
|
|
|
7
|
-
/**
|
|
8
|
-
* PasswordInput 전용 props. Text Input props에서 type을 password 전용으로 고정하고 보기/숨김 토글 옵션을 확장한다.
|
|
9
|
-
* @property {boolean} [defaultVisible=false] 초기 렌더 시 비밀번호를 드러낼지 여부.
|
|
10
|
-
* @property {{show: string; hide: string}} [toggleLabel] 토글 버튼에 사용할 라벨 텍스트 집합.
|
|
11
|
-
*/
|
|
12
|
-
export interface InputPasswordProps extends Omit<InputProps, "type"> {
|
|
13
|
-
defaultVisible?: boolean;
|
|
14
|
-
toggleLabel?: {
|
|
15
|
-
show: string;
|
|
16
|
-
hide: string;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
/**
|
|
21
10
|
* PasswordInput — 기본 Text 입력을 비밀번호 토글 UX로 확장한다.
|
|
22
11
|
* @component
|
|
23
12
|
* @param {InputPasswordProps} props
|
|
24
|
-
* @
|
|
25
|
-
* @
|
|
26
|
-
*
|
|
13
|
+
* @property {boolean} [props.defaultVisible=false] 초기 노출 여부
|
|
14
|
+
* @property {{show: string; hide: string}} [props.toggleLabel] 토글 버튼 라벨
|
|
15
|
+
* @property {ReactNode} [props.right] 오른쪽 슬롯 override
|
|
16
|
+
* @property {string | number | readonly string[]} [props.defaultValue] 초기값
|
|
27
17
|
*/
|
|
28
18
|
const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
|
|
29
19
|
(
|
|
@@ -38,33 +28,34 @@ const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
|
|
|
38
28
|
) => {
|
|
39
29
|
const [visible, setVisible] = useState(defaultVisible);
|
|
40
30
|
|
|
41
|
-
const
|
|
42
|
-
(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
31
|
+
const handleToggle = useCallback(() => {
|
|
32
|
+
setVisible(prev => !prev);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const toggleButton = (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
className="input-password-toggle"
|
|
39
|
+
onClick={handleToggle}
|
|
40
|
+
aria-pressed={visible}
|
|
41
|
+
aria-label={visible ? toggleLabel.hide : toggleLabel.show}
|
|
42
|
+
>
|
|
43
|
+
{/* 토글 상태에 따라 hide-on/off 아이콘을 교체한다. */}
|
|
44
|
+
{visible ? (
|
|
45
|
+
<HideOffIcon aria-hidden="true" />
|
|
46
|
+
) : (
|
|
47
|
+
<HideOnIcon aria-hidden="true" />
|
|
48
|
+
)}
|
|
49
|
+
</button>
|
|
59
50
|
);
|
|
60
51
|
|
|
61
52
|
return (
|
|
62
|
-
<
|
|
63
|
-
{...restProps}
|
|
53
|
+
<InputBase
|
|
64
54
|
ref={forwardedRef}
|
|
65
55
|
type={visible ? "text" : "password"}
|
|
66
56
|
defaultValue={defaultValue}
|
|
67
57
|
right={right ?? toggleButton}
|
|
58
|
+
{...restProps}
|
|
68
59
|
/>
|
|
69
60
|
);
|
|
70
61
|
},
|