Skip to content
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

모달 컴포넌트 완성 #523

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
79b9ed8
design(TagButton): 포커스 시 UI 사이즈 변경 문제 해결
vi-wolhwa Aug 20, 2024
d264e10
refactor(TagButton): 태그버튼 하이라이팅 시 사이즈 변경되는 문제 해결
vi-wolhwa Aug 21, 2024
d5f2f5c
refactor(src): TagFilterMenu 디자인 리팩토링
vi-wolhwa Aug 21, 2024
96e4a87
design(TagButton): 포커스 시 UI 사이즈 변경 문제 해결
vi-wolhwa Aug 20, 2024
beb25b8
refactor(TagButton): 태그버튼 하이라이팅 시 사이즈 변경되는 문제 해결
vi-wolhwa Aug 21, 2024
503bb4e
refactor(src): TagFilterMenu 디자인 리팩토링
vi-wolhwa Aug 21, 2024
fb8f9e2
Merge branch '#510/tagbox_redesign' of https://github.com/woowacourse…
vi-wolhwa Aug 21, 2024
15aff1e
refactor(TagFilterMenu): useToggle를 사용하여 태그 박스 Open 상태 관리
vi-wolhwa Aug 21, 2024
acc3ba8
refactor(TagFilterMenu): 더보기 버튼 사이즈 상수로 관리
vi-wolhwa Aug 21, 2024
c27f366
design(TagButton): 태그버튼 하이라이팅 시 테두리 두께 유지
vi-wolhwa Aug 21, 2024
fa832b2
refactor(MyTemplatePage): 불필요하게 렌더링되는 Flex 컴포넌트 삭제제
vi-wolhwa Aug 21, 2024
6c5564f
refactor(TagFilterMenu): 태그 더보기 버튼 반응형 노출
vi-wolhwa Aug 21, 2024
5c01cb9
refactor(Modal): 모달 컴포넌트 완성
vi-wolhwa Aug 21, 2024
1c4cc05
refactor(Modal): 모달 컴포넌트 완성
vi-wolhwa Aug 21, 2024
46885d2
Merge branch 'refactor/510-modal' of https://github.com/woowacourse-t…
vi-wolhwa Aug 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';

