<!--
If you are inserting it via a template element, move the CSS to a stylesheet or code manager. Move the JS to your header template. Feel free to delete this comment, and sign the code block.
-->
:root {
--tab-content-height: 450px;
--tab-content-bg: var(--tab-list-item-active-bg);
--tab-content-width: 100%;
--tab-list-item-active-bg: white; /* active and hover bg */
--tab-list-item-clr: black;
--tab-list-item-active-clr: black; /*active or hover color */
--tab-list-wrapper-bg: initial; /* tab sidebar bg */
--tab-list-width: 250px; /* tab sidebar width */
--tab-arrow-clr: gray;
--tab-divider-inset: 0px; /* divider line offset from top and bottom */
--tab-divider-width: 1px;
--tab-divider-clr: rgb(0 0 0 / 20%);
--bleed-bg: var(--tab-list-item-active-bg); /* list item bg color bleeds into tab content */
--bleed-height: 44px; /* adjust as needed based on list item height*/
--back-text-height: 40px;
}
/* reset on mobile */
.dwc-mobile {
--tab-list-wrapper-bg: initial;
--tab-list-item-clr: black;
}
/* reset on hero section */
[data-tabbed-hero="true"] {
--tab-content-width: 70%;
}
/* =================================
TABBED NAVIGATION FUNCTIONAL STYLES
================================= */
.dwc-tabbed-nav-list__li.active>button .dwc-tabbed-nav-list__li__btn-txt,
.dwc-tabbed-nav-list__li.active> :is(a, button),
.dwc-tabbed-nav-list__li.active>button i,
.dwc-tabbed-nav-list__li:hover>button .dwc-tabbed-nav-list__li__btn-txt,
.dwc-tabbed-nav-list__li:hover> :is(a, button) {
color: var(--tab-list-item-active-clr) !important;
}
/* Base structure - required for functionality */
.dwc-tabbed-nav-list {
inline-size: var(--tab-list-width);
}
.dwc-tabbed-nav-list__li__content__inner {
flex-grow: 1;
}
.dwc-tabbed-nav-list__li__content {
position: absolute;
inset-block: 0;
inset-inline-end: 0;
inset-inline-start: var(--tab-list-width);
inline-size: calc(100% - var(--tab-list-width));
overflow-y: auto;
scrollbar-width: none;
}
/* nested container */
.dwc-tabbed-nav-list__li__content .dwc-tabbed-nav-list__li__content {
inset-block-end: auto;
}
.dwc-tabbed-nav-list__li__content__inner:has(.dwc-tabbed-nav-container) {
padding-inline: 0;
padding-block: 0;
}
.dwc-tabbed-nav-list__li.active,
.dwc-tabbed-nav-list__li:hover,
.dwc-tabbed-nav-list__li:has(.dwc-tabbed-nav-container:hover) {
background-color: var(--tab-list-item-active-bg);
}
.bricks-is-frontend .dwc-tabbed-nav-list__li__content:not(.dwc-tabbed-nav-list__li.active > .dwc-tabbed-nav-list__li__content, :has(.dwc-tabbed-nav-list__li.active > .dwc-tabbed-nav-list__li__content)) {
visibility: hidden;
pointer-events: none;
}
.bricks-is-frontend .dwc-tabbed-nav-list__li__content:not(.dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__content) {
transition: max-height 0.3s, opacity 0.3s ease, transform 0.3s ease;
}
/* add active style to span inside active links (some tabbed nav links) */
.dwc-tabbed-nav-container a[aria-current="page"]>span,
.dwc-tabbed-nav-container .dwc-tabbed-nav-list__li:has(a[aria-current="page"])>button>span {
color: var(--menu-item-active-clr);
background-color: var(--menu-item-active-bg);
}
/* =================================
DESKTOP FUNCTIONALITY (768px and up)
================================= */
@media (min-width: 1201px) {
.dwc-tabbed-nav-list__li__content {
overscroll-behavior: contain;
}
/* hide, take out of document flow when hidden on desktop */
.bricks-is-frontend .dwc-tabbed-nav-list__li__content:not(.dwc-tabbed-nav-list__li.active > .dwc-tabbed-nav-list__li__content, :has(.dwc-tabbed-nav-list__li.active > .dwc-tabbed-nav-list__li__content), [data-fade-in-content="true"] .dwc-tabbed-nav-list__li__content) {
display: none;
}
/* Show content when active (hovered/clicked) */
.dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__content {
visibility: visible;
}
.dwc-tabbed-nav-list__li:hover>*>.dwc-tabbed-nav-list__li__arrow-icon {
transform: translate(0.25em)
}
/* Arrow icon transitions */
.dwc-tabbed-nav-list__li__arrow-icon {
transition: transform 0.3s ease;
}
/*fade in content*/
.bricks-is-frontend [data-fade-in-content="true"] .dwc-tabbed-nav-list__li__content:not(.dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__content) {
transition: visibility 0s 0.3s, max-height 0.3s, opacity 0.3s ease, transform 0.3s ease;
}
.bricks-is-frontend [data-fade-in-content="true"] .dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__content {
transition: visibility 0s, max-height 0.3s, opacity 0.3s ease, transform 0.3s ease;
}
.bricks-is-frontend [data-fade-in-content="true"] .dwc-tabbed-nav-list__li__content {
transform: translateX(-20px);
opacity: 0;
}
.bricks-is-frontend [data-fade-in-content="true"] .dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__content {
transform: translateX(0);
opacity: 1;
}
/* DIVIDER LINE */
:is([data-tabbed-hero="true"])>div .dwc-tabbed-nav-list {
margin-block: auto;
}
[data-divider="true"]>div .dwc-tabbed-nav-list>li {
margin-block: auto;
}
[data-divider="true"]>div .dwc-tabbed-nav-list::before {
content: '';
position: absolute;
left: calc(var(--tab-list-width) - 1px);
inset-block: var(--tab-divider-inset);
width: var(--tab-divider-width);
background-color: var(--tab-divider-clr);
}
[data-divider="true"] .dwc-tabbed-nav-list__li__content {
inline-size: calc(100% - (var(--tab-list-width) + var(--tab-divider-width)));
}
/* BLEEDING EDGE */
[data-bleed="true"] .dwc-tabbed-nav-list__li.active::after {
content: '';
position: absolute;
height: var(--bleed-height);
width: calc(var(--tab-list-width) + var(--tab-divider-width));
background-color: var(--bleed-bg);
inset-inline-start: 0;
z-index: -1;
border-right: solid var(--tab-divider-width) var(--bleed-bg);
}
[data-bleed="true"][data-divider="true"] .dwc-tabbed-nav-list__li.active::after {
border-top: solid var(--tab-divider-width) var(--tab-divider-clr);
border-bottom: solid var(--tab-divider-width) var(--tab-divider-clr);
}
[data-bleed="true"] .dwc-tabbed-nav-list__li.active {
isolation: isolate;
}
/* HERO TAB*/
/* place the hero block inside the tab nav inner wrap*/
[data-tabbed-hero="true"] .dwc-tabbed-nav-inner-wrap {
flex-direction: row;
align-items: stretch;
}
[data-tabbed-hero="true"] .dwc-tabbed-nav__list-wrapper {
flex-shrink: 0;
}
.dwc-tabbed-nav__list-wrapper {
inline-size: var(--tab-list-width);
}
.dwc-tabbed-nav-container:not([data-tabbed-hero="true"]) .dwc-tabbed-nav__list-wrapper {
flex-grow: 1;
}
[data-tabbed-hero="true"] .dwc-tabbed-nav-list {
flex-shrink: 0;
}
.dwc-tabbed-nav__list-wrapper>.dwc-tabbed-nav-list {
inline-size: 100%;
}
.dwc-tabbed-nav-list__li__content {
max-width: var(--tab-content-width)
}
}
/* =================================
MOBILE FUNCTIONALITY
================================= */
.dwc-mobile .dwc-tabbed-nav-container[data-slide-in="true"],
.dwc-mobile [data-slide-in="true"] .dwc-tabbed-nav-inner-wrap,
.dwc-mobile [data-slide-in="true"] .dwc-tabbed-nav__list-wrapper>.dwc-tabbed-nav-list,
.dwc-mobile [data-slide-in="true"] .dwc-tabbed-nav__list-wrapper,
.dwc-mobile [data-slide-in="true"] .dwc-tabbed-nav-list {
flex-grow: 1;
}
@media (max-width: 1200px) {
.dwc-tabbed-nav__list-wrapper>.dwc-tabbed-nav-list,
.dwc-tabbed-nav__list-wrapper,
.dwc-tabbed-nav-list {
inline-size: 100%;
}
/* Content positioned relatively on mobile */
.dwc-tabbed-nav-list__li__content {
position: relative;
left: auto;
top: auto;
max-height: 0;
opacity: 1;
visibility: visible;
transform: none;
overflow-y: auto;
overflow-x: hidden;
transition: visibility 0.3s, max-height 0.3s ease;
inline-size: 100%;
}
.bricks-is-frontend .dwc-tabbed-nav-list__li__content:not(.dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__content) {
transition: visibility 0.3s ease, max-height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;
}
.dwc-tabbed-nav-list__li {
flex-wrap: wrap;
}
/* =================================
SLIDE IN ON MOBILE
================================= */
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-inner-wrap {
height: calc(100dvb - var(--back-text-height));
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li__content {
position: absolute;
inset: 0;
transform: translateX(100%);
max-height: unset !important;
transition: 0.3s;
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active>.dwc-tabbed-nav-list__li__content {
transform: translateX(0%);
}
.bricks-is-frontend .dwc-nest-menu:not([data-single-back-button="true"]) [data-slide-in="true"].dwc-tabbed-nav-container {
padding-top: var(--back-text-height);
}
.bricks-is-frontend [data-slide-in="true"].dwc-tabbed-nav-container {
overflow-x: hidden;
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active>button>.dwc-tabbed-nav-list__li__arrow-icon {
position: absolute;
top: calc((var(--back-text-height) + 10px) * -1);
height: var(--back-text-height);
width: auto;
display: flex;
place-items: center;
transition: 0s;
background: #ffffff;
padding-inline: 10px;
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active>button>.dwc-tabbed-nav-list__li__arrow-icon:before {
rotate: 180deg;
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__arrow-icon::after {
font-family: 'Inter', sans-serif;
font-weight: 500;
letter-spacing: 1px;
font-size: 16px;
height: var(--back-text-height);
display: grid;
place-items: center;
padding-inline-start: 10px;
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active>button [data-back-text="auto"].dwc-tabbed-nav-list__li__arrow-icon::after {
content: attr(data-text);
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__arrow-icon:not([data-back-text="auto"])::after {
content: attr(data-back-text);
}
/* end slide in*/
/* second level slide in*/
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li.active>button>.dwc-tabbed-nav-list__li__arrow-icon {
top: 0px;
z-index: 2;
background: #ffffff;
}
.bricks-is-frontend [data-slide-in="true"] .dwc-tabbed-nav-list__li.active>.dwc-tabbed-nav-list__li__content .dwc-tabbed-nav-list__li__content {
padding-top: var(--back-text-height)
}
.bricks-is-frontend [data-slide-in="true"].dwc-tabbed-nav-container [data-slide-in="true"].dwc-tabbed-nav-container {
padding-top: 0;
}
.bricks-is-frontend [data-slide-in="true"].dwc-tabbed-nav-container:has(.dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li.active) {
padding-top: 0 !important;
}
.bricks-is-frontend .dwc-tabbed-nav-list [data-slide-in="true"] .dwc-tabbed-nav-list:has(.dwc-tabbed-nav-list__li.active) {
padding-top: var(--back-text-height);
}
/*end second level*/
/* second level indent */
.bricks-is-frontend .dwc-tabbed-nav-container:not([data-slide-in="true"]) .dwc-tabbed-nav-list .dwc-tabbed-nav-list {
padding-left: 10px
}
/* when not sliding in */
[data-slide-in]:not([data-slide-in="true"]) .dwc-tabbed-nav-list__li__arrow-icon {
rotate: 90deg;
transition: 0.3s ease;
}
[data-slide-in]:not([data-slide-in="true"]) .dwc-tabbed-nav-list__li.active .dwc-tabbed-nav-list__li__arrow-icon {
rotate: 270deg;
}
/* SHOW ONE BACK BUTTON FOR TABBED NAV */
.bricks-is-frontend .dwc-nest-menu[data-single-back-button="true"]:not([data-submenu-reveal="expand"]):has(.dwc-tabbed-nav-list__li.active) .brxe-dropdown:not([data-submenu-reveal="expand"]) .brx-submenu-toggle button[aria-expanded] {
opacity: 0 !important;
}
.bricks-is-frontend .dwc-nest-menu[data-single-back-button="true"]:not([data-submenu-reveal="expand"]):has(.dwc-tabbed-nav-list__li.active) .brx-has-megamenu.brxe-dropdown .brx-dropdown-content:not([data-hide-close-bar="true"] ul, [data-submenu-reveal="expand"].brx-has-megamenu ul) {
inset-block-start: 0 !important;
}
.bricks-is-frontend .dwc-nest-menu[data-single-back-button="true"]:not([data-submenu-reveal="expand"]):has(.dwc-tabbed-nav-list__li.active) [data-slide-in=true].dwc-tabbed-nav-container {
padding-top: calc(var(--back-text-height) * 1.5) !important;
}
/* max-width media query ends */
}
/* TABBED DROPDOWN */
html:not(.dwc-mobile) [data-tabbed-dropdown="true"] {
min-inline-size: var(--tab-list-width, 250px);
}
html:not(.dwc-mobile) [data-tabbed-dropdown="true"]>.brx-submenu-toggle>span {
margin-inline: auto
}
html:not(.dwc-mobile) [data-tabbed-dropdown="true"] .brx-dropdown-content {
background-color: initial;
}
html:not(.dwc-mobile) [data-tabbed-dropdown="true"].brxe-dropdown>.brx-dropdown-content {
transform: translateY(-15px) scaleX(1) scaleY(1);
}
/* =================================
TRANSITIONS AND ANIMATIONS
================================= */
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.dwc-tabbed-nav-list__li__content,
.dwc-tabbed-nav-list__li__arrow-icon {
transition: none;
}
}
/* inside builder */
[data-builder-mode] .dwc-tabbed-nav-list__li__content__inner>li::before {
content: 'WARNING: Change the nested Tabbed Nav Container tag from <li> to <div>. Click here to select';
background: red;
padding: 0.5em;
font-size: 1.5em;
color: white;
font-weight: bold;
}
[data-builder-mode] .brx-dropdown-content>*:not(li, .brxe-template):has(.dwc-tabbed-nav-container)::after {
content: 'STEP1: change the newly added wrapper tag to <li>';
display: inline-block;
background: red;
min-block-size: 80px;
padding: 1rem;
font-weight: bold;
color: white;
}
[data-builder-mode] .brx-dropdown-content>li:has(li.dwc-tabbed-nav-container):after {
content: 'STEP 2: now change the Tabbed Nav Container tag from <li> to <div>';
display: inline-block;
background: green;
min-block-size: 80px;
padding: 1rem;
font-weight: bold;
color: white;
}
// Configuration object for centralized settings
const TabbedNavConfig = {
DESKTOP_BREAKPOINT: 1201,
DEBOUNCE_DELAY: 250,
OPEN_FIRST_TAB: true,
CLOSE_TAB_ON_MOUSEOUT: false,
TOGGLE_ON_CLICK: false // false = hover, true = click
};
// Custom config for tabbed hero containers
const tabbedHeroConfig = {
...TabbedNavConfig,
OPEN_FIRST_TAB: false,
CLOSE_TAB_ON_MOUSEOUT: true
};
// Function to determine the appropriate config based on container context
function getConfigForContainer(container) {
// Check if container has closest ancestor with [data-tabbed-hero="true"]
const heroAncestor = container.getAttribute('data-tabbed-hero') === 'true' ? container : container.closest('[data-tabbed-dropdown="true"]');
if (heroAncestor) {
return tabbedHeroConfig;
}
// Check for specific data-instance attributes (if you still want to support them)
// const instance = container.getAttribute('data-instance');
// if (instance === 'two') {
// // You can still have specific instance configs if needed
// return {
// ...TabbedNavConfig,
// OPEN_FIRST_TAB: false,
// CLOSE_TAB_ON_MOUSEOUT: true
// };
// }
// Default config
return TabbedNavConfig;
}
class TabbedNavigation {
constructor(container, config = TabbedNavConfig) {
this.container = container;
this.config = config;
this.activeItem = null;
this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT;
this.hasMouseListeners = false;
this.megamenuParent = null;
this.megamenuObserver = null;
// Initialize bound handlers - these will be reused across reinitializations
this.mouseEnterHandler = this.handleMouseEnter.bind(this);
this.mouseLeaveHandler = this.handleMouseLeave.bind(this);
this.resizeHandler = this.debounce(this.handleResize.bind(this), this.config.DEBOUNCE_DELAY);
this.handleMegamenuToggle = this.handleMegamenuToggle.bind(this);
this.init();
}
init() {
// Cache DOM elements fresh each time
this.navItems = [...this.container.querySelectorAll('.dwc-tabbed-nav-list__li')];
this.elementsCache = new Map();
this.cacheElements();
this.setupAccessibility();
this.setupMegamenuObserver();
this.bindEvents();
// Set initial state
this.updateLayout();
this.setBackText();
// Calculate and apply minimum heights after everything is set up
if (this.isDesktop) {
// Use requestAnimationFrame to ensure DOM is fully rendered
requestAnimationFrame(() => {
this.calculateAndApplyMinHeight();
});
}
// Show first item on page load only if desktop, not nested, and OPEN_FIRST_TAB is true
if (this.navItems.length > 0 && this.isDesktop && this.config.OPEN_FIRST_TAB) {
setTimeout(() => {
const firstItem = this.navItems[0];
// Count how many .dwc-tabbed-nav-container ancestors this.container has
let count = 0;
let current = this.container.parentElement;
while (current) {
if (current.classList?.contains('dwc-tabbed-nav-container')) {
count++;
}
current = current.parentElement;
}
const isNested = count >= 1;
if (!isNested && this.elementsCache.has(firstItem)) {
this.showTab(firstItem);
}
}, 0);
}
}
// New method to calculate and apply minimum height
calculateAndApplyMinHeight() {
if (!this.isDesktop) {
// Clear any existing height variables on mobile
this.clearMinHeight();
return;
}
// Find all inner wrap containers within this tabbed navigation
setTimeout(() => {
const innerWraps = this.container.querySelectorAll('.dwc-tabbed-nav-inner-wrap');
innerWraps.forEach(innerWrap => {
this.setMinHeightForInnerWrap(innerWrap);
});
}, 1500);
console.log('min-height applied')
}
// Set minimum height for a specific inner wrap container
setMinHeightForInnerWrap(innerWrap) {
// Find all content inner elements within this inner wrap
const contentInners = innerWrap.querySelectorAll('.dwc-tabbed-nav-list__li__content__inner');
if (contentInners.length === 0) return;
let maxHeight = 0;
const originalStates = [];
// Temporarily show all content to measure accurate heights
contentInners.forEach((contentInner, index) => {
const contentElement = contentInner.closest('.dwc-tabbed-nav-list__li__content');
const navItem = contentInner.closest('.dwc-tabbed-nav-list__li');
if (contentElement && navItem) {
// Store original state
const originalState = {
element: contentElement,
navItem: navItem,
wasHidden: contentElement.getAttribute('aria-hidden') === 'true',
hadActiveClass: navItem.classList.contains('active'),
originalMaxHeight: contentElement.style.maxHeight,
originalDisplay: contentElement.style.display,
originalVisibility: contentElement.style.visibility
};
originalStates.push(originalState);
// Temporarily make visible for measurement
contentElement.style.display = 'block';
contentElement.style.visibility = 'hidden'; // Hidden but takes up space
contentElement.style.maxHeight = 'none';
contentElement.setAttribute('aria-hidden', 'false');
navItem.classList.add('active');
}
});
// Force a reflow to ensure styles are applied
innerWrap.offsetHeight;
// Now measure all heights
contentInners.forEach(contentInner => {
const height = contentInner.offsetHeight;
if (height > maxHeight) {
maxHeight = height;
}
});
// Restore original states
originalStates.forEach(state => {
const { element, navItem, wasHidden, hadActiveClass, originalMaxHeight, originalDisplay, originalVisibility } = state;
element.style.maxHeight = originalMaxHeight;
element.style.display = originalDisplay;
element.style.visibility = originalVisibility;
if (wasHidden) {
element.setAttribute('aria-hidden', 'true');
}
if (!hadActiveClass) {
navItem.classList.remove('active');
}
});
// Apply the maximum height as CSS custom property
if (maxHeight > 0) {
innerWrap.style.setProperty('--tabbed-content-min-height', `${maxHeight}px`);
}
}
// Clear minimum height custom property
clearMinHeight() {
const innerWraps = this.container.querySelectorAll('.dwc-tabbed-nav-inner-wrap');
innerWraps.forEach(innerWrap => {
innerWrap.style.removeProperty('--tabbed-content-min-height');
});
}
// Setup observer for megamenu parent
setupMegamenuObserver() {
// Find the .brx-has-megamenu parent
this.megamenuParent = this.container.closest('.brx-has-megamenu');
if (this.megamenuParent) {
// Set initial inert state
this.updateInertState();
// Create a MutationObserver to watch for class changes
this.megamenuObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
this.updateInertState();
}
});
});
// Start observing the megamenu parent for class changes
this.megamenuObserver.observe(this.megamenuParent, {
attributes: true,
attributeFilter: ['class']
});
}
}
// Update inert state based on megamenu open/closed state
updateInertState() {
if (!this.megamenuParent) return;
const isOpen = this.megamenuParent.classList.contains('open');
if (isOpen) {
this.container.removeAttribute('inert');
} else {
this.container.setAttribute('inert', '');
}
}
// Handle megamenu toggle (for manual triggering if needed)
handleMegamenuToggle() {
this.updateInertState();
}
// Method to clean up event listeners and reset state
cleanup() {
// Remove all event listeners
this.container.removeEventListener('click', this.handleClick);
this.container.removeEventListener('keydown', this.handleKeydown);
this.container.removeEventListener('focus', this.handleFocus);
// Remove mouse listeners if they exist
if (this.hasMouseListeners) {
this.container.removeEventListener('mouseenter', this.mouseEnterHandler, true);
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler, true);
}
// Remove resize listener
window.removeEventListener('resize', this.resizeHandler);
// Clean up megamenu observer
if (this.megamenuObserver) {
this.megamenuObserver.disconnect();
this.megamenuObserver = null;
}
// Clear minimum height custom properties
this.clearMinHeight();
// Reset state
this.activeItem = null;
this.hasMouseListeners = false;
this.megamenuParent = null;
// Clear caches
if (this.elementsCache) {
this.elementsCache.clear();
}
this.navItems = [];
// Reset all tab states and accessibility attributes
const allNavItems = this.container.querySelectorAll('.dwc-tabbed-nav-list__li');
allNavItems.forEach(item => {
const button = item.querySelector('.dwc-tabbed-nav-list__li__button') ||
item.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
const content = item.querySelector('.dwc-tabbed-nav-list__li__content');
// Remove classes
item.classList.remove('active');
// Reset button attributes
if (button) {
button.removeAttribute('role');
button.removeAttribute('aria-controls');
button.removeAttribute('aria-expanded');
button.removeAttribute('aria-selected');
button.removeAttribute('tabindex');
button.removeAttribute('id');
}
// Reset content attributes
if (content) {
content.removeAttribute('role');
content.removeAttribute('aria-labelledby');
content.removeAttribute('id');
content.removeAttribute('aria-hidden');
content.style.maxHeight = '';
}
// Remove data attributes
item.removeAttribute('data-tab-index');
});
// Reset nav list role
const navList = this.container.querySelector('.dwc-tabbed-nav-list');
if (navList) {
navList.removeAttribute('role');
}
// Remove inert attribute
this.container.removeAttribute('inert');
}
// Public method to reinitialize the navigation
reinitialize() {
this.cleanup();
this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT; // Use config breakpoint
this.init();
}
// Cache DOM elements to avoid repeated queries
cacheElements() {
const uniqueIdBase = `tab-${Date.now()}`;
this.navItems.forEach((item, index) => {
const button = item.querySelector('.dwc-tabbed-nav-list__li__button') ||
item.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
const content = item.querySelector('.dwc-tabbed-nav-list__li__content');
this.elementsCache.set(item, {
button,
content,
uniqueId: `${uniqueIdBase}-${index}`,
index
});
});
}
setupAccessibility() {
const navList = this.container.querySelector('.dwc-tabbed-nav-list');
if (!navList) return;
navList.setAttribute('role', 'tablist');
for (const [item, cache] of this.elementsCache) {
const { button, content, uniqueId, index } = cache;
if (button) {
button.setAttribute('role', 'tab');
button.setAttribute('aria-controls', `${uniqueId}-panel`);
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-selected', 'false');
button.setAttribute('tabindex', '0');
button.setAttribute('id', `${uniqueId}-tab`);
}
if (content) {
content.setAttribute('role', 'tabpanel');
content.setAttribute('aria-labelledby', `${uniqueId}-tab`);
content.setAttribute('id', `${uniqueId}-panel`);
content.setAttribute('aria-hidden', 'true');
}
item.setAttribute('data-tab-index', index);
}
}
bindEvents() {
// Store bound handlers for cleanup
this.handleClick = this.handleClick.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleFocus = this.handleFocus.bind(this);
// Single event delegation for better performance
this.container.addEventListener('click', this.handleClick);
this.container.addEventListener('keydown', this.handleKeydown);
this.container.addEventListener('focus', this.handleFocus, true);
// Passive resize listener for better performance
window.addEventListener('resize', this.resizeHandler, { passive: true });
this.updateMouseListeners();
}
updateMouseListeners() {
// Only add mouse listeners for hover behavior (when TOGGLE_ON_CLICK is false)
const shouldHaveMouseListeners = this.isDesktop && !this.config.TOGGLE_ON_CLICK;
if (shouldHaveMouseListeners && !this.hasMouseListeners) {
this.container.addEventListener('mouseenter', this.mouseEnterHandler, true);
this.container.addEventListener('mouseleave', this.mouseLeaveHandler, true);
this.hasMouseListeners = true;
} else if (!shouldHaveMouseListeners && this.hasMouseListeners) {
this.container.removeEventListener('mouseenter', this.mouseEnterHandler, true);
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler, true);
this.hasMouseListeners = false;
}
}
// Helper method to get sibling nav items (nav items in the same container level)
getSiblingNavItems(navItem) {
// Find the direct parent container of this nav item
const navItemContainer = navItem.closest('.dwc-tabbed-nav-list');
if (!navItemContainer) return [];
// Get all nav items that are direct children of the same container
const siblingItems = [...navItemContainer.querySelectorAll('.dwc-tabbed-nav-list__li')];
// Filter to only include items that are siblings (not the navItem itself)
// and check if they belong to any TabbedNavigation instance
return siblingItems.filter(item => item !== navItem);
}
handleFocus(e) {
if (!this.isDesktop) return;
const button = e.target.closest('[role="tab"]');
if (!button) return;
const navItem = button.closest('.dwc-tabbed-nav-list__li');
if (!navItem || !this.elementsCache.has(navItem)) return;
this.handleTabInteraction(navItem, 'focus');
}
handleClick(e) {
const navItem = e.target.closest('.dwc-tabbed-nav-list__li');
if (!navItem || !this.elementsCache.has(navItem)) return;
const button = e.target.closest('.dwc-tabbed-nav-list__li__button, .dwc-tabbed-nav-list__li__btn-txt');
if (!button) return;
// e.preventDefault();
this.handleTabInteraction(navItem, 'click');
}
handleKeydown(e) {
const button = e.target.closest('[role="tab"]');
if (!button) return;
const navItem = button.closest('.dwc-tabbed-nav-list__li');
if (!navItem || !this.elementsCache.has(navItem)) return;
const { index: currentIndex } = this.elementsCache.get(navItem);
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.handleTabInteraction(navItem, 'keyboard');
break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
this.focusNextTab(currentIndex);
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
this.focusPrevTab(currentIndex);
break;
case 'Home':
e.preventDefault();
this.focusTab(0);
break;
case 'End':
e.preventDefault();
this.focusTab(this.navItems.length - 1);
break;
}
}
handleMouseEnter(e) {
if (!this.isDesktop || this.config.TOGGLE_ON_CLICK) return;
const navItem = e.target.closest('.dwc-tabbed-nav-list__li') ||
(e.target.matches('.dwc-tabbed-nav-list__li') ? e.target : null);
if (!navItem || !this.elementsCache.has(navItem)) return;
this.handleTabInteraction(navItem, 'hover');
}
handleMouseLeave(e) {
if (!this.isDesktop || this.config.TOGGLE_ON_CLICK) return;
// Only close tabs if CLOSE_TAB_ON_MOUSEOUT is enabled
if (this.config.CLOSE_TAB_ON_MOUSEOUT) {
// Check if mouse is leaving the entire container
const rect = this.container.getBoundingClientRect();
const { clientX, clientY } = e;
// Check if mouse position is outside the container bounds
const isOutsideContainer = (
clientX < rect.left ||
clientX > rect.right ||
clientY < rect.top ||
clientY > rect.bottom
);
if (isOutsideContainer) {
this.hideAllTabs();
}
}
// If CLOSE_TAB_ON_MOUSEOUT is false, keep content visible (original behavior)
}
// Unified method to handle all tab interactions with nesting support
handleTabInteraction(navItem, interactionType) {
if (!this.elementsCache.has(navItem)) return;
if (this.isDesktop) {
// Check interaction type for desktop behavior
if (this.config.TOGGLE_ON_CLICK) {
// Click mode: only activate on click or keyboard
if (interactionType === 'click' || interactionType === 'keyboard') {
this.hideSiblingTabs(navItem);
this.showTab(navItem);
}
// Ignore hover and focus in click mode
} else {
// Hover mode: activate on hover, focus, click, or keyboard (original behavior)
this.hideSiblingTabs(navItem);
this.showTab(navItem);
}
} else {
// Mobile accordion behavior
if (interactionType === 'click' || interactionType === 'keyboard') {
this.toggleTabMobile(navItem);
}
}
}
// Hide only sibling tabs (same container level)
hideSiblingTabs(navItem) {
// console.log('Nav Item: ', navItem)
const siblings = this.getSiblingNavItems(navItem);
siblings.forEach(sibling => {
//console.log('sibling: ', sibling)
// Only hide siblings that have the active class to avoid affecting other instances
if (sibling.classList.contains('active')) {
this.hideSiblingTab(sibling);
}
});
}
// Hide a sibling tab (used when we know it's a sibling from another instance)
hideSiblingTab(navItem) {
const button = navItem.querySelector('.dwc-tabbed-nav-list__li__button') ||
navItem.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
const content = navItem.querySelector('.dwc-tabbed-nav-list__li__content');
if (content && button) {
content.setAttribute('aria-hidden', 'true');
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-selected', 'false');
navItem.classList.remove('active');
// Also handle mobile accordion collapse
if (!this.isDesktop) {
content.style.maxHeight = '0';
}
}
}
// Mobile-specific toggle behavior with proper nesting support
toggleTabMobile(navItem) {
if (!this.elementsCache.has(navItem)) return;
const isActive = navItem.classList.contains('active');
if (isActive) {
this.collapseTab(navItem);
} else {
// Close sibling tabs and open this one
this.hideSiblingTabs(navItem);
this.expandTab(navItem);
}
}
toggleTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
if (this.isDesktop) {
this.handleTabInteraction(navItem, 'click');
} else {
this.toggleTabMobile(navItem);
}
}
showTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
const { button, content } = this.elementsCache.get(navItem);
if (content && button) {
content.setAttribute('aria-hidden', 'false');
button.setAttribute('aria-expanded', 'true');
button.setAttribute('aria-selected', 'true');
navItem.classList.add('active');
}
this.setActiveTab(navItem);
}
hideTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
const { button, content } = this.elementsCache.get(navItem);
if (content && button) {
content.setAttribute('aria-hidden', 'true');
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-selected', 'false');
navItem.classList.remove('active');
}
// Only clear activeItem if it belongs to THIS navigation instance
// and it's the exact item being hidden
if (this.activeItem === navItem && this.navItems.includes(navItem)) {
this.activeItem = null;
}
}
expandTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
// Close sibling tabs
this.hideSiblingTabs(navItem);
this.showTab(navItem);
// Smooth height animation
const { content } = this.elementsCache.get(navItem);
if (content) {
const height = content.scrollHeight;
content.style.maxHeight = `${height}px`;
}
}
collapseTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
this.hideTab(navItem);
const { content } = this.elementsCache.get(navItem);
if (content) {
content.style.maxHeight = '0';
}
}
hideAllTabs() {
this.navItems.forEach(item => this.hideTab(item));
}
setActiveTab(navItem) {
// Only update aria-selected for tabs in THIS navigation instance
this.navItems.forEach(item => {
const cache = this.elementsCache.get(item);
if (cache?.button) {
cache.button.setAttribute('aria-selected', item === navItem ? 'true' : 'false');
}
});
// Only set activeItem if this navItem belongs to THIS navigation instance
if (this.navItems.includes(navItem)) {
this.activeItem = navItem;
}
}
focusTab(index) {
if (index >= 0 && index < this.navItems.length) {
const cache = this.elementsCache.get(this.navItems[index]);
if (cache?.button) {
cache.button.focus();
}
}
}
focusNextTab(currentIndex) {
const nextIndex = (currentIndex + 1) % this.navItems.length;
this.focusTab(nextIndex);
}
focusPrevTab(currentIndex) {
const prevIndex = currentIndex === 0 ? this.navItems.length - 1 : currentIndex - 1;
this.focusTab(prevIndex);
}
handleResize() {
const wasDesktop = this.isDesktop;
this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT; // Use config breakpoint
if (wasDesktop !== this.isDesktop) {
this.updateMouseListeners();
this.updateLayout();
// Recalculate heights when switching to desktop, or clear when switching to mobile
if (this.isDesktop) {
// Delay to ensure layout is stable after resize
setTimeout(() => {
this.calculateAndApplyMinHeight();
}, 0);
} else {
this.clearMinHeight();
}
}
}
updateLayout() {
const currentlyActive = this.activeItem;
// Reset all states
this.hideAllTabs();
// Clear inline styles
this.navItems.forEach(item => {
const cache = this.elementsCache.get(item);
if (cache?.content) {
cache.content.style.maxHeight = '';
}
});
if (this.isDesktop) {
// Determine if this tab instance is nested
let count = 0;
let current = this.container.parentElement;
while (current) {
if (current.classList?.contains('dwc-tabbed-nav-container')) {
count++;
}
current = current.parentElement;
}
const isNested = count >= 1;
if (!isNested && this.config.OPEN_FIRST_TAB) {
if (currentlyActive) {
this.showTab(currentlyActive);
} else if (this.navItems.length > 0) {
this.showTab(this.navItems[0]);
}
}
// Recalculate min height after layout update
setTimeout(() => {
this.calculateAndApplyMinHeight();
}, 1200);
} else {
// Clear min height on mobile
this.clearMinHeight();
}
}
setBackText() {
let tabElement = document.querySelector('.dwc-tabbed-nav-container');
let tabBackText = tabElement.getAttribute('data-back-text')
document.querySelectorAll('.dwc-tabbed-nav-list__li__button').forEach(tabBtn => {
const textContent = tabBtn.textContent.trim();
const tabIcon = tabBtn.querySelector('.dwc-tabbed-nav-list__li__arrow-icon');
if (tabIcon) {
tabIcon.setAttribute('data-text', textContent);
tabIcon.setAttribute('data-back-text', tabBackText);
}
});
}
// Simple debounce for resize
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Public method to recalculate heights (useful for dynamic content)
recalculateHeights() {
if (this.isDesktop) {
this.calculateAndApplyMinHeight();
}
}
// Public method to destroy the instance completely
destroy() {
this.cleanup();
// Additional cleanup can be added here if needed
}
// Public method to manually update inert state (if needed)
updateInert() {
this.updateInertState();
}
}
// Simplified initialization on DOM ready
document.addEventListener('DOMContentLoaded', function () {
const tabbedNavContainers = document.querySelectorAll('.dwc-tabbed-nav-container');
tabbedNavContainers.forEach(container => {
// Skip if this container is nested inside another dwc-tabbed-nav-container
const parentNavContainer = container.parentElement.closest('.dwc-tabbed-nav-container');
if (parentNavContainer) {
return; // Skip nested containers
}
// Get the appropriate config for this container
const config = getConfigForContainer(container);
// Initialize with the determined config
new TabbedNavigation(container, config);
});
});
// For dynamic content - Updated version
window.initTabbedNav = function (container, customConfig = null) {
if (container && container.classList.contains('dwc-tabbed-nav-container')) {
// Check if this is a nested container
const parentNavContainer = container.parentElement.closest('.dwc-tabbed-nav-container');
if (parentNavContainer) {
return null; // Don't initialize nested containers
}
// Use custom config if provided, otherwise determine config automatically
const config = customConfig || getConfigForContainer(container);
return new TabbedNavigation(container, config);
}
return null;
};
/*
// USAGE EXAMPLES:
// TO REINITIALIZE USE THIS
// Create an instance of TabbedNavigation
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));
// Call reinitialize on the instance
tabbedNav.reinitialize();
// TO RECALCULATE HEIGHTS (useful when content changes dynamically)
tabbedNav.recalculateHeights();
// TO USE WITH CUSTOM CONFIG
const customConfig = {
DESKTOP_BREAKPOINT: 768,
DEBOUNCE_DELAY: 300,
OPEN_FIRST_TAB: false, // This will prevent first tab from opening automatically
CLOSE_TAB_ON_MOUSEOUT: true // This will close tabs when mouse leaves container
};
const tabbedNavCustom = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'), customConfig);
// TO UPDATE ON URL CHANGE
function onUrlChange() {
setTimeout(function () {
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));
tabbedNav.reinitialize();
}, 1500); // Delay execution by 2 seconds
}
// Listen for popstate event (history navigation)
window.addEventListener('popstate', onUrlChange);
// Listen for hashchange event (URL hash change)
window.addEventListener('hashchange', onUrlChange);
// Handle pushState and replaceState
(function (history) {
const pushState = history.pushState;
const replaceState = history.replaceState;
history.pushState = function (state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({ state: state });
}
const result = pushState.apply(history, arguments);
onUrlChange(); // Call the function when pushState is used
return result;
};
history.replaceState = function (state) {
if (typeof history.onreplacestate == "function") {
history.onreplacestate({ state: state });
}
const result = replaceState.apply(history, arguments);
onUrlChange(); // Call the function when replaceState is used
return result;
};
})(window.history);
// Get the instance and manually calculate heights
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));
tabbedNav.recalculateHeights();
// CSS USAGE:
// The --tabbed-content-min-height variable is applied to .dwc-tabbed-nav-inner-wrap elements
// You can use it in your CSS like this:
/*
.dwc-tabbed-nav-inner-wrap {
min-height: var(--tabbed-content-min-height, auto);
}
*/