1- import { CURRENT_CLASS , EVENT_REFRESH , ROUTE_SEPARATOR } from "#common/constants" ;
1+ import { CURRENT_CLASS , EVENT_REFRESH } from "#common/constants" ;
22
33import { AKElement } from "#elements/Base" ;
44import { getURLParams , updateURLParams } from "#elements/router/RouteMatch" ;
@@ -7,8 +7,7 @@ import { isFocusable } from "#elements/utils/focus";
77
88import { msg } from "@lit/localize" ;
99import { css , CSSResult , html , LitElement , TemplateResult } from "lit" ;
10- import { customElement , property } from "lit/decorators.js" ;
11- import { ifDefined } from "lit/directives/if-defined.js" ;
10+ import { customElement , property , state } from "lit/decorators.js" ;
1211import { createRef , ref } from "lit/directives/ref.js" ;
1312
1413import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css" ;
@@ -20,18 +19,6 @@ export class Tabs extends AKElement {
2019 ...LitElement . shadowRootOptions ,
2120 delegatesFocus : true ,
2221 } ;
23-
24- #focusTargetRef = createRef < HTMLSlotElement > ( ) ;
25-
26- @property ( )
27- pageIdentifier = "page" ;
28-
29- @property ( )
30- currentPage ?: string ;
31-
32- @property ( { type : Boolean } )
33- vertical = false ;
34-
3522 static styles : CSSResult [ ] = [
3623 PFGlobal ,
3724 PFTabs ,
@@ -55,37 +42,90 @@ export class Tabs extends AKElement {
5542 ` ,
5643 ] ;
5744
58- observer : MutationObserver ;
45+ @property ( { type : String } )
46+ public pageIdentifier = "page" ;
5947
60- constructor ( ) {
61- super ( ) ;
62- this . observer = new MutationObserver ( ( ) => {
63- this . requestUpdate ( ) ;
64- } ) ;
65- }
48+ @property ( { type : Boolean , useDefault : true } )
49+ public vertical = false ;
50+
51+ @state ( )
52+ protected activeTabName : string | null = null ;
53+
54+ @state ( )
55+ protected tabs : ReadonlyMap < string , Element > = new Map ( ) ;
56+
57+ #focusTargetRef = createRef < HTMLSlotElement > ( ) ;
58+ #observer: MutationObserver | null = null ;
59+
60+ #updateTabs = ( ) : void => {
61+ this . tabs = new Map (
62+ Array . from ( this . querySelectorAll ( ":scope > [slot^='page-']" ) , ( element ) => {
63+ return [ element . getAttribute ( "slot" ) || "" , element ] ;
64+ } ) ,
65+ ) ;
66+ } ;
6667
67- connectedCallback ( ) : void {
68+ public override connectedCallback ( ) : void {
6869 super . connectedCallback ( ) ;
69- this . observer . observe ( this , {
70+
71+ this . #observer = new MutationObserver ( this . #updateTabs) ;
72+
73+ this . addEventListener ( "focus" , this . #delegateFocusListener) ;
74+
75+ if ( ! this . activeTabName ) {
76+ const params = getURLParams ( ) ;
77+ const tabParam = params [ this . pageIdentifier ] ;
78+
79+ if (
80+ tabParam &&
81+ typeof tabParam === "string" &&
82+ this . querySelector ( `[slot='${ tabParam } ']` )
83+ ) {
84+ this . activeTabName = tabParam ;
85+ } else {
86+ this . #updateTabs( ) ;
87+ this . activeTabName = this . tabs . keys ( ) . next ( ) . value || null ;
88+ }
89+ }
90+ }
91+
92+ public override firstUpdated ( ) : void {
93+ this . #observer?. observe ( this , {
7094 attributes : true ,
7195 childList : true ,
7296 subtree : true ,
7397 } ) ;
74-
75- this . addEventListener ( "focus" , this . #delegateFocusListener) ;
7698 }
7799
78- disconnectedCallback ( ) : void {
79- this . observer . disconnect ( ) ;
100+ public override disconnectedCallback ( ) : void {
101+ this . # observer? .disconnect ( ) ;
80102 super . disconnectedCallback ( ) ;
81103 }
82104
83- onClick ( slot ?: string ) : void {
84- this . currentPage = slot ;
85- const params : { [ key : string ] : string | undefined } = { } ;
86- params [ this . pageIdentifier ] = slot ;
87- updateURLParams ( params ) ;
88- const page = this . querySelector ( `[slot='${ this . currentPage } ']` ) ;
105+ public activateTab ( nextTabName : string ) : void {
106+ if ( ! nextTabName ) {
107+ console . warn ( "Cannot activate falsey tab name:" , nextTabName ) ;
108+ return ;
109+ }
110+
111+ if ( ! this . tabs . has ( nextTabName ) ) {
112+ console . warn ( "Cannot activate unknown tab name:" , nextTabName , this . tabs ) ;
113+ return ;
114+ }
115+
116+ const firstTab = this . tabs . keys ( ) . next ( ) . value || null ;
117+
118+ // We avoid adding the tab parameter to the URL if it's the first tab
119+ // to both reduce URL length and ensure that tests do not have to deal with
120+ // unnecessary URL parameters.
121+
122+ updateURLParams ( {
123+ [ this . pageIdentifier ] : nextTabName === firstTab ? null : nextTabName ,
124+ } ) ;
125+
126+ this . activeTabName = nextTabName ;
127+
128+ const page = this . querySelector ( `[slot='${ this . activeTabName } ']` ) ;
89129 if ( ! page ) return ;
90130
91131 page . dispatchEvent ( new CustomEvent ( EVENT_REFRESH ) ) ;
@@ -103,59 +143,49 @@ export class Tabs extends AKElement {
103143
104144 // We don't want to refocus if the user is tabbing between elements inside the tabpanel.
105145 if ( focusableElement && event . relatedTarget !== focusableElement ) {
106- focusableElement . focus ( ) ;
146+ focusableElement . focus ( {
147+ preventScroll : true ,
148+ } ) ;
107149 }
108150 } ;
109151
110- renderTab ( page : Element ) : TemplateResult {
111- const slot = page . attributes . getNamedItem ( "slot" ) ?. value ;
112- return html ` < li class ="pf-c-tabs__item ${ slot === this . currentPage ? CURRENT_CLASS : "" } ">
152+ renderTab ( slotName : string , tabPanel : Element ) : TemplateResult {
153+ return html ` < li
154+ class ="pf-c-tabs__item ${ slotName === this . activeTabName ? CURRENT_CLASS : "" } "
155+ >
113156 < button
114157 type ="button "
115158 role ="tab "
116- id =${ `${ slot } -tab` }
117- aria-selected =${ slot === this . currentPage ? "true" : "false" }
118- aria-controls=${ ifPresent ( slot ) }
159+ id =${ `${ slotName } -tab` }
160+ aria-selected =${ slotName === this . activeTabName ? "true" : "false" }
161+ aria-controls=${ ifPresent ( slotName ) }
119162 class="pf-c-tabs__link"
120- @click=${ ( ) => this . onClick ( slot ) }
163+ @click=${ ( ) => this . activateTab ( slotName ) }
121164 >
122- < span class ="pf-c-tabs__item-text "> ${ page . getAttribute ( "aria-label" ) } </ span >
165+ < span class ="pf-c-tabs__item-text "> ${ tabPanel . getAttribute ( "aria-label" ) } </ span >
123166 </ button >
124167 </ li > ` ;
125168 }
126169
127170 render ( ) : TemplateResult {
128- const pages = Array . from ( this . querySelectorAll ( ":scope > [slot^='page-']" ) ) ;
129- if ( window . location . hash . includes ( ROUTE_SEPARATOR ) ) {
130- const params = getURLParams ( ) ;
131- if (
132- this . pageIdentifier in params &&
133- ! this . currentPage &&
134- this . querySelector ( `[slot='${ params [ this . pageIdentifier ] } ']` ) !== null
135- ) {
136- // To update the URL to match with the current slot
137- this . onClick ( params [ this . pageIdentifier ] as string ) ;
138- }
139- }
140- if ( ! this . currentPage ) {
141- if ( pages . length < 1 ) {
142- return html `< h1 > ${ msg ( "no tabs defined" ) } </ h1 > ` ;
143- }
144- const wantedPage = pages [ 0 ] . attributes . getNamedItem ( "slot" ) ?. value ;
145- this . onClick ( wantedPage ) ;
171+ if ( ! this . tabs . size ) {
172+ return html `< h1 > ${ msg ( "no tabs defined" ) } </ h1 > ` ;
146173 }
174+
147175 return html `< div class ="pf-c-tabs ${ this . vertical ? "pf-m-vertical pf-m-box" : "" } ">
148176 < ul
149177 class ="pf-c-tabs__list "
150178 role ="tablist "
151179 aria-orientation =${ this . vertical ? "vertical" : "horizontal" }
152180 aria-label =${ ifPresent ( this . ariaLabel ) }
153181 >
154- ${ pages . map ( ( page ) => this . renderTab ( page ) ) }
182+ ${ Array . from ( this . tabs , ( [ slotName , tabPanel ] ) =>
183+ this . renderTab ( slotName , tabPanel ) ,
184+ ) }
155185 </ ul >
156186 </ div >
157187 < slot name ="header "> </ slot >
158- < slot ${ ref ( this . #focusTargetRef) } name =" ${ ifDefined ( this . currentPage ) } " > </ slot > ` ;
188+ < slot ${ ref ( this . #focusTargetRef) } name =${ ifPresent ( this . activeTabName ) } > </ slot > ` ;
159189 }
160190}
161191
0 commit comments