|
1 | | -import { ReactNode, useState } from 'react'; |
| 1 | +import React, { ReactNode, ReactElement, useId, useState } from 'react'; |
2 | 2 |
|
3 | | -export function Tabs({ children }: { children: ReactNode }) { |
4 | | - const tabList = Array.isArray(children) ? children : [children]; |
5 | | - const [activeTab, setActiveTab] = useState(0); |
| 3 | +type TabProps = { label: string; children: ReactNode }; |
6 | 4 |
|
7 | | - return ( |
8 | | - <div className="mb-8 w-full py-4"> |
9 | | - {/* 탭 버튼 영역 */} |
10 | | - <div className="flex"> |
11 | | - {tabList.map((child: any, idx) => { |
12 | | - const isActive = idx === activeTab; |
13 | | - |
14 | | - const borderClass = isActive ? '' : ''; |
| 5 | +export function Tabs({children, defaultIndex = 0, }: { |
| 6 | + children: ReactNode; |
| 7 | + defaultIndex?: number; |
| 8 | +}) { |
| 9 | + const tabList = React.Children.toArray(children) as ReactElement<TabProps>[]; |
| 10 | + const [active, setActive] = useState(Math.min(defaultIndex, tabList.length - 1)); |
| 11 | + const uid = useId(); |
15 | 12 |
|
| 13 | + return ( |
| 14 | + <div className="w-full"> |
| 15 | + {/* Tab bar */} |
| 16 | + <div role="tablist" aria-label="Tabs" className="relative flex items-end w-full"> |
| 17 | + {tabList.map((tab, i) => { |
| 18 | + const isActive = i === active; |
16 | 19 | return ( |
17 | | - <div |
18 | | - key={idx} |
19 | | - onClick={() => setActiveTab(idx)} |
20 | | - tabIndex={0} |
21 | | - className={`px-4 py-2 text-mi cursor-pointer transition-all rounded-t border |
22 | | - ${borderClass} |
23 | | - ${ |
24 | | - isActive |
25 | | - ? 'bg-blue-10 border-b-2 border-b-blue-500 text-blue-600 font-semibold' |
26 | | - : 'border-b border-gray-200 text-gray-500 hover:bg-gray-100 hover:text-black' |
27 | | - } |
28 | | - `} |
| 20 | + <button |
| 21 | + key={i} |
| 22 | + role="tab" |
| 23 | + id={`${uid}-tab-${i}`} |
| 24 | + aria-selected={isActive} |
| 25 | + aria-controls={`${uid}-panel-${i}`} |
| 26 | + onClick={() => setActive(i)} |
| 27 | + className={[ |
| 28 | + 'relative -mb-px px-4 py-2 text-sm md:text-base select-none outline-none transition border', |
| 29 | + 'first:rounded-tl-lg last:rounded-tr-lg', |
| 30 | + i > 0 ? '-ml-px' : '', |
| 31 | + isActive |
| 32 | + ? [ |
| 33 | + // 기존 스타일 유지 |
| 34 | + 'bg-white text-blue-600 border-gray-300 border-b-0 font-semibold z-20 relative scale-105', |
| 35 | + // 활성 탭이 "첫 번째"일 때 왼쪽 경계선을 재도색 |
| 36 | + "first:before:content-[''] first:before:absolute first:before:top-0 first:before:-left-px first:before:h-full first:before:w-px first:before:bg-gray-300 first:dark:before:bg-gray-700", |
| 37 | + // 스케일 기준점을 왼쪽-아래로 둬서 왼쪽 선이 밀리지 않게 함 |
| 38 | + 'origin-bottom-left', |
| 39 | + ].join(' ') |
| 40 | + : 'bg-white text-gray-500 hover:text-gray-700 hover:bg-gray-50 border-gray-300 font-normal' |
| 41 | + |
| 42 | + ].join(' ')} |
29 | 43 | > |
30 | | - {child.props.label} |
31 | | - </div> |
| 44 | + {tab.props.label} |
| 45 | + </button> |
32 | 46 | ); |
33 | 47 | })} |
| 48 | + {/* 오른쪽 빈 공간 라인 맞추기 */} |
| 49 | + <div className="flex-1 h-px bg-gray-300 dark:bg-gray-700" /> |
34 | 50 | </div> |
35 | 51 |
|
36 | | - {/* 콘텐츠 영역 */} |
| 52 | + {/* Panel */} |
37 | 53 | <div |
38 | | - className="space-x-2 px-6 border-t shadow-sm border border-gray-200 bg-white rounded-t"> |
39 | | - {tabList[activeTab]} |
| 54 | + role="tabpanel" |
| 55 | + id={`${uid}-panel-${active}`} |
| 56 | + aria-labelledby={`${uid}-tab-${active}`} |
| 57 | + className="rounded-b-lg border border-gray-300 border-t-0 bg-white p-5 md:p-7 shadow-sm |
| 58 | + dark:border-gray-700 dark:bg-gray-900" |
| 59 | + > |
| 60 | + {tabList[active]} |
40 | 61 | </div> |
41 | 62 | </div> |
42 | 63 | ); |
43 | 64 | } |
44 | 65 |
|
45 | | -export function Tab({children}: { children: ReactNode }) { |
| 66 | +export function Tab({ children }: TabProps) { |
46 | 67 | return <div>{children}</div>; |
47 | 68 | } |
48 | | - |
|
0 commit comments