Skip to content

Commit

Permalink
feat: Sidebar에 스크롤 시 특정 offset에서 화면에 고정되는 sticky 기능을 추가합니다. (#124)
Browse files Browse the repository at this point in the history
* feat: useWindowEventListener hook 구현

* feat: 요소가 특정 offset에 도달했는지 판별하는 useIsOverlap hook 구현

* feat: Sidebar 컴포넌트 구현

* feat: TagsLayout 컴포넌트에 Sidebar 컴포넌트 적용
  • Loading branch information
limgyumin authored Sep 17, 2023
1 parent 5219609 commit 81b9cbb
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 6 deletions.
21 changes: 15 additions & 6 deletions app/(home)/@tags/(tags)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import type { PropsWithChildren } from "react";

import { Sidebar } from "components/shared/ui/sidebar";
import { HEADER_HEIGHT } from "constants/header";

const TAGS_TOP = 52;
const TAGS_STICKY_OFFSET = TAGS_TOP + HEADER_HEIGHT;

const TagsLayout = ({ children }: PropsWithChildren) => {
return (
<div className="absolute left-[100%] top-[52px] pl-10">
<div className="w-56 rounded-md bg-zinc-100 px-3 py-2">
<p className="mb-3 text-base font-medium text-zinc-800">Popular Tags</p>
{children}
</div>
</div>
<Sidebar.Root align="right" top={TAGS_TOP}>
<Sidebar.Sticky offset={TAGS_STICKY_OFFSET}>
<Sidebar.Content>
<Sidebar.Title>Popular Tags</Sidebar.Title>

{children}
</Sidebar.Content>
</Sidebar.Sticky>
</Sidebar.Root>
);
};

Expand Down
14 changes: 14 additions & 0 deletions components/shared/ui/sidebar/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ComponentPropsWithoutRef } from "react";
import { forwardRef } from "react";

import { clsx } from "lib/clsx";

type Props = ComponentPropsWithoutRef<"div">;

export const Content = forwardRef<HTMLDivElement, Props>(({ children, className, ...rest }, ref) => {
return (
<div ref={ref} {...rest} className={clsx("w-56 rounded-md bg-zinc-100 px-3 py-2", className)}>
{children}
</div>
);
});
11 changes: 11 additions & 0 deletions components/shared/ui/sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Content } from "./content";
import { Root } from "./root";
import { Sticky } from "./sticky";
import { Title } from "./title";

export const Sidebar = {
Root,
Content,
Title,
Sticky,
};
35 changes: 35 additions & 0 deletions components/shared/ui/sidebar/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ComponentPropsWithoutRef } from "react";
import { forwardRef } from "react";

import { clsx } from "lib/clsx";
import type { Override } from "types/utilities";

const enum Align {
LEFT = "left",
RIGHT = "right",
}

type AlignValue = "left" | "right";

type BaseProps = {
align: AlignValue;
top?: number;
};

type Props = Override<ComponentPropsWithoutRef<"div">, BaseProps>;

export const Root = forwardRef<HTMLDivElement, Props>(({ children, className, align, top = 0, ...rest }, ref) => {
return (
<div ref={ref} style={{ top }} {...rest} className={clsx(styles.base, styles.align[align], className)}>
<div className="relative">{children}</div>
</div>
);
});

const styles = {
base: "absolute px-10",
align: {
[Align.LEFT]: "right-[100%]",
[Align.RIGHT]: "left-[100%]",
},
} as const;
28 changes: 28 additions & 0 deletions components/shared/ui/sidebar/sticky.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import type { ComponentPropsWithoutRef } from "react";
import { forwardRef } from "react";

import { useIsOverlap } from "hooks/use-is-overlap";
import { clsx } from "lib/clsx";

type Props = ComponentPropsWithoutRef<"div"> & {
offset: number;
};

export const Sticky = forwardRef<HTMLDivElement, Props>(({ children, className, offset, ...rest }, ref) => {
const [targetRef, isOverlap] = useIsOverlap<HTMLDivElement>(offset);

return (
<div ref={targetRef} className="absolute inset-0">
<div
ref={ref}
style={{ top: isOverlap ? offset : "auto" }}
{...rest}
className={clsx({ fixed: isOverlap }, className)}
>
{children}
</div>
</div>
);
});
14 changes: 14 additions & 0 deletions components/shared/ui/sidebar/title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ComponentPropsWithoutRef } from "react";
import { forwardRef } from "react";

import { clsx } from "lib/clsx";

type Props = ComponentPropsWithoutRef<"h3">;

export const Title = forwardRef<HTMLHeadingElement, Props>(({ children, className, ...rest }, ref) => {
return (
<h3 ref={ref} {...rest} className={clsx("mb-3 text-base font-medium text-zinc-800", className)}>
{children}
</h3>
);
});
1 change: 1 addition & 0 deletions constants/header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const HEADER_HEIGHT = 40;
24 changes: 24 additions & 0 deletions hooks/use-is-overlap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { MutableRefObject } from "react";
import { useCallback, useRef, useState } from "react";

import { useWindowEventListener } from "hooks/use-window-event-listener";

export const useIsOverlap = <T extends HTMLElement>(offset: number): [ref: MutableRefObject<T | null>, boolean] => {
const [isOverlap, setIsOverlap] = useState<boolean>(false);
const ref = useRef<T | null>(null);

const handleScroll = useCallback(() => {
if (ref.current === null) {
return;
}

const rect = ref.current.getBoundingClientRect();

setIsOverlap(rect.top <= offset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useWindowEventListener("scroll", handleScroll);

return [ref, isOverlap];
};
14 changes: 14 additions & 0 deletions hooks/use-window-event-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* global WindowEventMap */

import { useEffect } from "react";

export const useWindowEventListener = <K extends keyof WindowEventMap>(
type: K,
listener: (ev: WindowEventMap[K]) => any,
) => {
useEffect(() => {
window.addEventListener(type, listener);

return () => window.removeEventListener(type, listener);
}, [type, listener]);
};

0 comments on commit 81b9cbb

Please sign in to comment.