Skip to content

Commit

Permalink
Merge pull request #54 from alkong-dalkong/feature/#39_bottom_sheet
Browse files Browse the repository at this point in the history
feat: Bottom Sheet 컴포넌트 추가
  • Loading branch information
kimsuyeon0916 authored Aug 22, 2024
2 parents ea5f5e8 + bba8aa6 commit 6ebfb46
Show file tree
Hide file tree
Showing 20 changed files with 277 additions and 13 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ jobs:
with:
fetch-depth: 0

- name: Clean Yarn Cache
run: yarn cache clean --all

- name: Install dependencies
run: yarn install --immutable --check-cache

Expand Down
33 changes: 33 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@tanstack/react-query": "^5.51.15",
"axios": "^1.7.2",
"framer-motion": "^11.3.29",
"next": "14.2.4",
"react": "^18",
"react-dom": "^18",
Expand Down
10 changes: 6 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PropsWithChildren } from 'react'
import { Noto_Sans_KR } from 'next/font/google'

import QueryProvider from '@/hooks/QueryProvider'
import { LazyMotionProvider, QueryProvider } from '@/hooks'

import './globals.css'

Expand All @@ -17,9 +17,11 @@ export default function RootLayout({ children }: PropsWithChildren) {
<head />
<body className="flex-center font-medium">
<QueryProvider>
<div className="relative h-svh w-full min-w-[320px] max-w-[450px] overflow-y-scroll border-x scrollbar-hide">
{children}
</div>
<LazyMotionProvider>
<div className="relative h-svh w-full min-w-[320px] max-w-[450px] overflow-y-scroll border-x scrollbar-hide">
{children}
</div>
</LazyMotionProvider>
</QueryProvider>
</body>
</html>
Expand Down
32 changes: 32 additions & 0 deletions src/components/bottomSheet/BottomSheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type Meta } from '@storybook/react'
import { domMax, LazyMotion } from 'framer-motion'

import { useToggle } from '@/hooks'

import { BottomSheet } from './BottomSheet'

const meta: Meta<typeof BottomSheet> = {
title: 'BottomSheet',
component: BottomSheet,
}

export default meta

export function Default() {
const [isShowing, toggleShowing] = useToggle(true)

return (
<LazyMotion features={domMax}>
<button
className="rounded border border-blue-500 bg-transparent px-4 py-2 text-blue-700 hover:border-transparent hover:bg-blue-500 hover:text-white"
type="button"
onClick={toggleShowing}
>
toggle
</button>
<BottomSheet onClickScrim={toggleShowing} isShowing={isShowing}>
bottom sheet content
</BottomSheet>
</LazyMotion>
)
}
99 changes: 99 additions & 0 deletions src/components/bottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { type ComponentProps, type MouseEventHandler, useEffect } from 'react'
import { m, type Variants } from 'framer-motion'

import { zIndex } from '@/constants'
import { useScrollLock } from '@/hooks'

import { Icon } from '../icons'
import { AnimatePortal } from '../portal/AnimatePortal'

type BottomSheetProps = ComponentProps<typeof AnimatePortal> & {
onClickScrim?: VoidFunction
isShort?: boolean
}

export const BottomSheet = ({
onClickScrim,
isShort = false,
isShowing,
children,
mode,
}: BottomSheetProps) => {
const { lockScroll, unlockScroll } = useScrollLock()

const handleClickScrim: MouseEventHandler<HTMLDivElement> = (e) => {
if (e.target !== e.currentTarget) return
if (onClickScrim) onClickScrim()
}

useEffect(() => {
if (isShowing) {
lockScroll()
} else {
unlockScroll()
}
}, [isShowing, lockScroll, unlockScroll])

const heightStyle = isShort ? 'h-[calc(100vh-252px)]' : 'h-[calc(100vh-55px)]'

return (
<AnimatePortal isShowing={isShowing} mode={mode}>
<m.div
className={`fixed inset-0 ${zIndex.backdrop} h-full w-screen overflow-hidden bg-[rgba(15,23,42,0.5)]`}
onClick={handleClickScrim}
variants={bottomSheetFadeInVariants}
initial="initial"
animate="animate"
exit="exit"
>
<m.div
className={`flex-column-align ${heightStyle} absolute left-0 top-full ${zIndex.backdrop} w-full rounded-t-3xl bg-white px-[20px]`}
variants={bottomSheetVariants}
>
<div className="pb-[17px] pt-[8px]">
<Icon name="handle-bar" />
</div>
{children}
</m.div>
</m.div>
</AnimatePortal>
)
}

const easing = [0.6, -0.05, 0.01, 0.99]

const bottomSheetFadeInVariants: Variants = {
initial: {
opacity: 0,
transition: { duration: 0.3, ease: easing },
willChange: 'opacity',
},
animate: {
opacity: 1,
transition: { duration: 0.3, ease: easing },
willChange: 'opacity',
},
exit: {
opacity: 0,
transition: { duration: 0.3, ease: easing },
willChange: 'opacity',
},
}

const bottomSheetVariants: Variants = {
initial: {
y: 0,
transition: { duration: 0.3, ease: easing },
willChange: 'transform',
},
animate: {
y: '-100%',
transition: { duration: 0.3, ease: easing },
willChange: 'transform',
},
exit: {
y: 0,
transition: { duration: 0.3, ease: easing },
willChange: 'transform',
},
}
11 changes: 11 additions & 0 deletions src/components/icons/Bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { IconProps } from '.'

