From 5c7d831ebf7b11146d1eef3520213b3683f4c0d9 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sun, 10 Aug 2025 10:46:54 +1200 Subject: [PATCH] Improve tab strip accessibility and UI Enhances the tab strip with ARIA roles, keyboard navigation, and improved drag-and-drop reordering. Refactors tab rendering for better accessibility, adds dedicated new-tab button, and updates styles for a more modern look and clearer tab controls. --- .gitignore | 1 + renderer/script.js | 74 ++++++++++++++++++++++++++------ renderer/style.css | 104 +++++++++++++++++++++++++++++++-------------- 3 files changed, 133 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index f99083e..e70b61b 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ typings/ .vscode/ .idea/ site-history.json +site-history.json diff --git a/renderer/script.js b/renderer/script.js index 91935c4..038b5cd 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -486,35 +486,80 @@ function closeTab(id) { // 2) streamline renderTabs with a fragment function renderTabs() { const frag = document.createDocumentFragment(); + // ensure tablist role present + if (tabBarEl && tabBarEl.getAttribute('role') !== 'tablist') { + tabBarEl.setAttribute('role', 'tablist'); + } tabs.forEach(tab => { const el = document.createElement('div'); el.className = 'tab' + (tab.id === activeTabId ? ' active' : ''); + el.setAttribute('role', 'tab'); + el.setAttribute('aria-selected', String(tab.id === activeTabId)); + el.setAttribute('tabindex', tab.id === activeTabId ? '0' : '-1'); + // favicon if (tab.favicon) { const icon = document.createElement('img'); icon.src = tab.favicon; - icon.style.width = '16px'; - icon.style.height = '16px'; - icon.style.marginRight = '6px'; + icon.className = 'tab-favicon'; el.appendChild(icon); } - el.appendChild(document.createTextNode(getTabLabel(tab))); + // title + const title = document.createElement('span'); + title.className = 'tab-title'; + title.textContent = getTabLabel(tab); + el.appendChild(title); + // close const closeBtn = document.createElement('button'); + closeBtn.className = 'tab-close'; + closeBtn.title = 'Close tab'; closeBtn.textContent = '×'; - closeBtn.onclick = (e) => { + closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeTab(tab.id); - }; + }); + el.appendChild(closeBtn); - // 2a) make tab draggable + // middle-click to close + el.addEventListener('mousedown', (e) => { + if (e.button === 1) { // middle + e.preventDefault(); + closeTab(tab.id); + } + }); + + // make tab draggable el.draggable = true; el.addEventListener('dragstart', e => { e.dataTransfer.setData('tabId', tab.id); + // for Firefox compatibility + e.dataTransfer.setData('text/plain', tab.id); }); - // 2b) on dragend outside window, open in new window and close here + // allow drop reordering + el.addEventListener('dragover', e => { + e.preventDefault(); + }); + el.addEventListener('drop', e => { + e.preventDefault(); + const draggedId = e.dataTransfer.getData('tabId') || e.dataTransfer.getData('text/plain'); + if (!draggedId || draggedId === tab.id) return; + const fromIndex = tabs.findIndex(t => t.id === draggedId); + const toIndex = tabs.findIndex(t => t.id === tab.id); + if (fromIndex === -1 || toIndex === -1) return; + // determine insert position: before or after depending on cursor + const rect = el.getBoundingClientRect(); + const after = (e.clientX - rect.left) > rect.width / 2; + const newIndex = toIndex + (after ? 1 : 0); + const [moved] = tabs.splice(fromIndex, 1); + const adjIndex = fromIndex < newIndex ? newIndex - 1 : newIndex; + tabs.splice(adjIndex, 0, moved); + scheduleRenderTabs(); + }); + + // tear off to new window if drag ends outside window el.addEventListener('dragend', e => { if ( e.clientX < 0 || e.clientX > window.innerWidth || @@ -525,15 +570,16 @@ function renderTabs() { } }); - el.onclick = () => setActiveTab(tab.id); - el.appendChild(closeBtn); + el.addEventListener('click', () => setActiveTab(tab.id)); frag.appendChild(el); }); - // add the “+” at the end - const plus = document.createElement('div'); - plus.className = 'tab'; + // Dedicated new-tab button at end + const plus = document.createElement('button'); + plus.className = 'new-tab-button'; + plus.title = 'New tab'; + plus.setAttribute('aria-label', 'New tab'); plus.textContent = '+'; - plus.onclick = () => createTab(); + plus.addEventListener('click', () => createTab()); frag.appendChild(plus); tabBarEl.innerHTML = ''; // clear once diff --git a/renderer/style.css b/renderer/style.css index e055104..f47308b 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -7,23 +7,19 @@ html, body { font-family: 'Segoe UI', sans-serif; } +/* TAB STRIP */ #tab-bar { display: flex; - padding-left: 80px; /* leave room for macOS traffic lights */ - overflow-x: auto; /* allow scrolling when many tabs */ - /* custom scrollbar styling */ + align-items: flex-end; + gap: 2px; + padding: 2px 6px 0 6px; /* slimmer top padding */ + background: #1b1c20; /* strip background like modern browsers */ + border-bottom: 1px solid #2a2c33; /* hairline under tabs */ + overflow-x: auto; /* scroll when many tabs */ scrollbar-color: #444 #2a2a3c; /* thumb and track for Firefox */ scrollbar-width: thin; /* slimmer track */ } -#tab-bar > * { - flex: 1 1 0; - text-align: center; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - /* NAVBAR LAYOUT */ #nav { display: flex; @@ -175,45 +171,89 @@ html, body { /* TABS */ .tab { + position: relative; display: flex; align-items: center; - gap: 6px; - padding: 6px 8px; - margin: 4px 4px 0 0; - background: #222; - border-radius: 8px 8px 0 0; + gap: 8px; + padding: 4px 8px; /* slimmer padding */ + margin: 0; + height: 28px; /* reduce overall tab height */ + color: #ddd; + background: linear-gradient(180deg, #24262b 0%, #1f2025 100%); + border: 1px solid #2b2d34; + border-bottom: none; /* let it visually merge with the strip line */ + border-radius: 6px 6px 0 0; /* slightly tighter radius */ cursor: pointer; - transition: background 0.2s, flex 0.2s; - min-width: 80px; /* prevent tabs from getting too small */ + user-select: none; + max-width: 260px; + min-width: 120px; + flex: 0 1 180px; /* like Chrome: shrink when crowded */ + overflow: hidden; + transition: background .15s ease, color .15s ease, box-shadow .15s ease; } .tab:hover { - background: #333; + background: linear-gradient(180deg, #2a2c33 0%, #23252b 100%); } .tab.active { - background: #444; - font-weight: bold; - flex: 3 1 0; /* increased grow factor for larger active tab */ - min-width: 120px; /* larger min width for the active tab */ + color: #fff; + background: linear-gradient(180deg, #2f323a 0%, #2a2c33 100%); + box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 0 0 1px #3a3d46 inset; } -.tab img { +.tab .tab-favicon { width: 16px; height: 16px; - margin-right: 6px; border-radius: 2px; + flex: 0 0 auto; } -.tab button { - margin-left: 8px; - background: none; - border: none; - color: #f55; - font-weight: bold; - cursor: pointer; +.tab .tab-title { + flex: 1 1 auto; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; } +.tab .tab-close { + flex: 0 0 auto; + width: 22px; + height: 22px; + display: grid; + place-items: center; + border: none; + border-radius: 11px; + background: transparent; + color: #b5b5b5; + opacity: 0; /* hidden by default */ + transition: background .15s ease, color .15s ease, opacity .15s ease; +} + +.tab:hover .tab-close, +.tab.active .tab-close { opacity: 1; } +.tab .tab-close:hover { background: #3b3e47; color: #fff; } +.tab .tab-close:active { background: #2e3139; } + +/* New tab (+) button aligned to the right end of the strip */ +.new-tab-button { + margin-left: 6px; + flex: 0 0 auto; + width: 24px; /* tighter button */ + height: 24px; + display: grid; + place-items: center; + border-radius: 12px; + border: 1px solid #2b2d34; + background: #23252b; + color: #d5d5d5; + cursor: pointer; + transition: background .15s ease, color .15s ease, border-color .15s ease; +} +.new-tab-button:hover { background: #2b2d34; color: #fff; border-color: #3a3d46; } +.new-tab-button:active { background: #262830; } + /* ZOOM CONTROLS */ .zoom-controls { display: flex;