Skip to content

Commit

Permalink
[FE] 인풋 컴포넌트 디자인 수정 및 로그인 페이지 디자인 수정 (#417)
Browse files Browse the repository at this point in the history
* chore: ios 기기에서 버튼 텍스트 색상이 파란색으로 보였던 문제 해결

* chore: BottomFixedButton 높이 수정

* feat: floating 스타일, 타입 추가

* feat: FloatingInput 구현

- label이 인풋 태그 내부에 위치하도록 relative, absolute 활용
- focus 상태를 관리

* feat: 어떤 입력 Field 인지 나타내는 Title 컴포넌트 생성

- label과는 다른 역할을 한다는 생각을 바탕으로, FieldLabel과 마크업과 스타일이 동일하지만 추가로 하나의 컴포넌트를 추가로 생성

* refactor: FloatingInput을 사용해서 약속을 생성하는 것으로 변경

* chore: DESCRIPTION, ERROR_MESSAGE를 구분

* feat: 스크롤을 막는 컴포넌트 구현

- 모바일 환경에서만 스크롤을 막으면 되기 때문에, 터치 이벤트로 인한 스크롤만 막도록 구현

* refactor: 약속 참여자 로그인 페이지 로직, 스타일 수정

- FloatingInput으로 수정
- useAttendeeLogin 커스텀 훅 활용
- 필드가 페이지 상단에 위치하도록 변경

* refactor: 도메인 정책 변경으로 인한, 1년 뒤의 약속을 생성하려고 하는 경우 토스트 메시지로 알려주는 로직 추가

* design: css 컨벤션을 맞추기 위해 rem으로 수정

* refactor: FloatingLabelInput으로 컴포넌트명 수정, 불필요햔 isFocused 상태 제거

* chore: 컴포넌트명 변경 사항 반영, 버튼 텍스트 상수화 파일명 수정

* chore: 직관적인 함수명을 사용하는 것으로 수정

* chore: IOS 기기에서 드롭다운 텍스트가 파란색으로 보이는 문제 해결

* chore: css 선언 순서 수정

* refactor: 달(Month)를 이동시키는 함수 추상화

* test: 1년 범위를 벗어난 경우 토스트 UI를 활용해서 피드백을 전달하는 책임 테스트 추가

* chore: 테스트 환경에서 절대경로를 인식해야 하는 폴더 추가, svg를 자바스크립트 모듈로 바라볼 수 있도록 설정 추가

* design: 바텀고정버튼 높이 수정 변경 반영

* chore: 불필요한 예외 처리 로직 제거
  • Loading branch information
hwinkr authored Oct 22, 2024
1 parent fa329ba commit cf21b3d
Show file tree
Hide file tree
Showing 30 changed files with 468 additions and 148 deletions.
6 changes: 6 additions & 0 deletions frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+.tsx?$': ['ts-jest', {}],
'^.+\\.svg$': '<rootDir>/svgTransformer.js',
},
testEnvironmentOptions: {
customExportConditions: [''],
Expand All @@ -13,5 +14,10 @@ module.exports = {
moduleNameMapper: {
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^@constants/(.*)$': '<rootDir>/src/constants/$1',
'^@contexts/(.*)$': '<rootDir>/src/contexts/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^@assets/(.*)$': '<rootDir>/src/assets/$1',
'^@styles/(.*)$': '<rootDir>/src/styles/$1',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';

import FloatingLabelInput from '.';

const meta = {
title: 'Components/Inputs/FloatingLabelInput',
component: FloatingLabelInput,
argTypes: {
label: { control: 'text' },
isError: { control: 'boolean' },
},
} satisfies Meta<typeof FloatingLabelInput>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
label: '낙타해리빙봉',
placeholder: '송재석최현웅김윤경',
isError: false,
},
};
42 changes: 42 additions & 0 deletions frontend/src/components/FloatingInput/FloatingLabelInput.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { css } from '@emotion/react';

import theme from '@styles/theme';

export const s_floatingLabelContainer = (isError: boolean) => css`
position: relative;
display: inline-block;
width: 100%;
color: ${isError ? '#EB1E1E' : '#71717a'};
&:focus-within label {
color: ${isError ? '#EB1E1E' : theme.colors.pink.medium};
}
`;

export const s_floatingLabelInput = (isError: boolean) => css`
appearance: none;
box-shadow: ${isError ? `0 0 0 0.1rem #EB1E1E` : `0 0 0 0.1rem #71717a`};
transition: box-shadow 0.3s;
&::placeholder {
color: #71717a;
}
&:focus {
box-shadow: ${isError
? `0 0 0 0.1rem #EB1E1E`
: `0 0 0 0.1rem ${theme.colors.pink.mediumLight}`};
}
`;

export const s_floatingLabel = () => css`
position: absolute;
top: 0.4rem;
left: 1em;
${theme.typography.captionMedium};
background: transparent;
transition: color 0.3s;
`;
34 changes: 34 additions & 0 deletions frontend/src/components/FloatingInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { InputProps } from '@components/_common/Input';
import Input from '@components/_common/Input';

import {
s_floatingLabel,
s_floatingLabelContainer,
s_floatingLabelInput,
} from './FloatingLabelInput.styles';

interface FloatingLabelInputProps extends InputProps {
label: string;
isError: boolean;
}
export default function FloatingLabelInput({
label,
placeholder,
isError,
...props
}: FloatingLabelInputProps) {
return (
<div css={s_floatingLabelContainer(isError)}>
<label css={s_floatingLabel} htmlFor={label}>
{label}
</label>
<Input
variant="floating"
css={s_floatingLabelInput(isError)}
placeholder={placeholder}
id={label}
{...props}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const s_rangeStart = (isAllRangeSelected: boolean) => css`
position: absolute;
top: 0;
right: 0.4px;
right: 0.02rem;
bottom: 0;
width: 20%;
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/ScrollBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { PropsWithChildren } from 'react';
import { useEffect, useRef } from 'react';

export default function ScrollBlock({ children }: PropsWithChildren) {
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const preventTouchMove = (e: TouchEvent) => {
e.preventDefault();
};

// 터치 이벤트를 사용해서 스크롤을 할 경우, 해당 스크롤을 막는다는 것을 브라우저에게 명시적으로 알려주기 위해서 passive 속성 추가(@해리)
document.addEventListener('touchmove', preventTouchMove, { passive: false });

return () => {
document.removeEventListener('touchmove', preventTouchMove);
};
}, []);

