Skip to content

Commit 6620118

Browse files
authored
Merge pull request #84 from SOPT-36-NINEDOT/feat/#64/editTextField
[Feature]: 수정하기 TextField 컴포넌트
2 parents 3e6dd75 + 7d73cad commit 6620118

File tree

8 files changed

+442
-0
lines changed

8 files changed

+442
-0
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { SVGProps } from 'react';
2+
const SvgIcMediumTextdelete = (props: SVGProps<SVGSVGElement>) => (
3+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" {...props}>
4+
<path
5+
stroke="#E3E4E5"
6+
strokeLinecap="round"
7+
strokeLinejoin="round"
8+
strokeWidth={2}
9+
d="M5 19 19 5M5 5l14 14"
10+
/>
11+
</svg>
12+
);
13+
export default SvgIcMediumTextdelete;

src/assets/svg/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { default as IcCheckboxChecked } from './IcCheckboxChecked';
33
export { default as IcCheckboxDefault } from './IcCheckboxDefault';
44
export { default as IcDropdown } from './IcDropdown';
55
export { default as IcLock } from './IcLock';
6+
export { default as IcMediumTextdelete } from './IcMediumTextdelete';
67
export { default as IcModalDelete } from './IcModalDelete';
78
export { default as IcPencil } from './IcPencil';
89
export { default as IcRadioChecked } from './IcRadioChecked';
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { style, styleVariants } from '@vanilla-extract/css';
2+
3+
import { colors, fonts } from '@/style/token';
4+
5+
const leftAligned = { textAlign: 'left' as const };
6+
7+
const subGoalBase = {
8+
display: 'flex',
9+
width: '62rem',
10+
height: '6.6rem',
11+
padding: '1.6rem 2rem',
12+
alignItems: 'center',
13+
flexShrink: 0,
14+
borderRadius: '8px',
15+
border: '3px solid transparent',
16+
fontSize: '2.2rem',
17+
fontWeight: 600,
18+
lineHeight: '140%',
19+
fontFamily: 'Pretendard',
20+
...leftAligned,
21+
};
22+
23+
const todoBase = {
24+
display: 'flex',
25+
width: '48.5rem',
26+
padding: '1.4rem 2rem',
27+
alignItems: 'center',
28+
borderRadius: '8px',
29+
border: '2px solid transparent',
30+
...fonts.subtitle03,
31+
...leftAligned,
32+
};
33+
34+
export const subGoalBaseClass = style({ ...subGoalBase });
35+
export const todoBaseClass = style({ ...todoBase });
36+
37+
export const inputBase = style({
38+
display: 'block',
39+
width: '100%',
40+
height: '100%',
41+
background: 'transparent',
42+
border: 'none',
43+
outline: 'none',
44+
padding: 0,
45+
color: 'inherit',
46+
textAlign: 'inherit',
47+
'::placeholder': {
48+
color: colors.grey6,
49+
},
50+
});
51+
52+
export const subGoalVariants = styleVariants({
53+
filled: {
54+
...subGoalBase,
55+
border: `3px solid ${colors.blue04}`,
56+
background: colors.grey2,
57+
color: colors.grey11,
58+
cursor: 'pointer',
59+
},
60+
empty: {
61+
...subGoalBase,
62+
border: `3px solid ${colors.blue04}`,
63+
background: colors.grey2,
64+
color: colors.grey6,
65+
cursor: 'pointer',
66+
},
67+
});
68+
69+
export const todoVariants = styleVariants({
70+
modify_empty: {
71+
...todoBase,
72+
background: colors.grey4,
73+
color: colors.grey6,
74+
cursor: 'pointer',
75+
},
76+
modify_hover: {
77+
...todoBase,
78+
background: colors.grey3,
79+
color: colors.grey6,
80+
cursor: 'pointer',
81+
},
82+
modify_clicked: {
83+
...todoBase,
84+
border: `2px solid ${colors.blue06}`,
85+
background: colors.grey3,
86+
color: colors.grey6,
87+
cursor: 'pointer',
88+
},
89+
modify_typing: {
90+
...todoBase,
91+
border: `2px solid ${colors.blue06}`,
92+
background: colors.grey3,
93+
color: colors.grey10,
94+
justifyContent: 'space-between',
95+
},
96+
modify_filled: {
97+
...todoBase,
98+
background: colors.grey4,
99+
color: colors.grey10,
100+
fontWeight: 600,
101+
cursor: 'pointer',
102+
},
103+
});
104+
105+
export const clearButton = style({
106+
display: 'flex',
107+
alignItems: 'center',
108+
justifyContent: 'center',
109+
width: '2.4rem',
110+
height: '2.4rem',
111+
background: 'none',
112+
border: 'none',
113+
padding: 0,
114+
cursor: 'pointer',
115+
flexShrink: 0,
116+
});
117+
118+
export const inputContainer = style({
119+
display: 'flex',
120+
justifyContent: 'space-between',
121+
alignItems: 'center',
122+
flex: '1 0 0',
123+
});
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import type { ModifyTextFieldProps, ModifyTextFieldVariant } from './ModifyTextField.types';
2+
import * as styles from './ModifyTextField.css';
3+
import { useModifyTextFieldState, type Action } from './useModifyTextFieldState';
4+
5+
import { IcMediumTextdelete } from '@/assets/svg';
6+
7+
const DEFAULT_PLACEHOLDER: Record<ModifyTextFieldVariant, string> = {
8+
subGoal: '세부 목표를 입력해주세요',
9+
todo: '할 일을 입력해주세요',
10+
};
11+
12+
// ====== 타입 정의 ======
13+
type SubGoalFieldState = 'filled' | 'empty';
14+
type TodoFieldState =
15+
| 'modify_empty'
16+
| 'modify_hover'
17+
| 'modify_clicked'
18+
| 'modify_typing'
19+
| 'modify_filled';
20+
21+
// ====== 상태 결정 함수 분리 ======
22+
function getSubGoalFieldState(hasValue: boolean): SubGoalFieldState {
23+
return hasValue ? 'filled' : 'empty';
24+
}
25+
26+
function getTodoFieldState(
27+
isFocused: boolean,
28+
isHovered: boolean,
29+
hasValue: boolean,
30+
): TodoFieldState {
31+
if (isFocused) {
32+
return hasValue ? 'modify_typing' : 'modify_clicked';
33+
}
34+
if (hasValue) {
35+
return 'modify_filled';
36+
}
37+
if (isHovered) {
38+
return 'modify_hover';
39+
}
40+
return 'modify_empty';
41+
}
42+
43+
// ====== 스타일 결정 함수 ======
44+
function getWrapperClass(
45+
variant: ModifyTextFieldVariant,
46+
fieldState: SubGoalFieldState | TodoFieldState,
47+
) {
48+
if (variant === 'subGoal') {
49+
return styles.subGoalVariants[fieldState as SubGoalFieldState];
50+
}
51+
return styles.todoVariants[fieldState as TodoFieldState];
52+
}
53+
54+
function getPlaceholder(variant: ModifyTextFieldVariant, placeholder?: string) {
55+
return placeholder ?? DEFAULT_PLACEHOLDER[variant];
56+
}
57+
58+
function getInputProps({
59+
value,
60+
onChange,
61+
onFocus,
62+
onBlur,
63+
handleKeyDown,
64+
dispatch,
65+
placeholder,
66+
disabled,
67+
}: {
68+
value: string;
69+
onChange: (value: string) => void;
70+
onFocus?: () => void;
71+
onBlur?: () => void;
72+
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
73+
dispatch: (action: Action) => void;
74+
placeholder?: string;
75+
disabled?: boolean;
76+
}) {
77+
return {
78+
type: 'text',
79+
value,
80+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value),
81+
onFocus: () => {
82+
dispatch({ type: 'FOCUS' });
83+
onFocus?.();
84+
},
85+
onBlur: () => {
86+
dispatch({ type: 'BLUR' });
87+
onBlur?.();
88+
},
89+
onKeyDown: handleKeyDown,
90+
onCompositionStart: () => dispatch({ type: 'COMPOSE_START' }),
91+
onCompositionEnd: () => dispatch({ type: 'COMPOSE_END' }),
92+
placeholder,
93+
disabled,
94+
className: styles.inputBase,
95+
};
96+
}
97+
98+
function getWrapperProps({
99+
disabled,
100+
handleWrapperClick,
101+
handleWrapperKeyDown,
102+
dispatch,
103+
isFocused,
104+
}: {
105+
disabled?: boolean;
106+
handleWrapperClick: () => void;
107+
handleWrapperKeyDown: (e: React.KeyboardEvent) => void;
108+
dispatch: (action: Action) => void;
109+
isFocused: boolean;
110+
}) {
111+
return disabled
112+
? { tabIndex: -1 as const }
113+
: {
114+
role: 'button' as const,
115+
tabIndex: 0 as const,
116+
onClick: isFocused ? undefined : handleWrapperClick,
117+
onKeyDown: handleWrapperKeyDown,
118+
onMouseEnter: () => dispatch({ type: 'HOVER_ENTER' }),
119+
onMouseLeave: () => dispatch({ type: 'HOVER_LEAVE' }),
120+
};
121+
}
122+
123+
function ClearButton({ onClick }: { onClick: (e: React.MouseEvent) => void }) {
124+
return (
125+
<button
126+
type="button"
127+
onClick={onClick}
128+
onMouseDown={(e) => e.preventDefault()}
129+
aria-label="입력값 삭제"
130+
className={styles.clearButton}
131+
>
132+
<IcMediumTextdelete />
133+
</button>
134+
);
135+
}
136+
137+
export default function ModifyTextField({
138+
variant = 'todo',
139+
value,
140+
onChange,
141+
placeholder,
142+
disabled = false,
143+
onBlur,
144+
onFocus,
145+
}: ModifyTextFieldProps) {
146+
const {
147+
state,
148+
dispatch,
149+
inputRef,
150+
handleKeyDown,
151+
handleClearClick,
152+
handleWrapperClick,
153+
handleWrapperKeyDown,
154+
} = useModifyTextFieldState({ onChange });
155+
156+
const hasValue = Boolean(value);
157+
const fieldState =
158+
variant === 'subGoal'
159+
? getSubGoalFieldState(hasValue)
160+
: getTodoFieldState(state.isFocused, state.isHovered, hasValue);
161+
const wrapperClass = getWrapperClass(variant, fieldState);
162+
const effectivePlaceholder = getPlaceholder(variant, placeholder);
163+
const inputProps = getInputProps({
164+
value,
165+
onChange,
166+
onFocus,
167+
onBlur,
168+
handleKeyDown,
169+
dispatch,
170+
placeholder: effectivePlaceholder,
171+
disabled,
172+
});
173+
const wrapperProps = getWrapperProps({
174+
disabled,
175+
handleWrapperClick,
176+
handleWrapperKeyDown,
177+
dispatch,
178+
isFocused: state.isFocused,
179+
});
180+
const showClearButton = fieldState === 'modify_typing';
181+
182+
return (
183+
<div className={wrapperClass} {...wrapperProps}>
184+
<div className={styles.inputContainer}>
185+
<input {...inputProps} ref={inputRef} />
186+
{showClearButton && <ClearButton onClick={handleClearClick} />}
187+
</div>
188+
</div>
189+
);
190+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export type ModifyTextFieldVariant = 'subGoal' | 'todo';
2+
3+
export interface ModifyTextFieldProps {
4+
variant?: ModifyTextFieldVariant;
5+
value: string;
6+
onChange: (value: string) => void;
7+
placeholder?: string;
8+
disabled?: boolean;
9+
onBlur?: () => void;
10+
onFocus?: () => void;
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './ModifyTextField';
2+
export type { ModifyTextFieldProps, ModifyTextFieldVariant } from './ModifyTextField.types';

0 commit comments

Comments
 (0)