-
Notifications
You must be signed in to change notification settings - Fork 2
[Feat]: 회원가입 TextField 컴포넌트 #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
4c12e9e
feat: 텍스트 삭제 svg 작은 버전 아이콘 추가
shinjigu 7e9a3d1
feat: 회원가입 TextField 컴포넌트 추가
shinjigu 8a04f98
test: Home 페이지에 회원가입 TextField 컴포넌트 테스트 추가
shinjigu b99d10d
refactor: SignupTextField 코드 개선
shinjigu 0ce965d
style: lint & prettier 오류 수정
shinjigu 6fb6468
fix: completed/에러 상태 UX 개선 및 입력 끊김 현상 해결
shinjigu daf9575
fix: 'error' 상태 input/X버튼 직접 렌더링 분기 추가
shinjigu faafc68
refactor: 회원가입 TextField 코드 리뷰 반영
shinjigu 587d520
feat: 회원가입 텍스트필드에 직업 추가
shinjigu b84233c
test: Home에 회원가입 직업 텍스트필드 테스트 코드 추가
shinjigu 3975eeb
refactor: 회원가입 텍스트 필드 코드 개선
shinjigu 48de58a
refactor: SignupTextField 컴포넌트 구조 개선 및 UX 최적화
shinjigu 2f310a1
refactor: 회원가입 텍스트 필드 스타일 수정
shinjigu 3a9fe0b
test: 회원가입 텍스트 필드 테스트 코드 제거
shinjigu 4a41d7f
Merge branch 'develop' into feat/#58/signupTextField
shinjigu 34079b3
fix: SignupTextField 빌드 오류 해결
shinjigu 9af6b43
fix: 충돌 해결 및 불필요한 파일 삭제
shinjigu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { SVGProps } from 'react'; | ||
| const SvgIcSmallTextdelete = (props: SVGProps<SVGSVGElement>) => ( | ||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" {...props}> | ||
| <path | ||
| stroke="#E3E4E5" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| strokeWidth={1.5} | ||
| d="M4.166 15.835 15.833 4.168m-11.667 0 11.667 11.667" | ||
| /> | ||
| </svg> | ||
| ); | ||
| export default SvgIcSmallTextdelete; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| export { default as IcLock } from './IcLock'; | ||
| export { default as IcTextdelete } from './IcTextdelete'; | ||
| export { default as IcBigNext } from './IcBigNext'; | ||
| export { default as IcCheckboxChecked } from './IcCheckboxChecked'; | ||
| export { default as IcCheckboxDefault } from './IcCheckboxDefault'; | ||
| export { default as IcDropdown } from './IcDropdown'; | ||
| export { default as IcLock } from './IcLock'; | ||
| export { default as IcModalDelete } from './IcModalDelete'; | ||
| export { default as IcSmallTextdelete } from './IcSmallTextdelete'; | ||
| export { default as IcTextdelete } from './IcTextdelete'; | ||
| export { default as IcTooltipDelete } from './IcTooltipDelete'; | ||
| export { default as IcTriangle } from './IcTriangle'; | ||
| export { default as Vite } from './Vite'; |
51 changes: 51 additions & 0 deletions
51
src/common/component/SignupTextField/RenderInputContent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import React from 'react'; | ||
|
|
||
| import IcSmallTextdelete from '@/assets/svg/IcSmallTextdelete'; | ||
| import IcLock from '@/assets/svg/IcLock'; | ||
|
|
||
| interface RenderInputContentProps { | ||
| fieldState: string; | ||
| inputProps: React.InputHTMLAttributes<HTMLInputElement>; | ||
| value: string; | ||
| isLocked: boolean; | ||
| handleClearClick: (e: React.MouseEvent) => void; | ||
| styles: typeof import('./SignupTextField.css'); | ||
| } | ||
|
|
||
| export function RenderInputContent({ | ||
| fieldState, | ||
| inputProps, | ||
| value, | ||
| isLocked, | ||
| handleClearClick, | ||
| styles, | ||
| }: RenderInputContentProps) { | ||
| if (fieldState === 'typing' || fieldState === 'error') { | ||
| return ( | ||
| <> | ||
| <input {...inputProps} /> | ||
| {value && !isLocked && ( | ||
| <button | ||
| type="button" | ||
| onClick={handleClearClick} | ||
| onMouseDown={(e) => e.preventDefault()} | ||
| tabIndex={-1} | ||
| className={styles.clearButton} | ||
| aria-label="입력값 삭제" | ||
| > | ||
| <IcSmallTextdelete className={styles.iconClass} /> | ||
| </button> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
| if (fieldState === 'locked') { | ||
| return ( | ||
| <div className={styles.inputContent}> | ||
| <input {...inputProps} /> | ||
| <IcLock className={styles.lockIconClass} /> | ||
| </div> | ||
| ); | ||
| } | ||
| return <input {...inputProps} />; | ||
| } |
124 changes: 124 additions & 0 deletions
124
src/common/component/SignupTextField/SignupTextField.css.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { styleVariants, style } from '@vanilla-extract/css'; | ||
|
|
||
| import { colors } from '@/style/token/color.css'; | ||
| import { fonts } from '@/style/token/typography.css'; | ||
|
|
||
| export const baseClass = style({ | ||
| display: 'flex', | ||
| width: '52.2rem', | ||
| height: '5rem', | ||
| padding: '1.4rem 2rem', | ||
| alignItems: 'center', | ||
| flexShrink: 0, | ||
| borderRadius: '0.8rem', | ||
shinjigu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ...fonts.body03, | ||
| }); | ||
|
|
||
| export const fieldVariants = styleVariants({ | ||
| default: { | ||
| border: '2px solid transparent', | ||
| background: colors.grey4, | ||
| color: colors.grey6, | ||
| }, | ||
| clicked: { | ||
| border: `2px solid ${colors.blue06}`, | ||
| background: colors.grey3, | ||
| color: colors.grey6, | ||
| }, | ||
| typing: { | ||
| border: `2px solid ${colors.blue06}`, | ||
| background: colors.grey3, | ||
| color: colors.grey10, | ||
| }, | ||
| filled: { | ||
| border: '2px solid transparent', | ||
| background: colors.grey4, | ||
| color: colors.grey10, | ||
| }, | ||
| completed: { | ||
| border: '2px solid transparent', | ||
| background: colors.grey4, | ||
| color: colors.grey10, | ||
| }, | ||
| locked: { | ||
| border: '2px solid transparent', | ||
| background: colors.grey4, | ||
| color: colors.grey5, | ||
| }, | ||
| error: { | ||
| border: `2px solid ${colors.error01}`, | ||
| background: colors.grey4, | ||
| color: colors.grey10, | ||
| gap: '1rem', | ||
shinjigu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| }); | ||
|
|
||
| export const inputContent = style({ | ||
| display: 'flex', | ||
| flex: 1, | ||
| alignItems: 'center', | ||
| justifyContent: 'space-between', | ||
| flexShrink: 0, | ||
| }); | ||
|
|
||
| export const clearButton = style({ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| width: '2rem', | ||
| height: '2rem', | ||
| aspectRatio: '1/1', | ||
| background: 'none', | ||
| border: 'none', | ||
| padding: 0, | ||
| cursor: 'pointer', | ||
| }); | ||
shinjigu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export const iconClass = style({ | ||
| width: '2rem', | ||
| height: '2rem', | ||
| flexShrink: 0, | ||
| }); | ||
|
|
||
| export const lockIconClass = style({ | ||
| width: '2rem', | ||
| height: '2rem', | ||
| flexShrink: 0, | ||
| aspectRatio: '1/1', | ||
shinjigu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| export const errorMessageWrapper = style({ | ||
| display: 'flex', | ||
| width: '52.2rem', | ||
| flexDirection: 'column', | ||
| alignItems: 'flex-start', | ||
| gap: '0.4rem', | ||
| }); | ||
|
|
||
| export const inputBase = style({ | ||
| display: 'block', | ||
| width: '100%', | ||
| height: '100%', | ||
| background: 'transparent', | ||
| border: 'none', | ||
| outline: 'none', | ||
| padding: 0, | ||
| color: 'inherit', | ||
| }); | ||
|
|
||
| const makeInputStyle = (font: typeof fonts.body03) => | ||
| style({ | ||
| ...font, | ||
| '::placeholder': { | ||
| color: colors.grey6, | ||
| ...font, | ||
| }, | ||
| }); | ||
|
|
||
| export const inputStyle = makeInputStyle(fonts.body03); | ||
|
|
||
| export const errorMessage = style({ | ||
| alignSelf: 'stretch', | ||
| color: colors.error01, | ||
| ...fonts.caption02, | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export const MOCK_SIGNUP_DATA = { | ||
| name: '신지수?((#((3ㅓ우렁', | ||
| email: 'sjsz0811@hanyang.ac.kr', | ||
| birth: '2002-08-11', | ||
| }; |
129 changes: 129 additions & 0 deletions
129
src/common/component/SignupTextField/SignupTextField.tsx
shinjigu marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import type { SignupTextFieldProps } from './SignupTextField.types'; | ||
| import * as styles from './SignupTextField.css'; | ||
| import { validateField, formatBirthDate } from './utils'; | ||
| import { useSignupTextFieldState } from './useSignupTextFieldState'; | ||
| import { RenderInputContent } from './RenderInputContent'; | ||
|
|
||
| function getFieldState( | ||
| isFocused: boolean, | ||
| hasValue: boolean, | ||
| isHovered: boolean, | ||
| error: boolean, | ||
| isLocked: boolean, | ||
| ): keyof typeof styles.fieldVariants { | ||
| if (isLocked) { | ||
| return 'locked'; | ||
| } | ||
| if (error) { | ||
| return 'error'; | ||
| } | ||
| if (isFocused && hasValue) { | ||
| return 'typing'; | ||
| } | ||
| if (isFocused && !hasValue) { | ||
| return 'clicked'; | ||
| } | ||
| if (!isFocused && hasValue) { | ||
| return 'completed'; | ||
| } | ||
| if (isHovered) { | ||
| return 'clicked'; | ||
| } | ||
| return 'default'; | ||
| } | ||
|
|
||
| export default function SignupTextField({ | ||
| type, | ||
| value, | ||
| onChange, | ||
| placeholder, | ||
| error: externalError, | ||
| onBlur, | ||
| onFocus, | ||
| }: SignupTextFieldProps) { | ||
| const { | ||
| state, | ||
| dispatch, | ||
| inputRef, | ||
| handleKeyDown, | ||
| handleClearClick, | ||
| handleWrapperClick, | ||
| handleWrapperKeyDown, | ||
| } = useSignupTextFieldState({ onChange }); | ||
|
|
||
| const isLocked = type === 'email'; | ||
| const error = | ||
| externalError || | ||
| (type === 'email' ? undefined : validateField(type as 'name' | 'birth' | 'job', value)); | ||
| const hasValue = Boolean(value); | ||
| const fieldState = getFieldState(state.isFocused, hasValue, state.isHovered, !!error, isLocked); | ||
|
|
||
| const wrapperProps = isLocked | ||
| ? { tabIndex: -1 as const } | ||
| : { | ||
| role: 'button' as const, | ||
| tabIndex: 0 as const, | ||
| onClick: handleWrapperClick, | ||
| onKeyDown: handleWrapperKeyDown, | ||
| onMouseEnter: () => dispatch({ type: 'HOVER_ENTER' }), | ||
| onMouseLeave: () => dispatch({ type: 'HOVER_LEAVE' }), | ||
| }; | ||
|
|
||
| function createInputProps() { | ||
| return { | ||
| ref: inputRef, | ||
| type: 'text' as const, | ||
| value, | ||
| onChange: (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| if (!isLocked) { | ||
| if (type === 'birth') { | ||
| onChange(formatBirthDate(e.target.value)); | ||
| } else { | ||
| onChange(e.target.value); | ||
| } | ||
| } | ||
| }, | ||
| onFocus: () => { | ||
| dispatch({ type: 'FOCUS' }); | ||
| onFocus?.(); | ||
| }, | ||
| onBlur: () => { | ||
| dispatch({ type: 'BLUR' }); | ||
| onBlur?.(); | ||
| }, | ||
| onKeyDown: handleKeyDown, | ||
| onCompositionStart: () => dispatch({ type: 'COMPOSE_START' }), | ||
| onCompositionEnd: (e: React.CompositionEvent<HTMLInputElement>) => { | ||
| dispatch({ type: 'COMPOSE_END' }); | ||
| if (type === 'name' || type === 'job') { | ||
| const filtered = e.currentTarget.value.replace(/[^a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ\s]/g, ''); | ||
| onChange(filtered); | ||
| } | ||
| }, | ||
| placeholder: placeholder ?? (type === 'job' ? '정보를 입력해주세요' : placeholder), | ||
| disabled: isLocked, | ||
| className: [styles.inputBase, styles.inputStyle].join(' '), | ||
| }; | ||
| } | ||
|
|
||
| const inputProps = createInputProps(); | ||
|
|
||
| return ( | ||
| <div className={error ? styles.errorMessageWrapper : undefined}> | ||
| <div | ||
| className={[styles.baseClass, styles.fieldVariants[fieldState]].join(' ')} | ||
| {...wrapperProps} | ||
| > | ||
| <RenderInputContent | ||
| fieldState={fieldState} | ||
| inputProps={inputProps} | ||
| value={value} | ||
| isLocked={isLocked} | ||
| handleClearClick={handleClearClick} | ||
| styles={styles} | ||
| /> | ||
| </div> | ||
| {error && <div className={styles.errorMessage}>{error}</div>} | ||
| </div> | ||
| ); | ||
| } |
13 changes: 13 additions & 0 deletions
13
src/common/component/SignupTextField/SignupTextField.types.ts
shinjigu marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export type SignupTextFieldType = 'name' | 'email' | 'birth' | 'job'; | ||
|
|
||
| export interface SignupTextFieldProps { | ||
| type: SignupTextFieldType; | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| placeholder?: string; | ||
| error?: string; | ||
| disabled?: boolean; | ||
| onBlur?: () => void; | ||
| onFocus?: () => void; | ||
| maxLength?: number; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { default } from './SignupTextField'; | ||
| export * from './SignupTextField.types'; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.