11'use client'
22
3- import { useState , createContext , useContext } from 'react'
3+ import { useState , createContext , useContext , Children , isValidElement , useId } from 'react'
44import { cn } from '@/lib/utils'
55
66interface TabsContextValue {
77 activeTab : string
88 setActiveTab : ( tab : string ) => void
9+ tabsId : string
910}
1011
1112const TabsContext = createContext < TabsContextValue | null > ( null )
@@ -17,10 +18,87 @@ interface TabsProps {
1718
1819export function Tabs ( { children, defaultValue } : TabsProps ) {
1920 const [ activeTab , setActiveTab ] = useState ( defaultValue || '' )
21+ const tabsId = useId ( )
22+
23+ // Extract tab titles and content from children
24+ const tabs : { title : string ; content : React . ReactNode } [ ] = [ ]
25+ Children . forEach ( children , ( child ) => {
26+ if ( isValidElement < TabProps > ( child ) && child . props . title ) {
27+ tabs . push ( {
28+ title : child . props . title ,
29+ content : child . props . children ,
30+ } )
31+ }
32+ } )
33+
34+ // Set default active tab if not set
35+ const currentActiveTab = activeTab || ( tabs [ 0 ] ?. title ?? '' )
2036
2137 return (
22- < TabsContext . Provider value = { { activeTab, setActiveTab } } >
23- < div className = "my-6" > { children } </ div >
38+ < TabsContext . Provider value = { { activeTab : currentActiveTab , setActiveTab, tabsId } } >
39+ < div className = "my-6" >
40+ { /* Tab list */ }
41+ < div role = "tablist" aria-label = "Tabs" className = "flex border-b border-border" >
42+ { tabs . map ( ( tab , index ) => {
43+ const isActive = currentActiveTab === tab . title
44+ const tabId = `${ tabsId } -tab-${ index } `
45+ const panelId = `${ tabsId } -panel-${ index } `
46+
47+ return (
48+ < button
49+ key = { tab . title }
50+ role = "tab"
51+ id = { tabId }
52+ aria-selected = { isActive }
53+ aria-controls = { panelId }
54+ tabIndex = { isActive ? 0 : - 1 }
55+ onClick = { ( ) => setActiveTab ( tab . title ) }
56+ onKeyDown = { ( e ) => {
57+ if ( e . key === 'ArrowRight' ) {
58+ e . preventDefault ( )
59+ const nextIndex = ( index + 1 ) % tabs . length
60+ setActiveTab ( tabs [ nextIndex ] . title )
61+ } else if ( e . key === 'ArrowLeft' ) {
62+ e . preventDefault ( )
63+ const prevIndex = ( index - 1 + tabs . length ) % tabs . length
64+ setActiveTab ( tabs [ prevIndex ] . title )
65+ }
66+ } }
67+ className = { cn (
68+ 'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors' ,
69+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2' ,
70+ isActive
71+ ? 'border-[var(--accent)] text-[var(--accent)]'
72+ : 'border-transparent text-muted-foreground hover:text-foreground'
73+ ) }
74+ >
75+ { tab . title }
76+ </ button >
77+ )
78+ } ) }
79+ </ div >
80+
81+ { /* Tab panels */ }
82+ { tabs . map ( ( tab , index ) => {
83+ const isActive = currentActiveTab === tab . title
84+ const tabId = `${ tabsId } -tab-${ index } `
85+ const panelId = `${ tabsId } -panel-${ index } `
86+
87+ return (
88+ < div
89+ key = { tab . title }
90+ role = "tabpanel"
91+ id = { panelId }
92+ aria-labelledby = { tabId }
93+ hidden = { ! isActive }
94+ tabIndex = { 0 }
95+ className = { cn ( 'pt-4 [&>pre]:mt-0' , ! isActive && 'hidden' ) }
96+ >
97+ { tab . content }
98+ </ div >
99+ )
100+ } ) }
101+ </ div >
24102 </ TabsContext . Provider >
25103 )
26104}
@@ -30,31 +108,9 @@ interface TabProps {
30108 children : React . ReactNode
31109}
32110
111+ // Tab is now just a data container, rendering is handled by Tabs
33112export function Tab ( { title, children } : TabProps ) {
34- const context = useContext ( TabsContext )
35- if ( ! context ) return null
36-
37- const { activeTab, setActiveTab } = context
38- const isActive = activeTab === title || ( ! activeTab && title )
39-
40- return (
41- < >
42- < button
43- onClick = { ( ) => setActiveTab ( title ) }
44- className = { cn (
45- 'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors' ,
46- isActive
47- ? 'border-[var(--accent)] text-[var(--accent)]'
48- : 'border-transparent text-muted-foreground hover:text-foreground'
49- ) }
50- >
51- { title }
52- </ button >
53- { isActive && (
54- < div className = "pt-4 [&>pre]:mt-0" >
55- { children }
56- </ div >
57- ) }
58- </ >
59- )
113+ // This component is used for data extraction only
114+ // Actual rendering happens in Tabs component
115+ return null
60116}
0 commit comments