15
15
import { CorePopovers } from '@services/overlays/popovers' ;
16
16
import { CoreFormatTextOptions } from '@components/bs-tooltip/bs-tooltip' ;
17
17
import { CoreModals } from '@services/overlays/modals' ;
18
+ import { CoreDom } from './dom' ;
18
19
19
20
/**
20
21
* Singleton with helper functions for Bootstrap.
@@ -40,6 +41,7 @@ export class CoreBootstrap {
40
41
this . handleTooltipsAndPopovers ( rootElement , formatTextOptions ) ;
41
42
this . handleAccordionsAndCollapse ( rootElement ) ;
42
43
this . handleModals ( rootElement , formatTextOptions ) ;
44
+ this . handleTabs ( rootElement ) ;
43
45
this . enableDismissTrigger ( rootElement , 'alert' , 'close' ) ; // Alert uses close method.
44
46
this . enableDismissTrigger ( rootElement , 'modal' ) ; // Modal uses hide method
45
47
this . enableDismissTrigger ( rootElement , 'offcanvas' ) ; // Offcanvas uses hide method
@@ -194,6 +196,133 @@ export class CoreBootstrap {
194
196
} ) ;
195
197
}
196
198
199
+ /**
200
+ * Handle Bootstrap tab elements in a certain element.
201
+ * Supports both Bootstrap 4 and 5.
202
+ * https://getbootstrap.com/docs/5.3/components/navs-tabs/#tabs
203
+ *
204
+ * @param rootElement Element where to search for elements to treat.
205
+ */
206
+ protected static handleTabs ( rootElement : HTMLElement ) : void {
207
+ const elements = Array . from ( rootElement . querySelectorAll < HTMLElement > (
208
+ '.list-group, .nav, [role="tablist"]' ,
209
+ ) ) ;
210
+
211
+ if ( ! elements . length ) {
212
+ return ;
213
+ }
214
+ const toggleSelectorsList = [
215
+ '[data-bs-toggle="tab"]' ,
216
+ '[data-toggle="tab"]' ,
217
+ '[data-bs-toggle="pill"]' ,
218
+ '[data-toggle="pill"]' ,
219
+ '[data-bs-toggle="list"]' ,
220
+ '[data-toggle="list"]' ,
221
+ ] ;
222
+
223
+ elements . forEach ( ( element ) => {
224
+ if ( ! element . hasAttribute ( 'role' ) ) {
225
+ element . setAttribute ( 'role' , 'tablist' ) ;
226
+ }
227
+ const childrenSelectors = [
228
+ '.nav-link:not(.dropdown-toggle)' ,
229
+ '.list-group-item:not(.dropdown-toggle)' ,
230
+ '[role="tab"]:not(.dropdown-toggle)' ,
231
+ ...toggleSelectorsList ,
232
+ ] . join ( ',' ) ;
233
+
234
+ const children = Array . from ( element . querySelectorAll < HTMLElement > ( childrenSelectors ) ) ;
235
+
236
+ children . forEach ( ( child ) => {
237
+ const isActive = child . classList . contains ( 'active' ) ;
238
+ const outerElement = child . closest ( '.nav-item, .list-group-item' ) ;
239
+ child . setAttribute ( 'aria-selected' , isActive ? 'true' : 'false' ) ;
240
+ if ( outerElement !== child ) {
241
+ outerElement ?. setAttribute ( 'role' , 'presentation' ) ;
242
+ }
243
+ if ( ! isActive ) {
244
+ child . setAttribute ( 'tabindex' , '-1' ) ;
245
+ }
246
+ if ( ! child . hasAttribute ( 'role' ) ) {
247
+ child . setAttribute ( 'role' , 'tab' ) ;
248
+ }
249
+
250
+ const target = this . getTargets ( rootElement , child ) ?. [ 0 ] ;
251
+ if ( ! target ) {
252
+ return ;
253
+ }
254
+ if ( ! target . hasAttribute ( 'role' ) ) {
255
+ target . setAttribute ( 'role' , 'tabpanel' ) ;
256
+ }
257
+ if ( child . id && ! target . hasAttribute ( 'aria-labelledby' ) ) {
258
+ target . setAttribute ( 'aria-labelledby' , child . id ) ;
259
+ }
260
+ } ) ;
261
+ } ) ;
262
+
263
+ const targetElements = Array . from ( rootElement . querySelectorAll < HTMLElement > ( toggleSelectorsList . join ( ',' ) ) ) ;
264
+
265
+ targetElements . forEach ( ( element ) => {
266
+ // Initialize the accordion.
267
+ element . addEventListener ( 'click' , async ( ev : Event ) => {
268
+ if ( element . classList . contains ( 'active' ) ) {
269
+ return ;
270
+ }
271
+
272
+ ev . preventDefault ( ) ;
273
+ ev . stopPropagation ( ) ;
274
+
275
+ // After rendering of core-format-text directive, the DOM element wrapped within a div
276
+ // is moved inside the current DOM core-format-text element. So the div element is not in the DOM and empty.
277
+ // @see formatAndRenderContents on format-text.ts.
278
+ const root = CoreDom . isElementInDom ( rootElement )
279
+ ? rootElement
280
+ : element . closest < HTMLElement > ( 'core-format-text' ) ;
281
+
282
+ if ( ! root ) {
283
+ return ;
284
+ }
285
+
286
+ const activeSelectors = toggleSelectorsList . map ( ( selector ) => `${ selector } .active` ) . join ( ',' ) ;
287
+ const active = root . querySelector < HTMLElement > ( activeSelectors ) ;
288
+ if ( active ) {
289
+ this . tabSetActive ( root , active , false ) ;
290
+ }
291
+
292
+ this . tabSetActive ( root , element , true ) ;
293
+ } ) ;
294
+ } ) ;
295
+ }
296
+
297
+ /**
298
+ * Set the active state of a tab element.
299
+ *
300
+ * @param root Root element where the tab is located.
301
+ * @param element Tab element to set active.
302
+ * @param isActive Whether the tab should be set as active or not.
303
+ */
304
+ protected static tabSetActive (
305
+ root : HTMLElement ,
306
+ element : HTMLElement ,
307
+ isActive : boolean ,
308
+ ) : void {
309
+ const target = this . getTargets ( root , element ) ?. [ 0 ] ;
310
+ if ( ! target ) {
311
+ return ;
312
+ }
313
+
314
+ element . classList . toggle ( 'active' , isActive ) ;
315
+ element . setAttribute ( 'aria-selected' , isActive ? 'true' : 'false' ) ;
316
+ if ( isActive ) {
317
+ element . removeAttribute ( 'tabindex' ) ;
318
+ } else {
319
+ element . setAttribute ( 'tabindex' , '-1' ) ;
320
+ }
321
+
322
+ target . classList . toggle ( 'active' , isActive ) ;
323
+ target . classList . toggle ( 'show' , isActive ) ;
324
+ }
325
+
197
326
/**
198
327
* Set the expanded state of a collapse element.
199
328
*
0 commit comments