1- import { Component , Element , Event , EventEmitter , h , Host , Method , Prop , State } from '@stencil/core' ;
1+ import { Component , Element , Event , EventEmitter , h , Host , Method , Prop } from '@stencil/core' ;
22import { version } from '@root/package.json' ;
33import { fadeIn , fadeOut } from '@/animations' ;
44import { componentOnReady } from '@/utils' ;
@@ -16,34 +16,26 @@ import { componentOnReady } from '@/utils';
1616 shadow : true ,
1717} )
1818export class PostTabs {
19- private currentActiveTab : HTMLPostTabHeaderElement ;
19+ private activeTab : HTMLPostTabHeaderElement ;
2020 private showing : Animation ;
2121 private hiding : Animation ;
2222 private isLoaded = false ;
2323
24- @State ( ) isNavigationMode : boolean = false ;
25-
2624 private get tabs ( ) : HTMLPostTabHeaderElement [ ] {
2725 return Array . from (
2826 this . host . querySelectorAll < HTMLPostTabHeaderElement > ( 'post-tab-header' ) ,
2927 ) . filter ( tab => tab . closest ( 'post-tabs' ) === this . host ) ;
3028 }
3129
32- private get panels ( ) : HTMLPostTabPanelElement [ ] {
33- return Array . from (
34- this . host . querySelectorAll < HTMLPostTabPanelElement > ( 'post-tab-panel' ) ,
35- ) . filter ( panel => panel . closest ( 'post-tabs' ) === this . host ) ;
36- }
37-
3830 @Element ( ) host : HTMLPostTabsElement ;
3931
4032 /**
41- * The name of the tab that is initially active .
42- * If not specified, it defaults to the first tab.
33+ * The name of the panel that is initially shown .
34+ * If not specified, it defaults to the panel associated with the first tab.
4335 *
4436 * **Changing this value after initialization has no effect.**
4537 */
46- @Prop ( ) readonly activeTab ?: string ;
38+ @Prop ( ) readonly activePanel ?: HTMLPostTabPanelElement [ 'name' ] ;
4739
4840 /**
4941 * When set to true, this property allows the tabs container to span the
@@ -53,78 +45,45 @@ export class PostTabs {
5345
5446 /**
5547 * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished.
56- * The payload is the name of the newly active tab .
48+ * The payload is the name of the newly shown panel .
5749 */
5850 @Event ( ) postChange : EventEmitter < string > ;
5951
6052 componentDidLoad ( ) {
61- this . detectMode ( ) ;
6253 this . moveMisplacedTabs ( ) ;
6354 this . enableTabs ( ) ;
6455
65- const initiallyActiveTab = this . activeTab || this . tabs [ 0 ] ?. getAttribute ( 'name' ) ;
66- void this . show ( initiallyActiveTab ) ;
56+ const initiallyActivePanel = this . activePanel || this . tabs [ 0 ] ?. panel ;
57+ void this . show ( initiallyActivePanel ) ;
6758
6859 this . isLoaded = true ;
6960 }
7061
71- private detectMode ( ) {
72- // Check if any tab headers contain anchor elements (via data-attribute exposure)
73- const hasNavigationTabs = this . tabs . some ( tab =>
74- tab . getAttribute ( 'data-navigation-mode' ) === 'true'
75- ) ;
76-
77- // Check if there are any panels
78- const hasPanels = this . panels . length > 0 ;
79-
80- // Validate for mixed mode (error condition)
81- if ( hasNavigationTabs && hasPanels ) {
82- console . error ( 'PostTabs: Mixed mode detected. Cannot use both navigation mode (anchor elements) and panel mode (post-tab-panel elements) at the same time.' ) ;
83- return ;
84- }
85-
86- this . isNavigationMode = hasNavigationTabs ;
87- }
88-
8962 /**
9063 * Shows the panel with the given name and selects its associated tab.
91- * In navigation mode, only updates the active tab state.
9264 * Any other panel that was previously shown becomes hidden and its associated tab is unselected.
9365 */
9466 @Method ( )
95- async show ( tabName : string ) {
67+ async show ( panelName : string ) {
9668 // do nothing if the tab is already active
97- if ( tabName === this . currentActiveTab ?. getAttribute ( 'name' ) ) {
69+ if ( panelName === this . activeTab ?. panel ) {
9870 return ;
9971 }
10072
101- const previousTab = this . currentActiveTab ;
73+ const previousTab = this . activeTab ;
10274 const newTab : HTMLPostTabHeaderElement = this . host . querySelector (
103- `post-tab-header[name =${ tabName } ]` ,
75+ `post-tab-header[panel =${ panelName } ]` ,
10476 ) ;
105-
106- if ( ! newTab ) {
107- console . warn ( `PostTabs: No tab found with name "${ tabName } "` ) ;
108- return ;
109- }
110-
111- await this . activateTab ( newTab ) ;
77+ this . activateTab ( newTab ) ;
11278
113- // In navigation mode, we don't need to handle panels
114- if ( this . isNavigationMode ) {
115- if ( this . isLoaded ) this . postChange . emit ( this . currentActiveTab . getAttribute ( 'name' ) ) ;
116- return ;
117- }
118-
119- // Panel mode logic
12079 // if a panel is currently being displayed, remove it from the view and complete the associated animation
12180 if ( this . showing ) {
12281 this . showing . effect [ 'target' ] . style . display = 'none' ;
12382 this . showing . finish ( ) ;
12483 }
12584
12685 // hide the currently visible panel only if no other animation is running
127- if ( previousTab && ! this . showing && ! this . hiding ) this . hidePanel ( previousTab . getAttribute ( 'name' ) ) ;
86+ if ( previousTab && ! this . showing && ! this . hiding ) this . hidePanel ( previousTab . panel ) ;
12887
12988 // wait for any hiding animation to complete before showing the selected tab
13089 if ( this . hiding ) await this . hiding . finished ;
@@ -134,7 +93,7 @@ export class PostTabs {
13493 // wait for any display animation to complete for the returned promise to fully resolve
13594 if ( this . showing ) await this . showing . finished ;
13695
137- if ( this . isLoaded ) this . postChange . emit ( this . currentActiveTab . getAttribute ( 'name' ) ) ;
96+ if ( this . isLoaded ) this . postChange . emit ( this . activeTab . panel ) ;
13897 }
13998
14099 private moveMisplacedTabs ( ) {
@@ -152,29 +111,21 @@ export class PostTabs {
152111 this . tabs . forEach ( async tab => {
153112 await componentOnReady ( tab ) ;
154113
155- // Skip tab setup in navigation mode - anchors handle their own navigation
156- if ( this . isNavigationMode ) {
157- return ;
158- }
159-
160- // Panel mode: set up tab-panel relationships
161114 // if the tab has an "aria-controls" attribute it was already linked to its panel: do nothing
162115 if ( tab . getAttribute ( 'aria-controls' ) ) return ;
163116
164- const tabPanel = this . getPanel ( tab . getAttribute ( 'name' ) ) ;
165- if ( tabPanel ) {
166- tab . setAttribute ( 'aria-controls' , tabPanel . id ) ;
167- tabPanel . setAttribute ( 'aria-labelledby' , tab . id ) ;
168- }
117+ const tabPanel = this . getPanel ( tab . panel ) ;
118+ tab . setAttribute ( 'aria-controls' , tabPanel . id ) ;
119+ tabPanel . setAttribute ( 'aria-labelledby' , tab . id ) ;
169120
170121 tab . addEventListener ( 'click' , ( ) => {
171- void this . show ( tab . getAttribute ( 'name' ) ) ;
122+ void this . show ( tab . panel ) ;
172123 } ) ;
173124
174125 tab . addEventListener ( 'keydown' , ( e : KeyboardEvent ) => {
175126 if ( e . key === 'Enter' || e . key === ' ' ) {
176127 e . preventDefault ( ) ;
177- void this . show ( tab . getAttribute ( 'name' ) ) ;
128+ void this . show ( tab . panel ) ;
178129 }
179130 } ) ;
180131
@@ -184,37 +135,23 @@ export class PostTabs {
184135 } ) ;
185136
186137 // if the currently active tab was removed from the DOM then select the first one
187- if ( this . currentActiveTab && ! this . currentActiveTab . isConnected ) {
188- void this . show ( this . tabs [ 0 ] ?. getAttribute ( 'name' ) ) ;
138+ if ( this . activeTab && ! this . activeTab . isConnected ) {
139+ void this . show ( this . tabs [ 0 ] ?. panel ) ;
189140 }
190141 }
191142
192- private async activateTab ( tab : HTMLPostTabHeaderElement ) {
193- // Deactivate previous tab
194- if ( this . currentActiveTab ) {
195- this . currentActiveTab . setAttribute ( 'aria-selected' , 'false' ) ;
196- this . currentActiveTab . setAttribute ( 'tabindex' , '-1' ) ;
197- this . currentActiveTab . classList . remove ( 'active' ) ;
198-
199- // Remove aria-current from previous tab's anchor (navigation mode)
200- const previousAnchor = this . currentActiveTab . querySelector ( 'a' ) ;
201- if ( previousAnchor ) {
202- previousAnchor . removeAttribute ( 'aria-current' ) ;
203- }
143+ private activateTab ( tab : HTMLPostTabHeaderElement ) {
144+ if ( this . activeTab ) {
145+ this . activeTab . setAttribute ( 'aria-selected' , 'false' ) ;
146+ this . activeTab . setAttribute ( 'tabindex' , '-1' ) ;
147+ this . activeTab . classList . remove ( 'active' ) ;
204148 }
205149
206- // Activate new tab
207150 tab . setAttribute ( 'aria-selected' , 'true' ) ;
208151 tab . setAttribute ( 'tabindex' , '0' ) ;
209152 tab . classList . add ( 'active' ) ;
210-
211- // Set aria-current on new tab's anchor (navigation mode)
212- const newAnchor = tab . querySelector ( 'a' ) ;
213- if ( newAnchor ) {
214- newAnchor . setAttribute ( 'aria-current' , 'page' ) ;
215- }
216153
217- this . currentActiveTab = tab ;
154+ this . activeTab = tab ;
218155 }
219156
220157 private hidePanel ( panelName : HTMLPostTabPanelElement [ 'name' ] ) {
@@ -230,7 +167,7 @@ export class PostTabs {
230167 }
231168
232169 private showSelectedPanel ( ) {
233- const panel = this . getPanel ( this . currentActiveTab . getAttribute ( 'name' ) ) ;
170+ const panel = this . getPanel ( this . activeTab . panel ) ;
234171 panel . style . display = 'block' ;
235172
236173 // prevent the initially selected panel from fading in
@@ -243,7 +180,7 @@ export class PostTabs {
243180 }
244181
245182 private getPanel ( name : string ) : HTMLPostTabPanelElement {
246- return this . host . querySelector ( `post-tab-panel[for =${ name } ]` ) ;
183+ return this . host . querySelector ( `post-tab-panel[name =${ name } ]` ) ;
247184 }
248185
249186 private navigateTabs ( tab : HTMLPostTabHeaderElement , key : 'ArrowRight' | 'ArrowLeft' ) {
@@ -262,28 +199,17 @@ export class PostTabs {
262199 }
263200
264201 render ( ) {
265- const tabsRole = this . isNavigationMode ? undefined : 'tablist' ;
266- const ariaLabel = this . isNavigationMode ? 'Tabs navigation' : undefined ;
267-
268202 return (
269203 < Host data-version = { version } >
270204 < div class = "tabs-wrapper" part = "tabs" >
271- { this . isNavigationMode ? (
272- < nav class = "tabs" role = { tabsRole } aria-label = { ariaLabel } >
273- < slot name = "tabs" onSlotchange = { ( ) => this . enableTabs ( ) } />
274- </ nav >
275- ) : (
276- < div class = "tabs" role = { tabsRole } aria-label = { ariaLabel } >
277- < slot name = "tabs" onSlotchange = { ( ) => this . enableTabs ( ) } />
278- </ div >
279- ) }
280- </ div >
281- { ! this . isNavigationMode && (
282- < div class = "tab-content" part = "content" >
283- < slot onSlotchange = { ( ) => this . moveMisplacedTabs ( ) } />
205+ < div class = "tabs" role = "tablist" >
206+ < slot name = "tabs" onSlotchange = { ( ) => this . enableTabs ( ) } />
284207 </ div >
285- ) }
208+ </ div >
209+ < div class = "tab-content" part = "content" >
210+ < slot onSlotchange = { ( ) => this . moveMisplacedTabs ( ) } />
211+ </ div >
286212 </ Host >
287213 ) ;
288214 }
289- }
215+ }
0 commit comments