return <div ref={contentRef}>{children}</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { css } from '@emotion/react';
export const s_dropdownContainer = css`
display: flex;
gap: 1.2rem;
justify-content: flex-start;
align-items: center;
justify-content: flex-start;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ export const s_bottomFixedStyles = css`
/* 버튼 컴포넌트의 full variants를 사용하려고 했으나 6rem보다 height값이 작아 직접 높이를 정의했어요(@해리)
full 버튼에 이미 의존하고 있는 컴포넌트들이 많아서 높이를 full 스타일을 변경할 수는 없었습니다.
버튼의 높이가 너무 높다는 피드백을 반영하기 위해서 높이 수정 5.2rem(@해리)
*/
height: 6rem;
height: 5.2rem;
box-shadow: 0 -4px 4px rgb(0 0 0 / 25%);
`;

Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/_common/Dropdown/Dropdown.styles.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { css } from '@emotion/react';

import theme from '@styles/theme';

export const s_dropdown = css`
width: fit-content;
height: 2.8rem;
padding: 0.4rem;
color: ${theme.colors.black};
border-radius: 0.4rem;
`;
1 change: 1 addition & 0 deletions frontend/src/components/_common/Field/Field.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const s_field = css`
display: flex;
flex-direction: column;
gap: 0.8rem;
margin-bottom: 1.2rem;
`;

export const s_label = css`
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/_common/Field/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { ReactNode } from 'react';

import FloatingLabelInput from '@components/FloatingInput';