export const HandleBar = (props: IconProps) => {
const { color = '#B4B5B8' } = props

return (
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="13" viewBox="0 0 84 13" fill="none">
<rect x="24" y="4" width="36" height="5" rx="2.5" fill={color} />
</svg>
)
}
2 changes: 2 additions & 0 deletions src/components/icons/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon } from './Arrow'
import { HandleBar } from './Bar'
import { CheckNoIcon, CheckYesIcon } from './Check'
import { CloseIcon } from './Close'
import {
Expand Down Expand Up @@ -40,6 +41,7 @@ export const iconMap = {
medicine: MedicineIcon,
clinic: ClinicIcon,
health: HealthIcon,
'handle-bar': HandleBar,
}

export type IconComponentProps = IconProps & {
Expand Down
17 changes: 17 additions & 0 deletions src/components/portal/AnimatePortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type ComponentProps } from 'react'
import { AnimatePresence } from 'framer-motion'

import { Portal } from './Portal'

type AnimatePortalProps = ComponentProps<typeof Portal> & {
isShowing: boolean
mode?: ComponentProps<typeof AnimatePresence>['mode']
}

export const AnimatePortal = ({ children, isShowing, mode = 'wait' }: AnimatePortalProps) => {
return (
<Portal>
<AnimatePresence mode={mode}>{isShowing && children}</AnimatePresence>
</Portal>
)
}
16 changes: 16 additions & 0 deletions src/components/portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type PropsWithChildren, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'

export const Portal = ({ children }: PropsWithChildren) => {
const [container, setContainer] = useState<Element | null>(null)

useEffect(() => {
if (document) {
setContainer(document.body)
}
}, [])

if (!container) return null

return createPortal(<>{children}</>, container)
}
Empty file removed src/constants/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './zIndex'
5 changes: 5 additions & 0 deletions src/constants/zIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const zIndex = {
fab: 'z-[999]',
backdrop: 'z-[1000]',
modal: 'z-[1001]',
}
4 changes: 1 addition & 3 deletions src/hooks/Hydration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface QueryProps {
queryFn: QueryFunction
}

const Hydration = async ({ queryKey, queryFn, children }: PropsWithChildren<QueryProps>) => {
export const Hydration = async ({ queryKey, queryFn, children }: PropsWithChildren<QueryProps>) => {
const getQueryClient = cache(() => new QueryClient())
const queryClient = getQueryClient()

Expand All @@ -22,5 +22,3 @@ const Hydration = async ({ queryKey, queryFn, children }: PropsWithChildren<Quer

return <HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
}

export default Hydration
8 changes: 8 additions & 0 deletions src/hooks/LazyMotionProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'

import { type PropsWithChildren } from 'react'
import { domMax, LazyMotion } from 'framer-motion'

export const LazyMotionProvider = ({ children }: PropsWithChildren) => {
return <LazyMotion features={domMax}>{children}</LazyMotion>
}
4 changes: 1 addition & 3 deletions src/hooks/QueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const queryClientOption: QueryClientConfig = {
},
}

const QueryProvider = ({ children }: PropsWithChildren) => {
export const QueryProvider = ({ children }: PropsWithChildren) => {
const [queryClient] = useState(() => new QueryClient(queryClientOption))

return (
Expand All @@ -23,5 +23,3 @@ const QueryProvider = ({ children }: PropsWithChildren) => {
</QueryClientProvider>
)
}

export default QueryProvider
5 changes: 5 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './Hydration'
export * from './LazyMotionProvider'
export * from './QueryProvider'
export * from './useScrollLock'
export * from './useToggle'
18 changes: 18 additions & 0 deletions src/hooks/useScrollLock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

export const useScrollLock = () => {
const lockScroll = React.useCallback(() => {
document.body.dataset.scrollLock = 'true'
document.body.style.overflow = 'hidden'
}, [])

const unlockScroll = React.useCallback(() => {
document.body.style.overflow = ''
document.body.style.paddingRight = ''
}, [])

return {
lockScroll,
unlockScroll,
}
}
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7228,6 +7228,26 @@ __metadata:
languageName: node
linkType: hard

"framer-motion@npm:^11.3.29":
version: 11.3.29
resolution: "framer-motion@npm:11.3.29"
dependencies:
tslib: "npm:^2.4.0"
peerDependencies:
"@emotion/is-prop-valid": "*"
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
"@emotion/is-prop-valid":
optional: true
react:
optional: true
react-dom:
optional: true
checksum: 10c0/f60d9bbeca70a803f80d390bd7db42c2b7cb497d24bc788719dc46325ce81c629a0c92260aea0a25747b39b6dee889203a1c44710e0741cfc4d83441ba32a763
languageName: node
linkType: hard

"fresh@npm:0.5.2":
version: 0.5.2
resolution: "fresh@npm:0.5.2"
Expand Down Expand Up @@ -9405,6 +9425,7 @@ __metadata:
eslint-plugin-storybook: "npm:^0.8.0"
eslint-plugin-tailwindcss: "npm:^3.17.4"
eslint-plugin-unused-imports: "npm:^4.0.0"
framer-motion: "npm:^11.3.29"
husky: "npm:^8.0.0"
lint-staged: "npm:^15.2.7"
next: "npm:14.2.4"
Expand Down

0 comments on commit 6ebfb46

Please sign in to comment.