Skip to content

Commit

Permalink
[FE] refactor: scrollTop에서 ref로 접근하는 방식으로 변경 (#639)
Browse files Browse the repository at this point in the history
* refactor: 스크롤 이벤트를 엘리먼트에 접근하지 않고 ref로 접근하게 구현

* feat: 스크롤 버튼 디자인 수정

* feat: 상품 상세 페이지에도 스크롤 적용

* refactor: hook이 최상단에 있게끔 수정

* feat: 마이페이지에서도 스크롤 버튼 추가

* refactor: scrollTop 함수가 ref를 받도록 수정

* refactor: 스크롤 버튼에서 불필요한 useEffect삭제

* refactor: useScroll 훅 리팩토링

* refactor: useScrollRestoration 훅을 컴포넌트에서 페이지로 이동

* refactor: recipeRef를 컴포넌트에서 페이지로 이동

* fix: if문 setTimeout 안으로 이동
  • Loading branch information
xodms0309 authored Sep 15, 2023
1 parent 9778f73 commit b17c35b
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 68 deletions.
29 changes: 14 additions & 15 deletions frontend/src/components/Common/ScrollButton/ScrollButton.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,47 @@
import { Button } from '@fun-eat/design-system';
import { useState, useEffect } from 'react';
import type { RefObject } from 'react';
import { styled } from 'styled-components';

import SvgIcon from '../Svg/SvgIcon';

import { useScroll } from '@/hooks/common';

interface ScrollButtonProps {
targetRef: RefObject<HTMLElement>;
isRecipePage?: boolean;
}

const ScrollButton = ({ isRecipePage = false }: ScrollButtonProps) => {
const ScrollButton = ({ targetRef, isRecipePage = false }: ScrollButtonProps) => {
const { scrollToTop } = useScroll();
const [scrollTop, setScrollTop] = useState(false);

const handleScroll = () => {
setScrollTop(true);
};

useEffect(() => {
const mainElement = document.getElementById('main');
if (mainElement) {
scrollToTop(mainElement);
setScrollTop(false);
if (targetRef) {
scrollToTop(targetRef);
}
}, [scrollTop]);
};

return (
<ScrollButtonWrapper
isRecipePage={isRecipePage}
customWidth="45px"
customHeight="45px"
variant="filled"
color="gray5"
color="white"
onClick={handleScroll}
>
<SvgIcon variant="triangle" color="white" width={16} height={14} />
<SvgIcon variant="arrow" color="gray5" width={16} height={14} />
</ScrollButtonWrapper>
);
};

export default ScrollButton;

const ScrollButtonWrapper = styled(Button)<ScrollButtonProps>`
const ScrollButtonWrapper = styled(Button)<Pick<ScrollButtonProps, 'isRecipePage'>>`
position: fixed;
bottom: ${({ isRecipePage }) => (isRecipePage ? '210px' : '90px')};
right: 20px;
border-radius: 50%;
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 4px;
@media screen and (min-width: 600px) {
left: calc(50% + 234px);
Expand All @@ -56,4 +51,8 @@ const ScrollButtonWrapper = styled(Button)<ScrollButtonProps>`
transform: scale(1.1);
transition: all 200ms ease-in-out;
}
svg {
rotate: 90deg;
}
`;
22 changes: 7 additions & 15 deletions frontend/src/components/Product/ProductList/ProductList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styled from 'styled-components';
import ProductItem from '../ProductItem/ProductItem';

import { PATH } from '@/constants/path';
import { useIntersectionObserver, useScrollRestoration } from '@/hooks/common';
import { useIntersectionObserver } from '@/hooks/common';
import { useCategoryValueContext } from '@/hooks/context';
import { useInfiniteProductsQuery } from '@/hooks/queries/product';
import type { CategoryVariant, SortOption } from '@/types/common';
Expand All @@ -18,8 +18,6 @@ interface ProductListProps {

const ProductList = ({ category, selectedOption }: ProductListProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
const productListRef = useRef<HTMLDivElement>(null);

const { categoryIds } = useCategoryValueContext();

const { fetchNextPage, hasNextPage, data } = useInfiniteProductsQuery(
Expand All @@ -29,33 +27,27 @@ const ProductList = ({ category, selectedOption }: ProductListProps) => {

useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

useScrollRestoration(categoryIds[category], productListRef);

const productList = data.pages.flatMap((page) => page.products);

return (
<ProductListContainer ref={productListRef}>
<ProductListWrapper>
<>
<ProductListContainer>
{productList.map((product) => (
<li key={product.id}>
<Link as={RouterLink} to={`${PATH.PRODUCT_LIST}/${category}/${product.id}`}>
<ProductItem product={product} />
</Link>
</li>
))}
</ProductListWrapper>
</ProductListContainer>
<div ref={scrollRef} aria-hidden />
</ProductListContainer>
</>
);
};
export default ProductList;

const ProductListContainer = styled.div`
height: calc(100% - 150px);
overflow-y: auto;
`;
export default ProductList;

const ProductListWrapper = styled.ul`
const ProductListContainer = styled.ul`
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/contexts/CategoryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const initialState = {
store: 7,
};

type CategoryIds = {
export type CategoryIds = {
[k in CategoryVariant]: number;
};

Expand Down
15 changes: 10 additions & 5 deletions frontend/src/hooks/common/useScroll.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { RefObject } from 'react';

const useScroll = () => {
const scrollToTop = (mainElement: HTMLElement) => {
mainElement.scrollTo(0, 0);
const scrollToTop = <T extends HTMLElement>(ref: RefObject<T>) => {
if (ref.current) {
ref.current.scrollTo(0, 0);
}
};

const scrollToPosition = <T extends HTMLElement>(ref?: RefObject<T>) => {
setTimeout(() => {
ref?.current?.scrollIntoView({ behavior: 'smooth' });
const scrollToPosition = <T extends HTMLElement>(ref: RefObject<T>) => {
const timeout = setTimeout(() => {
if (ref.current) {
ref.current.scrollIntoView({ behavior: 'smooth' });
clearTimeout(timeout);
}
}, 100);
};

Expand Down
3 changes: 1 addition & 2 deletions frontend/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Suspense } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styled from 'styled-components';

import { ScrollButton, Loading, ErrorBoundary, ErrorComponent, CategoryList } from '@/components/Common';
import { Loading, ErrorBoundary, ErrorComponent, CategoryList } from '@/components/Common';
import { ProductRankingList, ReviewRankingList, RecipeRankingList } from '@/components/Rank';
import { IMAGE_URL } from '@/constants';
import channelTalk from '@/service/channelTalk';
Expand Down Expand Up @@ -75,7 +75,6 @@ const HomePage = () => {
</ErrorBoundary>
</SectionWrapper>
<Spacing size={36} />
<ScrollButton />
</>
);
};
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/pages/MemberRecipePage.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { Spacing } from '@fun-eat/design-system';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { Suspense } from 'react';
import { Suspense, useRef } from 'react';
import styled from 'styled-components';

import { ErrorBoundary, ErrorComponent, Loading, ScrollButton, SectionTitle } from '@/components/Common';
import { MemberRecipeList } from '@/components/Members';

const MemberRecipePage = () => {
const { reset } = useQueryErrorResetBoundary();
const memberRecipeRef = useRef<HTMLDivElement>(null);

return (
<MemberRecipePageContainer>
<MemberRecipePageContainer ref={memberRecipeRef}>
<SectionTitle name="내가 작성한 꿀조합" />
<Spacing size={18} />
<ErrorBoundary fallback={ErrorComponent} handleReset={reset}>
<Suspense fallback={<Loading />}>
<MemberRecipeList />
</Suspense>
</ErrorBoundary>
<ScrollButton />
<ScrollButton targetRef={memberRecipeRef} />
<Spacing size={40} />
</MemberRecipePageContainer>
);
Expand All @@ -27,5 +28,7 @@ const MemberRecipePage = () => {
export default MemberRecipePage;

const MemberRecipePageContainer = styled.div`
height: 100%;
padding: 20px 20px 0;
overflow-y: auto;
`;
9 changes: 6 additions & 3 deletions frontend/src/pages/MemberReviewPage.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { Spacing } from '@fun-eat/design-system';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { Suspense } from 'react';
import { Suspense, useRef } from 'react';
import styled from 'styled-components';

import { ErrorBoundary, ErrorComponent, Loading, ScrollButton, SectionTitle } from '@/components/Common';
import { MemberReviewList } from '@/components/Members';

const MemberReviewPage = () => {
const { reset } = useQueryErrorResetBoundary();
const memberReviewRef = useRef<HTMLDivElement>(null);

return (
<MemberReviewPageContainer>
<MemberReviewPageContainer ref={memberReviewRef}>
<SectionTitle name="내가 작성한 리뷰" />
<Spacing size={18} />
<ErrorBoundary fallback={ErrorComponent} handleReset={reset}>
<Suspense fallback={<Loading />}>
<MemberReviewList />
</Suspense>
</ErrorBoundary>
<ScrollButton />
<ScrollButton targetRef={memberReviewRef} />
<Spacing size={40} />
</MemberReviewPageContainer>
);
Expand All @@ -27,5 +28,7 @@ const MemberReviewPage = () => {
export default MemberReviewPage;

const MemberReviewPageContainer = styled.div`
height: 100%;
padding: 20px 20px 0;
overflow-y: auto;
`;
30 changes: 20 additions & 10 deletions frontend/src/pages/ProductDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,19 @@ const ProductDetailPage = () => {
const { data: productDetail } = useProductDetailQuery(Number(productId));
const { reset } = useQueryErrorResetBoundary();

const tabMenus = [`리뷰 ${productDetail.reviewCount}`, '꿀조합'];
const { selectedTabMenu, isFirstTabMenu: isReviewTab, handleTabMenuClick, initTabMenu } = useTabMenu();
const tabRef = useRef<HTMLUListElement>(null);

const sortOptions = isReviewTab ? REVIEW_SORT_OPTIONS : RECIPE_SORT_OPTIONS;
const initialSortOption = isReviewTab ? REVIEW_SORT_OPTIONS[0] : RECIPE_SORT_OPTIONS[0];

const { selectedOption, selectSortOption } = useSortOption(initialSortOption);
const { selectedOption, selectSortOption } = useSortOption(REVIEW_SORT_OPTIONS[0]);
const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();

const [activeSheet, setActiveSheet] = useState<'registerReview' | 'sortOption'>('sortOption');

const productDetailPageRef = useRef<HTMLDivElement>(null);

const tabMenus = [`리뷰 ${productDetail.reviewCount}`, '꿀조합'];
const sortOptions = isReviewTab ? REVIEW_SORT_OPTIONS : RECIPE_SORT_OPTIONS;
const currentSortOption = isReviewTab ? REVIEW_SORT_OPTIONS[0] : RECIPE_SORT_OPTIONS[0];

if (!category) {
return null;
}
Expand All @@ -64,7 +65,7 @@ const ProductDetailPage = () => {

const handleTabMenuSelect = (index: number) => {
handleTabMenuClick(index);
selectSortOption(initialSortOption);
selectSortOption(currentSortOption);

ReactGA.event({
category: '버튼',
Expand All @@ -74,7 +75,7 @@ const ProductDetailPage = () => {
};

return (
<>
<ProductDetailPageContainer ref={productDetailPageRef}>
<SectionTitle name={productDetail.name} bookmark={productDetail.bookmark} />
<Spacing size={36} />
<ProductDetailItem category={category} productDetail={productDetail} />
Expand Down Expand Up @@ -122,7 +123,7 @@ const ProductDetailPage = () => {
onClick={handleOpenRegisterReviewSheet}
/>
</ReviewRegisterButtonWrapper>
<ScrollButton />
<ScrollButton targetRef={productDetailPageRef} />
<BottomSheet maxWidth="600px" ref={ref} isClosing={isClosing} close={handleCloseBottomSheet}>
{activeSheet === 'registerReview' ? (
<ReviewFormProvider>
Expand All @@ -142,12 +143,21 @@ const ProductDetailPage = () => {
/>
)}
</BottomSheet>
</>
</ProductDetailPageContainer>
);
};

export default ProductDetailPage;

const ProductDetailPageContainer = styled.div`
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`;

const SortButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
Expand Down
Loading

0 comments on commit b17c35b

Please sign in to comment.