const smTabHeader = document.createElement('template') smTabHeader.innerHTML = `
`; customElements.define('sm-tab-header', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smTabHeader.content.cloneNode(true)) this.prevTab this.allTabs this.activeTab this.indicator = this.shadowRoot.querySelector('.indicator'); this.tabSlot = this.shadowRoot.querySelector('slot'); this.tabHeader = this.shadowRoot.querySelector('.tab-header'); this.changeTab = this.changeTab.bind(this) this.handleClick = this.handleClick.bind(this) this.handlePanelChange = this.handlePanelChange.bind(this) this.moveIndiactor = this.moveIndiactor.bind(this) } fireEvent(index) { this.dispatchEvent( new CustomEvent(`switchedtab${this.target}`, { bubbles: true, detail: { index: parseInt(index) } }) ) } moveIndiactor(tabDimensions) { this.indicator.setAttribute('style', `width: ${tabDimensions.width}px; transform: translateX(${tabDimensions.left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft}px)`) } changeTab(target) { if (target === this.prevTab || !target.closest('sm-tab')) return if (this.prevTab) this.prevTab.classList.remove('active') target.classList.add('active') this.tabHeader.scrollTo({ behavior: 'smooth', left: target.getBoundingClientRect().left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft }) this.moveIndiactor(target.getBoundingClientRect()) this.prevTab = target; this.activeTab = target; } handleClick(e) { if (e.target.closest('sm-tab')) { this.changeTab(e.target) this.fireEvent(e.target.dataset.index) } } handlePanelChange(e) { this.changeTab(this.allTabs[e.detail.index]) } connectedCallback() { if (!this.hasAttribute('target') || this.getAttribute('target').value === '') return; this.target = this.getAttribute('target') this.tabSlot.addEventListener('slotchange', () => { this.allTabs = this.tabSlot.assignedElements(); this.allTabs.forEach((tab, index) => { tab.dataset.index = index }) }) this.addEventListener('click', this.handleClick) document.addEventListener(`switchedpanel${this.target}`, this.handlePanelChange) let resizeObserver = new ResizeObserver(entries => { entries.forEach((entry) => { if (this.prevTab) { let tabDimensions = this.activeTab.getBoundingClientRect(); this.moveIndiactor(tabDimensions) } }) }) resizeObserver.observe(this) let observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.indicator.style.transition = 'none' if (this.activeTab) { let tabDimensions = this.activeTab.getBoundingClientRect(); this.moveIndiactor(tabDimensions) } else { this.allTabs[0].classList.add('active') let tabDimensions = this.allTabs[0].getBoundingClientRect(); this.moveIndiactor(tabDimensions) this.fireEvent(0) this.prevTab = this.tabSlot.assignedElements()[0]; this.activeTab = this.prevTab; } } }) }, { threshold: 1.0 }) observer.observe(this) } disconnectedCallback() { this.removeEventListener('click', this.handleClick) document.removeEventListener(`switchedpanel${this.target}`, this.handlePanelChange) } }) // tab const smTab = document.createElement('template') smTab.innerHTML = `
`; customElements.define('sm-tab', class extends HTMLElement { constructor() { super() this.shadow = this.attachShadow({ mode: 'open' }).append(smTab.content.cloneNode(true)) } }) // tab-panels const smTabPanels = document.createElement('template') smTabPanels.innerHTML = `
Nothing to see here.
`; customElements.define('sm-tab-panels', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smTabPanels.content.cloneNode(true)) this.isTransitioning = false this.panelContainer = this.shadowRoot.querySelector('.panel-container'); this.handleTabChange = this.handleTabChange.bind(this) } handleTabChange(e) { this.isTransitioning = true this.panelContainer.scrollTo({ left: this.allPanels[e.detail.index].getBoundingClientRect().left - this.panelContainer.getBoundingClientRect().left + this.panelContainer.scrollLeft, behavior: 'smooth' }) setTimeout(() => { this.isTransitioning = false }, 300); } fireEvent(index) { this.dispatchEvent( new CustomEvent(`switchedpanel${this.id}`, { bubbles: true, detail: { index: parseInt(index) } }) ) } connectedCallback() { const slot = this.shadowRoot.querySelector('slot'); slot.addEventListener('slotchange', (e) => { this.allPanels = e.target.assignedElements() this.allPanels.forEach((panel, index) => { panel.dataset.index = index intersectionObserver.observe(panel) }) }) document.addEventListener(`switchedtab${this.id}`, this.handleTabChange) const intersectionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (!this.isTransitioning && entry.isIntersecting) { this.fireEvent(entry.target.dataset.index) } }) }, { threshold: 0.6 }) } disconnectedCallback() { intersectionObserver.disconnect() document.removeEventListener(`switchedtab${this.id}`, this.handleTabChange) } })