import { Heading, Text, Modal, Input, Flex, Button } from '@/components';
import { Text, Modal, Input, Flex, Button } from '@/components';
import { useCategoryNameValidation } from '@/hooks/category';
import { useCategoryDeleteMutation, useCategoryEditMutation, useCategoryUploadMutation } from '@/queries/category';
import type { Category, CustomError } from '@/types';
Expand Down Expand Up @@ -116,9 +116,7 @@ const CategoryEditModal = ({ isOpen, toggleModal, categories, handleCancelEdit }

return (
<Modal isOpen={isOpen} toggleModal={handleCancelEditWithReset} size='small'>
<Modal.Header>
<Heading.XSmall color={theme.color.light.secondary_900}>카테고리 편집</Heading.XSmall>
</Modal.Header>
<Modal.Title>{'카테고리 편집'}</Modal.Title>
<Modal.Body>
<S.EditCategoryItemList>
<CategoryItems
Expand Down
37 changes: 20 additions & 17 deletions frontend/src/components/Modal/Modal.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from '@emotion/styled';

import { ModalSize } from './Modal';

export const Container = styled.div`
export const Base = styled.div`
position: fixed;
z-index: 200;
top: 0;
Expand Down Expand Up @@ -33,37 +33,45 @@ export const Backdrop = styled.div`
background-color: rgba(0, 0, 0, 0.3);
`;

export const Header = styled.div`
margin-bottom: 1rem;
font-size: 1.25rem;
font-weight: bold;
export const TitleWrapper = styled.div`
display: flex;
flex-shrink: 0;
align-items: center;
padding-bottom: 1rem;
`;

export const Body = styled.div`
margin-bottom: 1.5rem;
export const BodyContainer = styled.div`
overflow-y: auto;
flex-grow: 1;
font-size: 1rem;
`;

export const Footer = styled.div`
export const FooterContainer = styled.div`
display: flex;
flex-shrink: 0;
gap: 1rem;
justify-content: space-between;

padding-top: 1rem;
`;

export const Base = styled.div<{ size: ModalSize }>`
export const ModalContainer = styled.div<{ size: ModalSize }>`
position: relative;
z-index: 202;

display: flex;
flex-direction: column;
gap: 1rem;

max-height: 90vh;
padding: 1.5rem;

background-color: white;
border-radius: 24px;

@media (min-width: 48rem) {
position: fixed;
padding: 1.5rem;
background-color: white;

width: 90%;
${({ size }) => size && sizes[size]};
}

Expand All @@ -74,30 +82,25 @@ export const Base = styled.div<{ size: ModalSize }>`
overflow-y: auto;

width: 100%;
max-height: 90vh;

border-radius: 1.5rem 1.5rem 0 0;
}
`;

const sizes = {
xsmall: css`
width: 90%;
max-width: 17.5rem;
min-height: 11.25rem;
`,
small: css`
width: 90%;
max-width: 25rem;
min-height: 18.75rem;
`,
medium: css`
width: 90%;
max-width: 37.5rem;
min-height: 28.125rem;
`,
large: css`
width: 90%;
max-width: 50rem;
min-height: 37.5rem;
`,
Expand Down
37 changes: 19 additions & 18 deletions frontend/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { HTMLAttributes, PropsWithChildren } from 'react';
import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import { createPortal } from 'react-dom';

import { theme } from '../../style/theme';
import Heading from '../Heading/Heading';
import * as S from './Modal.style';

export type ModalSize = 'xsmall' | 'small' | 'medium' | 'large';
Expand All @@ -11,43 +13,42 @@ export interface BaseProps extends HTMLAttributes<HTMLDivElement> {
size?: ModalSize;
}

const Base = ({
isOpen,
toggleModal,
size = 'small',

children,
...rests
}: PropsWithChildren<BaseProps>) => {
const Base = ({ isOpen, toggleModal, size = 'small', children, ...props }: PropsWithChildren<BaseProps>) => {
if (!isOpen) {
return null;
}

return createPortal(
<S.Container>
<S.Base>
<S.Backdrop onClick={toggleModal} />
<S.Base size={size} {...rests}>
<S.ModalContainer size={size} {...props}>
{children}
</S.Base>
</S.Container>,
</S.ModalContainer>
</S.Base>,
document.body,
);
};

const Header = ({ children, ...props }: PropsWithChildren<HTMLAttributes<HTMLDivElement>>) => (
<S.Header {...props}>{children}</S.Header>
const Title = ({ children }: { children: ReactNode }) => (
<S.TitleWrapper>
{typeof children === 'string' ? (
<Heading.XSmall color={theme.color.light.secondary_900}>{children}</Heading.XSmall>
) : (
children
)}
</S.TitleWrapper>
);

const Body = ({ children, ...props }: PropsWithChildren<HTMLAttributes<HTMLDivElement>>) => (
<S.Body {...props}>{children}</S.Body>
<S.BodyContainer {...props}>{children}</S.BodyContainer>
);

const Footer = ({ children, ...props }: PropsWithChildren<HTMLAttributes<HTMLDivElement>>) => (
<S.Footer {...props}>{children}</S.Footer>
<S.FooterContainer {...props}>{children}</S.FooterContainer>
);

const Modal = Object.assign(Base, {
Header,
Title,
Body,
Footer,
});
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/TagButton/TagButton.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export const TagButtonWrapper = styled.button<{ isFocused: boolean }>`
justify-content: center;

box-sizing: border-box;
padding: 0.25rem 0.75rem;
height: 1.75rem;
padding: 0 0.75rem;

background-color: ${({ isFocused }) => (isFocused ? theme.color.light.primary_400 : theme.color.light.tertiary_50)};
border: ${({ isFocused }) =>
isFocused ? `2px solid ${theme.color.light.primary_600}` : `1px solid ${theme.color.light.tertiary_200}`};
border: 1px solid ${({ isFocused }) => (isFocused ? theme.color.light.primary_600 : theme.color.light.tertiary_200)};
border-radius: 2.5rem;

&:not(:disabled):hover {
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/components/TagFilterMenu/TagFilterMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';

import { tags } from '@/mocks/tagList.json';
import TagFilterMenu from './TagFilterMenu';

const meta: Meta<typeof TagFilterMenu> = {
title: 'TagFilterMenu',
component: TagFilterMenu,
args: { tags },
};

export default meta;

type Story = StoryObj<typeof TagFilterMenu>;

export const Default: Story = {
args: {
selectedTagIds: [],
},
};

export const Selected: Story = {
args: {
selectedTagIds: [1, 5, 6, 9],
},
};
37 changes: 34 additions & 3 deletions frontend/src/components/TagFilterMenu/TagFilterMenu.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,44 @@ import { theme } from '@/style/theme';

export const TagFilterMenuContainer = styled.div`
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: 1rem;
align-items: flex-start;

width: 12.5rem;
width: 100%;
padding: 1rem;

border: 1px solid ${theme.color.light.secondary_300};
border-radius: 8px;
`;

export const TagButtonsContainer = styled.div<{ height: string }>`
overflow: hidden;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: flex-start;

width: 100%;
height: ${({ height }) => height};

transition: height 0.3s ease-in-out;
`;

export const ShowMoreButton = styled.button<{ size: number; isExpanded: boolean }>`
cursor: pointer;

display: flex;
align-items: center;
justify-content: center;

width: ${({ size }) => `${size}rem`};
height: ${({ size }) => `${size}rem`};

transition: transform 0.3s ease-in-out;

${({ isExpanded }) =>
isExpanded &&
`
transform: rotate(180deg);
`}
`;
68 changes: 60 additions & 8 deletions frontend/src/components/TagFilterMenu/TagFilterMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,84 @@
import { useState, useRef, useEffect } from 'react';

import { ChevronIcon } from '@/assets/images';
import { TagButton } from '@/components';
import { useToggle, useWindowWidth } from '@/hooks/utils';
import type { Tag } from '@/types';
import { remToPx } from '@/utils';
import * as S from './TagFilterMenu.style';

const LINE_HEIGHT_REM = 1.875;

interface Props {
tags: Tag[];
selectedTagIds: number[];
onSelectTags: (selectedTagIds: number[]) => void;
}

const TagFilterMenu = ({ tags, selectedTagIds, onSelectTags }: Props) => {
const [deselectedTags, setDeselectedTags] = useState<Tag[]>([]);
const [isTagBoxOpen, toggleTagBox] = useToggle(false);
const [height, setHeight] = useState('auto');
const containerRef = useRef<HTMLDivElement>(null);
const [showMoreButton, setShowMoreButton] = useState(false);
const windowWidth = useWindowWidth();

const updateTagContainerState = () => {
if (containerRef.current) {
const containerHeight = containerRef.current.scrollHeight;

setHeight(isTagBoxOpen ? `${containerHeight}px` : `${LINE_HEIGHT_REM}rem`);

if (containerHeight > remToPx(LINE_HEIGHT_REM)) {
setShowMoreButton(true);
} else {
setShowMoreButton(false);
}
}
};

useEffect(() => {
updateTagContainerState();
}, [tags, selectedTagIds, isTagBoxOpen, windowWidth]);

const handleButtonClick = (tagId: number) => {
if (selectedTagIds.includes(tagId)) {
const deselectedTag = tags.find((tag) => tag.id === tagId);

if (deselectedTag) {
setDeselectedTags((prev) => [deselectedTag, ...prev.filter((tag) => tag.id !== tagId)]);
}

onSelectTags(selectedTagIds.filter((id) => id !== tagId));
} else {
setDeselectedTags((prev) => prev.filter((tag) => tag.id !== tagId));
onSelectTags([...selectedTagIds, tagId]);
}
};

const selectedTags = selectedTagIds.map((id) => tags.find((tag) => tag.id === id)!).filter(Boolean);

const unselectedTags = deselectedTags.concat(
tags.filter(
(tag) => !selectedTagIds.includes(tag.id) && !deselectedTags.some((deselectedTag) => deselectedTag.id === tag.id),
),
);

return (
<S.TagFilterMenuContainer data-testid='tag-filter-menu'>
{tags.map((tag) => (
<TagButton
key={tag.id}
name={tag.name}
isFocused={selectedTagIds.includes(tag.id)}
onClick={() => handleButtonClick(tag.id)}
/>
))}
<S.TagButtonsContainer ref={containerRef} height={height}>
{selectedTags.map((tag) => (
<TagButton key={tag.id} name={tag.name} isFocused={true} onClick={() => handleButtonClick(tag.id)} />
))}
{unselectedTags.map((tag) => (
<TagButton key={tag.id} name={tag.name} isFocused={false} onClick={() => handleButtonClick(tag.id)} />
))}
</S.TagButtonsContainer>
{showMoreButton && (
<S.ShowMoreButton size={LINE_HEIGHT_REM} onClick={toggleTagBox} isExpanded={isTagBoxOpen}>
<ChevronIcon width={16} height={16} aria-label='태그 더보기' />
</S.ShowMoreButton>
)}
</S.TagFilterMenuContainer>
);
};
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/MyTemplatesPage/MyTemplatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ const MyTemplatePage = () => {
<S.MainContainer>
<Flex direction='column' gap='2.5rem' style={{ marginTop: '4.5rem' }}>
<CategoryFilterMenu categories={categories} onSelectCategory={handleCategoryMenuClick} />
{tags.length !== 0 && (
<TagFilterMenu tags={tags} selectedTagIds={selectedTagIds} onSelectTags={handleTagMenuClick} />
)}
</Flex>

<Flex direction='column' width='100%' gap='1rem'>
Expand Down Expand Up @@ -154,6 +151,9 @@ const MyTemplatePage = () => {
getOptionLabel={(option) => option.value}
/>
</Flex>
{tags.length && (
<TagFilterMenu tags={tags} selectedTagIds={selectedTagIds} onSelectTags={handleTagMenuClick} />
)}
{templates.length ? (
<TemplateGrid
templates={templates}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { formatRelativeTime } from './formatRelativeTime';
export { getLanguageByFilename } from './getLanguageByFileName';
export { removeAllWhitespace } from './removeAllWhitespace';
export { remToPx } from './remToPx';
export { scroll } from './scroll';
1 change: 1 addition & 0 deletions frontend/src/utils/remToPx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const remToPx = (rem: number, rootFontSize: number = 16): number => rem * rootFontSize;