11import React , { createContext , useContext , useState , useEffect } from "react" ;
22import type { KeyboardEvent , ReactNode } from "react" ;
3+ import { useSearchParams } from "react-router-dom" ;
34import "./Tabs.css" ;
45
56interface TabsContextType {
@@ -27,61 +28,23 @@ interface TabsProps {
2728 className ?: string ;
2829}
2930
30- /** Inner component that uses useSearchParams — only rendered when syncWithUrl=true */
3131function TabsWithUrl ( {
3232 defaultValue,
3333 value : controlledValue ,
3434 onValueChange,
3535 urlParam = "tab" ,
3636 children,
3737 className = "" ,
38- } : TabsProps ) {
39- const [ internalValue , setInternalValue ] = useState ( defaultValue || "" ) ;
40-
41- const [ urlValue , setUrlValue ] = useState < string | null > ( ( ) => {
42- if ( ! syncWithUrl || typeof window === "undefined" ) {
43- return null ;
44- }
45- return new URLSearchParams ( window . location . search ) . get ( urlParam ) ;
46- } ) ;
4738} : Omit < TabsProps , "syncWithUrl" > ) {
4839 const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
4940 const [ internalValue , setInternalValue ] = useState ( defaultValue || "" ) ;
5041
5142 const urlValue = searchParams . get ( urlParam ) ;
52- const activeValue =
53- controlledValue !== undefined
54- ? controlledValue
55- : urlValue
56- ? urlValue
57- : internalValue ;
58-
59- useEffect ( ( ) => {
60- if ( ! syncWithUrl || typeof window === "undefined" ) {
61- return ;
62- }
63-
64- const handlePopState = ( ) => {
65- setUrlValue ( new URLSearchParams ( window . location . search ) . get ( urlParam ) ) ;
66- } ;
67-
68- window . addEventListener ( "popstate" , handlePopState ) ;
69- return ( ) => {
70- window . removeEventListener ( "popstate" , handlePopState ) ;
71- } ;
72- } , [ syncWithUrl , urlParam ] ) ;
43+ const activeValue = controlledValue !== undefined
44+ ? controlledValue
45+ : ( urlValue || internalValue ) ;
7346
7447 useEffect ( ( ) => {
75- if ( ! syncWithUrl || typeof window === "undefined" ) {
76- return ;
77- }
78- if ( ! urlValue && defaultValue ) {
79- const params = new URLSearchParams ( window . location . search ) ;
80- params . set ( urlParam , defaultValue ) ;
81- window . history . replaceState ( { } , "" , `${ window . location . pathname } ?${ params . toString ( ) } ` ) ;
82- setUrlValue ( defaultValue ) ;
83- }
84- } , [ syncWithUrl , urlValue , defaultValue , urlParam ] ) ;
8548 if ( ! urlValue && defaultValue ) {
8649 setSearchParams (
8750 ( prev ) => {
@@ -98,12 +61,7 @@ function TabsWithUrl({
9861 if ( controlledValue === undefined ) {
9962 setInternalValue ( newValue ) ;
10063 }
101-
102- if ( syncWithUrl ) {
103- const params = new URLSearchParams ( window . location . search ) ;
104- params . set ( urlParam , newValue ) ;
105- window . history . replaceState ( { } , "" , `${ window . location . pathname } ?${ params . toString ( ) } ` ) ;
106- setUrlValue ( newValue ) ;
64+
10765 setSearchParams (
10866 ( prev ) => {
10967 const newParams = new URLSearchParams ( prev ) ;
@@ -127,7 +85,6 @@ function TabsWithUrl({
12785 ) ;
12886}
12987
130- /** Inner component for tabs without URL sync */
13188function TabsWithoutUrl ( {
13289 defaultValue,
13390 value : controlledValue ,
@@ -137,8 +94,7 @@ function TabsWithoutUrl({
13794} : Omit < TabsProps , "syncWithUrl" | "urlParam" > ) {
13895 const [ internalValue , setInternalValue ] = useState ( defaultValue || "" ) ;
13996
140- const activeValue =
141- controlledValue !== undefined ? controlledValue : internalValue ;
97+ const activeValue = controlledValue !== undefined ? controlledValue : internalValue ;
14298
14399 const handleValueChange = ( newValue : string ) => {
144100 if ( controlledValue === undefined ) {
@@ -158,10 +114,7 @@ function TabsWithoutUrl({
158114 ) ;
159115}
160116
161- export function Tabs ( {
162- syncWithUrl = false ,
163- ...props
164- } : TabsProps ) {
117+ export function Tabs ( { syncWithUrl = false , ...props } : TabsProps ) {
165118 if ( syncWithUrl ) {
166119 return < TabsWithUrl { ...props } /> ;
167120 }
@@ -170,12 +123,7 @@ export function Tabs({
170123
171124export function TabsList ( { children, className = "" , style } : { children : ReactNode ; className ?: string ; style ?: React . CSSProperties } ) {
172125 return (
173- < div
174- role = "tablist"
175- aria-orientation = "horizontal"
176- className = { `tabs-list ${ className } ` }
177- style = { style }
178- >
126+ < div role = "tablist" aria-orientation = "horizontal" className = { `tabs-list ${ className } ` } style = { style } >
179127 { children }
180128 </ div >
181129 ) ;
@@ -208,13 +156,16 @@ export function TabsTrigger({ value, children, className = "" }: { value: string
208156
209157 e . preventDefault ( ) ;
210158 tabs [ nextIndex ] . focus ( ) ;
211- onValueChange ( tabs [ nextIndex ] . dataset . value ! ) ;
159+ onValueChange ( tabs [ nextIndex ] . getAttribute ( 'data- value' ) ! ) ;
212160 } ;
213161
214162 return (
215163 < button
216164 type = "button"
217- aria-pressed = { isActive }
165+ role = "tab"
166+ aria-selected = { isActive }
167+ aria-controls = { `panel-${ value } ` }
168+ id = { `tab-${ value } ` }
218169 data-state = { isActive ? "active" : "inactive" }
219170 data-value = { value }
220171 className = { `tabs-trigger ${ isActive ? "active" : "" } ${ className } ` }
@@ -234,6 +185,9 @@ export function TabsContent({ value, children, className = "" }: { value: string
234185
235186 return (
236187 < div
188+ role = "tabpanel"
189+ id = { `panel-${ value } ` }
190+ aria-labelledby = { `tab-${ value } ` }
237191 data-state = { isActive ? "active" : "inactive" }
238192 className = { `tabs-content ${ className } ` }
239193 tabIndex = { 0 }
0 commit comments