import Text from '../Text';
import {
s_description,
s_errorMessage,
Expand All @@ -16,6 +19,14 @@ const Field = ({ children }: FieldProps) => {
return <div css={s_field}>{children}</div>;
};

interface FieldTitleProps {
title: string;
}

const FieldTitle = ({ title }: FieldTitleProps) => {
return <Text typo="subTitleBold">{title}</Text>;
};

interface FieldLabelProps {
id: string;
labelText: string;
Expand All @@ -29,6 +40,7 @@ const FieldLabel = ({ id, labelText }: FieldLabelProps) => (

interface FieldDescriptionProps {
description?: string;
accentText?: string;
}

const FieldDescription = ({ description }: FieldDescriptionProps) =>
Expand All @@ -44,8 +56,10 @@ const FieldErrorMessage = ({ errorMessage }: FieldErrorMessageProps) => (
</div>
);

Field.Title = FieldTitle;
Field.Label = FieldLabel;
Field.Description = FieldDescription;
Field.ErrorMessage = FieldErrorMessage;
Field.FloatingLabelInput = FloatingLabelInput;

export default Field;
12 changes: 9 additions & 3 deletions frontend/src/components/_common/Input/Input.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import theme from '@styles/theme';

const baseInputStyle = css`
width: 100%;
height: 4.4rem;
padding: 0.8rem;
outline-color: ${theme.colors.primary};
height: 4.8rem;
outline: none;
${theme.typography.bodyMedium}
`;

Expand All @@ -19,4 +18,11 @@ export const s_input = {
border: none;
outline: none;
`,
floating: css`
padding-top: 1.6rem; /* 텍스트를 아래로 내리기 위해 top padding을 더 줌 (@해리) */
padding-left: 1.2rem;
border: none;
${baseInputStyle}
border-radius: 1.2rem;
`,
};
5 changes: 3 additions & 2 deletions frontend/src/components/_common/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import type { InputHTMLAttributes } from 'react';

import { s_input } from './Input.styles';

export type InputVariant = 'default' | 'transparent';
export type InputVariant = 'default' | 'transparent' | 'floating';

export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
variant?: InputVariant;
isError?: boolean;
}

export default function Input({ variant = 'default', type = 'text', ...props }: InputProps) {
return <input css={s_input[variant]} type={type} {...props} />;
return <input css={s_input[variant]} type={type} spellCheck={false} {...props} />;
}
5 changes: 5 additions & 0 deletions frontend/src/constants/buttons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const MEETING_BUTTON_TEXTS = {
create: '약속 생성하기',
next: '다음',
register: '등록하러 가기',
};
29 changes: 28 additions & 1 deletion frontend/src/constants/inputFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,35 @@ export const INPUT_RULES = {
};

export const FIELD_DESCRIPTIONS = {
meetingName: '약속 이름은 1~10자 사이로 입력해 주세요.',
nickname: '1~5자 사이로 입력해 주세요.',
password: '4자리 숫자로 입력해 주세요.',
date: '날짜를 하나씩 클릭해 여러 날짜를 선택하거나\n시작일과 종료일을 클릭해 사이의 모든 날짜를 선택해 보세요',
};

export const FIELD_TITLES = {
meetingName: '약속 정보 입력',
meetingHostInfo: '약속 주최자 정보 입력',
meetingDateTime: '약속 후보 날짜 선택',
meetingTimeRange: '약속 시간 범위 선택',
attendeeLogin: '내 정보 입력',
};

export const FIELD_LABELS = {
meetingName: '약속 이름',
nickname: '닉네임',
password: '비밀번호',
onlyDate: '날짜만 선택할래요',
};

export const FIELD_PLACEHOLDERS = {
meetingName: '10자 이내의 약속 이름 입력',
nickname: '5자 이내의 약속 닉네임 입력',
password: '4자리 숫자 입력',
};

export const FIELD_ERROR_MESSAGES = {
meetingName: '약속 이름은 1~10자 사이로 입력해 주세요.',
nickname: '닉네임은 1~5자 사이로 입력해 주세요.',
password: '비밀번호는 4자리 숫자로 입력해 주세요.',
date: '날짜를 하나씩 클릭해 여러 날짜를 선택하거나\n시작일과 종료일을 클릭해 사이의 모든 날짜를 선택해 보세요',
};
3 changes: 3 additions & 0 deletions frontend/src/constants/toasts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const TOAST_MESSAGES = {
OUT_OF_ONE_YEAR_RANGE: '최대 1년뒤의 약속만 생성할 수 있어요',
};
8 changes: 8 additions & 0 deletions frontend/src/hooks/__test__/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PropsWithChildren } from 'react';

import ToastProvider from '@contexts/ToastProvider';

// 필요한 _Provider 들은 유동적으로 추가해서 테스트 환경에서 사용할 수 있어요(@해리)
export default function Providers({ children }: PropsWithChildren) {
return <ToastProvider>{children}</ToastProvider>;
}
11 changes: 11 additions & 0 deletions frontend/src/hooks/__test__/renderHookWithProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RenderOptions } from '@testing-library/react';
import { renderHook } from '@testing-library/react';

import Providers from './Providers';

export default function render<T>(callback: () => T, options?: Omit<RenderOptions, 'wrapper'>) {
return renderHook(callback, {
wrapper: Providers,
...options,
});
}
Loading

0 comments on commit cf21b3d

Please sign in to comment.