Skip to content

Commit d55a8f0

Browse files
authored
Merge pull request #4511 from crazyserver/MOBILE-4855
MOBILE-4855 bootstrap: Support Bootstrap 4 and 5 tabs
2 parents 431ebf6 + 5675dbf commit d55a8f0

File tree

1 file changed

+129
-0
lines changed

1 file changed

+129
-0
lines changed

src/core/singletons/bootstrap.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import { CorePopovers } from '@services/overlays/popovers';
1616
import { CoreFormatTextOptions } from '@components/bs-tooltip/bs-tooltip';
1717
import { CoreModals } from '@services/overlays/modals';
18+
import { CoreDom } from './dom';
1819

1920
/**
2021
* Singleton with helper functions for Bootstrap.
@@ -40,6 +41,7 @@ export class CoreBootstrap {
4041
this.handleTooltipsAndPopovers(rootElement, formatTextOptions);
4142
this.handleAccordionsAndCollapse(rootElement);
4243
this.handleModals(rootElement, formatTextOptions);
44+
this.handleTabs(rootElement);
4345
this.enableDismissTrigger(rootElement, 'alert', 'close'); // Alert uses close method.
4446
this.enableDismissTrigger(rootElement, 'modal'); // Modal uses hide method
4547
this.enableDismissTrigger(rootElement, 'offcanvas'); // Offcanvas uses hide method
@@ -194,6 +196,133 @@ export class CoreBootstrap {
194196
});
195197
}
196198

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+
197326
/**
198327
* Set the expanded state of a collapse element.
199328
*

0 commit comments

Comments
 (0)