diff --git a/src/assets/logos/logoIcon.svg b/src/assets/logos/logoIcon.svg new file mode 100644 index 0000000..fb21871 --- /dev/null +++ b/src/assets/logos/logoIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/sidebar/MobileDrawer.stories.tsx b/src/components/sidebar/MobileDrawer.stories.tsx new file mode 100644 index 0000000..13c710f --- /dev/null +++ b/src/components/sidebar/MobileDrawer.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; + +import { fn } from 'storybook/test'; +import Image from 'next/image'; + +import MobileDrawer from './MobileDrawer'; +import SidebarButton from './SidebarButton'; +import SidebarAddButton from './SidebarAddButton'; +import chessSmall from '@/assets/icons/chess/chessSmall.svg'; +import boardSmall from '@/assets/icons/board/boardSmall.svg'; + +const TEAMS = ['경영관리팀', '프로덕트팀', '마케팅팀', '콘텐츠팀']; + +const meta = { + title: 'Components/MobileDrawer', + component: MobileDrawer, + parameters: { + layout: 'fullscreen', + viewport: { defaultViewport: 'mobile1' }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Open: Story = { + args: { + isOpen: true, + onClose: fn(), + children: ( + <> + {TEAMS.map((team) => ( + } + label={team} + isActive={team === '경영관리팀'} + /> + ))} + +
+ } + label="자유게시판" + /> + + ), + }, +}; diff --git a/src/components/sidebar/MobileDrawer.tsx b/src/components/sidebar/MobileDrawer.tsx new file mode 100644 index 0000000..4cf0ec5 --- /dev/null +++ b/src/components/sidebar/MobileDrawer.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useEffect, useRef, type ReactNode } from 'react'; +import Image from 'next/image'; + +import styles from './styles/MobileDrawer.module.css'; +import xMarkBig from '@/assets/icons/xMark/xMarkBig.svg'; + +type MobileDrawerProps = { + /** 드로어 열림/닫힘 상태 */ + isOpen: boolean; + /** 드로어 내부에 표시할 콘텐츠 */ + children: ReactNode; + /** 닫기 시 호출되는 콜백 (오버레이 클릭, X 버튼, ESC 키) */ + onClose: () => void; +}; + +/** + * 모바일 사이드 드로어 (슬라이드 패널). + * 오버레이 클릭, X 버튼, ESC 키로 닫을 수 있으며 포커스 트랩이 적용됩니다. + * children에 사이드바 메뉴 등 원하는 콘텐츠를 주입할 수 있습니다. + */ +const FOCUSABLE_SELECTOR = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + +export default function MobileDrawer({ isOpen, children, onClose }: MobileDrawerProps) { + const drawerRef = useRef(null); + const focusableRef = useRef([]); + + useEffect(() => { + if (!isOpen || !drawerRef.current) return; + + focusableRef.current = Array.from( + drawerRef.current.querySelectorAll(FOCUSABLE_SELECTOR), + ); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + return; + } + + if (e.key !== 'Tab') return; + + const items = focusableRef.current; + if (items.length === 0) return; + + const first = items[0]; + const last = items[items.length - 1]; + + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + useEffect(() => { + if (isOpen) { + drawerRef.current?.querySelector('button')?.focus(); + } + }, [isOpen]); + + if (!isOpen) return null; + + return ( + <> +