1customElements.define('kiss-tabs', class extends HTMLElement { 2 3 static get observedAttributes() { 4 return []; 5 } 6 7 constructor() { 8 super(); 9 } 10 11 connectedCallback() { 12 13 if (this.getAttribute('static') == 'true') { 14 return; 15 } 16 17 this.activeIndex = Number(this.getAttribute('index') || 0); 18 19 this.nav = document.createElement("ul"); 20 21 this.nav.classList.add('kiss-tabs-nav'); 22 this.nav.setAttribute('role', 'tablist'); 23 this.prepend(this.nav); 24 25 this.render(); 26 27 this.addEventListener('click', e => { 28 if (!e.target.classList.contains('kiss-tabs-nav-link')) return; 29 this.setIndex(e.target.getAttribute('index')); 30 e.preventDefault(); 31 }); 32 33 // Keyboard navigation 34 this.nav.addEventListener('keydown', e => { 35 36 let link = e.target.closest('.kiss-tabs-nav-link'); 37 if (!link) return; 38 39 let handled = false; 40 41 switch (e.key) { 42 case 'ArrowRight': 43 this.setIndex((this.activeIndex + 1) % this.tabs.length); 44 this.focusActiveTab(); 45 handled = true; 46 break; 47 case 'ArrowLeft': 48 this.setIndex((this.activeIndex - 1 + this.tabs.length) % this.tabs.length); 49 this.focusActiveTab(); 50 handled = true; 51 break; 52 case 'Home': 53 this.setIndex(0); 54 this.focusActiveTab(); 55 handled = true; 56 break; 57 case 'End': 58 this.setIndex(this.tabs.length - 1); 59 this.focusActiveTab(); 60 handled = true; 61 break; 62 } 63 64 if (handled) { 65 e.preventDefault(); 66 } 67 }); 68 } 69 70 attributeChangedCallback(name, oldvalue, newvalue) { 71 if (oldvalue != newvalue) this.render(); 72 } 73 74 focusActiveTab() { 75 let link = this.nav.querySelector('.kiss-tabs-nav-link[aria-selected="true"]'); 76 if (link) link.focus(); 77 } 78 79 setIndex(index) { 80 81 this.activeIndex = Number(index); 82
83 this.tabs.forEach((tab, idx) => { 84 85 let isActive = this.activeIndex == idx; 86 let link = this.nav.children[idx]?.querySelector('.kiss-tabs-nav-link'); 87 88 this.nav.children[idx].setAttribute('active', isActive ? 'true' : 'false'); 89 tab.setAttribute('active', isActive ? 'true' : 'false'); 90 91 // ARIA: tab 92 if (link) { 93 link.setAttribute('aria-selected', isActive ? 'true' : 'false'); 94 link.setAttribute('tabindex', isActive ? '0' : '-1'); 95 } 96 97 // ARIA: panel 98 tab.setAttribute('aria-hidden', isActive ? 'false' : 'true'); 99 }); 100 } 101 102 render() { 103 104 if (this.getAttribute('static') == 'true') { 105 return; 106 } 107 108 this.tabs = []; 109 110 this.nav.innerHTML = ''; 111 112 let item, counter = 0; 113 114 for (let i = 0; i < this.children.length; i++) { 115 116 if (this.children[i].tagName.toLowerCase() == 'tab') { 117 118 let tabIndex = this.tabs.length; 119 let tabId = `kiss-tab-${Date.now()}-${counter}`; 120 let panelId = `kiss-tabpanel-${Date.now()}-${counter}`; 121 counter++; 122 123 item = document.createElement("li"); 124 item.setAttribute('role', 'presentation'); 125 item.innerHTML = `<a class="kiss-tabs-nav-link" role="tab" id="${tabId}" aria-controls="${panelId}" index="${tabIndex}" tabindex="-1">${this.children[i].getAttribute('caption') || 'Tab'}</a>`; 126 this.nav.append(item); 127 128 this.tabs.push(this.children[i]); 129 130 // ARIA: panel 131 this.children[i].setAttribute('role', 'tabpanel'); 132 this.children[i].id = panelId; 133 this.children[i].setAttribute('aria-labelledby', tabId); 134 this.children[i].setAttribute('active', 'false'); 135 this.children[i].setAttribute('aria-hidden', 'true'); 136 item.setAttribute('active', 'false'); 137 } 138 } 139 140 this.setIndex(this.activeIndex); 141 } 142});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.