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

feat: 내비게이션 바 애니메이션 추가 #435

Merged
merged 12 commits into from
Oct 19, 2023
100 changes: 22 additions & 78 deletions frontend/src/components/@common/Navbar/index.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R

전체적으로 Roof관련된 로직을 훅으로 분리하면 좀 더 깔끔할 것 같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R

아까 말한 프레임 드랍도 같이 해결하면 좋을 것 같아요!

Copy link
Member Author

@WaiNaat WaiNaat Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 이걸 훅으로 분리한다고 관심사 분리가 잘 될지는 모르겠네요..
roof position 자체가 navbar 내용물 순서에 종속되어 있는데 이 내용들이 jsx에 하드코딩되어 있기 때문에 어디를 분리하는 게 맞는지 잘 모르겠어요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestAnimationFrame도 찾아보니 화면 주사율에 따라서 호출 횟수가 달라서 이동 시간을 모든 모니터에서 동일하게 맞출 수 있는 방법이 있을까요?

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { Link, matchRoutes, useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { Button, Roof, Wrapper } from './Navbar.style';
import { isShowPageLoadingState } from 'store/atoms/@common';
import useAddToast from 'hooks/@common/useAddToast';
import useNavbarRoofAnimation from 'hooks/@common/useNavbarRoofAnimation';
import useCheckSessionId from 'hooks/queries/auth/useCheckSessionId';
import { URL_PATH } from 'constants/index';
import NavItem from './NavItem';
Expand All @@ -16,47 +15,16 @@ const NO_NAVIGATION_BAR_URLS = [
URL_PATH.authorization,
].map((path) => ({ path }));

const getRoofPosition = (pathname: string) => {
switch (pathname) {
case URL_PATH.main:
return 1;
case URL_PATH.garden:
return 2;
case URL_PATH.reminder:
return 3;
case URL_PATH.petList:
return 4;
case URL_PATH.myPage:
return 5;
default:
return null;
}
};

const Navbar = () => {
const navigate = useNavigate();
const { pathname, state } = useLocation();
const navBar = useRef<HTMLElement>(null);
const navItemPositions = useRef<number[]>([]);

const isPageLoading = useRecoilValue(isShowPageLoadingState);

const addToast = useAddToast();
const { isSuccess: isLoggedIn } = useCheckSessionId(false);

useEffect(() => {
const resizeObserver = new ResizeObserver(([entry]) => {
navItemPositions.current = Array.from(entry.target.children)
.slice(0, -1)
.map((child) => {
const { left } = child.getBoundingClientRect();
return left;
});
});

if (navBar.current) resizeObserver.observe(navBar.current);
return () => resizeObserver.disconnect();
}, []);
const { navbarRef, roofPosition, transitionOffset } = useNavbarRoofAnimation(
state ? state.prevPathname ?? pathname : pathname,
pathname
);

useEffect(() => {
const resetHistoryState = () => history.replaceState(null, '');
Expand All @@ -78,48 +46,28 @@ const Navbar = () => {
});
};

const prevPathname = state ? state.prevPathname : pathname;

const isActive = (targetPathname: string) => {
if (isPageLoading) {
return targetPathname === prevPathname;
}
return targetPathname === pathname;
};

const hideNavbar = matchRoutes(NO_NAVIGATION_BAR_URLS, pathname) !== null;

const prevRoofPosition = getRoofPosition(prevPathname);
const roofPosition = isPageLoading ? prevRoofPosition : getRoofPosition(pathname);

const transitionOffset =
roofPosition && prevRoofPosition
? navItemPositions.current[prevRoofPosition - 1] - navItemPositions.current[roofPosition - 1]
: 0;
const newHistoryState = { prevPathname: pathname };

return (
<Wrapper ref={navBar} $hide={hideNavbar}>
<Button as={Link} to={URL_PATH.main} state={{ prevPathname: pathname }}>
<NavItem isActive={isActive(URL_PATH.main)} iconId="home-line" label="메인" />
<Wrapper ref={navbarRef} $hide={hideNavbar}>
<Button as={Link} to={URL_PATH.main} state={newHistoryState}>
<NavItem isActive={roofPosition === 1} iconId="home-line" label="메인" />
</Button>
<Button as={Link} to={URL_PATH.garden} state={{ prevPathname: pathname }}>
<NavItem
isActive={isActive(URL_PATH.garden)}
iconId="bulletin-board-line"
label="모두의 정원"
/>
<Button as={Link} to={URL_PATH.garden} state={newHistoryState}>
<NavItem isActive={roofPosition === 2} iconId="bulletin-board-line" label="모두의 정원" />
</Button>
{isLoggedIn ? (
<>
<Button as={Link} to={URL_PATH.reminder} state={{ prevPathname: pathname }}>
<NavItem isActive={isActive(URL_PATH.reminder)} iconId="reminder" label="리마인더" />
<Button as={Link} to={URL_PATH.reminder} state={newHistoryState}>
<NavItem isActive={roofPosition === 3} iconId="reminder" label="리마인더" />
</Button>
<Button as={Link} to={URL_PATH.petList} state={{ prevPathname: pathname }}>
<NavItem isActive={isActive(URL_PATH.petList)} iconId="leaf" label="내 식물" />
<Button as={Link} to={URL_PATH.petList} state={newHistoryState}>
<NavItem isActive={roofPosition === 4} iconId="leaf" label="내 식물" />
</Button>
<Button as={Link} to={URL_PATH.myPage} state={{ prevPathname: pathname }}>
<Button as={Link} to={URL_PATH.myPage} state={newHistoryState}>
<NavItem
isActive={isActive(URL_PATH.myPage)}
isActive={roofPosition === 5}
iconId="account-circle-line"
label="마이페이지"
/>
Expand All @@ -128,17 +76,13 @@ const Navbar = () => {
) : (
<>
<Button type="button" onClick={askLogin}>
<NavItem isActive={isActive(URL_PATH.reminder)} iconId="reminder" label="리마인더" />
<NavItem isActive={false} iconId="reminder" label="리마인더" />
</Button>
<Button type="button" onClick={askLogin}>
<NavItem isActive={isActive(URL_PATH.petList)} iconId="leaf" label="내 식물" />
<NavItem isActive={false} iconId="leaf" label="내 식물" />
</Button>
<Button as={Link} to={URL_PATH.login} state={{ prevPathname: pathname }}>
<NavItem
isActive={isActive(URL_PATH.login)}
iconId="account-circle-line"
label="로그인"
/>
<Button as={Link} to={URL_PATH.login} state={newHistoryState}>
<NavItem isActive={false} iconId="account-circle-line" label="로그인" />
</Button>
</>
)}
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/hooks/@common/useChildrenLeftPositions.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useRef } from 'react';

const useChildrenLeftPositions = <T extends HTMLElement>(domRef: React.RefObject<T>) => {
const childrenPositions = useRef<number[]>([]);

useEffect(() => {
const resizeObserver = new ResizeObserver(([entry]) => {
childrenPositions.current = Array.from(entry.target.children).map(
(child) => child.getBoundingClientRect().left
);
});

if (domRef.current) resizeObserver.observe(domRef.current);
return () => resizeObserver.disconnect();
}, [domRef]);

return childrenPositions.current;
};

export default useChildrenLeftPositions;
50 changes: 50 additions & 0 deletions frontend/src/hooks/@common/useNavbarRoofAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { isShowPageLoadingState } from 'store/atoms/@common';
import { URL_PATH } from 'constants/index';
import useChildrenLeftPositions from './useChildrenLeftPositions';

const getRoofPosition = (pathname: string) => {
switch (pathname) {
case URL_PATH.main:
return 1;
case URL_PATH.garden:
return 2;
case URL_PATH.reminder:
return 3;
case URL_PATH.petList:
return 4;
case URL_PATH.myPage:
return 5;
default:
return null;
}
};

const getAnimationOffset = (prevPathname: string, currentPathname: string, positions: number[]) => {
const prevRoofPosition = getRoofPosition(prevPathname);
const roofPosition = getRoofPosition(currentPathname);

const transitionOffset =
roofPosition && prevRoofPosition
? positions[prevRoofPosition - 1] - positions[roofPosition - 1]
: 0;

return transitionOffset !== 0 ? transitionOffset + Math.random() : 0;
};

const useNavbarRoofAnimation = (prevPathname: string, currentPathname: string) => {
const navbarRef = useRef<HTMLElement>(null);
const isPageLoading = useRecoilValue(isShowPageLoadingState);

const navItemPositions = useChildrenLeftPositions(navbarRef);

const roofPosition = getRoofPosition(isPageLoading ? prevPathname : currentPathname);
const transitionOffset = !isPageLoading
? getAnimationOffset(prevPathname, currentPathname, navItemPositions)
: 0;

return { navbarRef, roofPosition, transitionOffset };
};

export default useNavbarRoofAnimation;
2 changes: 2 additions & 0 deletions frontend/src/pages/auth/MyPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FixedButtonArea } from 'pages/garden/GardenPostList/GardenPostList.style';
import ContentHeader from 'components/@common/ContentHeader';
import Footer from 'components/@common/Footer';
import PageLogger from 'components/@common/PageLogger';
import SvgFill from 'components/@common/SvgIcons/SvgFill';
import Toggle from 'components/@common/Toggle';
Expand Down Expand Up @@ -75,6 +76,7 @@ const MyPage = () => {
</Button>
</ButtonBox>
</Main>
<Footer />
<FixedButtonArea>
<BottomSheet to="https://forms.gle/rQUAi9GbVwrr7oG2A" target="blank">
<SvgFill icon="survey" color={theme.color.background} size={16} />
Expand Down