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.
This commit is contained in:
2025-08-10 10:46:54 +12:00
parent abe4126e78
commit 5c7d831ebf
3 changed files with 133 additions and 46 deletions
+1
View File
@@ -83,3 +83,4 @@ typings/
.vscode/ .vscode/
.idea/ .idea/
site-history.json site-history.json
site-history.json
+60 -14
View File
@@ -486,35 +486,80 @@ function closeTab(id) {
// 2) streamline renderTabs with a fragment // 2) streamline renderTabs with a fragment
function renderTabs() { function renderTabs() {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
// ensure tablist role present
if (tabBarEl && tabBarEl.getAttribute('role') !== 'tablist') {
tabBarEl.setAttribute('role', 'tablist');
}
tabs.forEach(tab => { tabs.forEach(tab => {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'tab' + (tab.id === activeTabId ? ' active' : ''); 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) { if (tab.favicon) {
const icon = document.createElement('img'); const icon = document.createElement('img');
icon.src = tab.favicon; icon.src = tab.favicon;
icon.style.width = '16px'; icon.className = 'tab-favicon';
icon.style.height = '16px';
icon.style.marginRight = '6px';
el.appendChild(icon); 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'); const closeBtn = document.createElement('button');
closeBtn.className = 'tab-close';
closeBtn.title = 'Close tab';
closeBtn.textContent = '×'; closeBtn.textContent = '×';
closeBtn.onclick = (e) => { closeBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
closeTab(tab.id); 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.draggable = true;
el.addEventListener('dragstart', e => { el.addEventListener('dragstart', e => {
e.dataTransfer.setData('tabId', tab.id); 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 => { el.addEventListener('dragend', e => {
if ( if (
e.clientX < 0 || e.clientX > window.innerWidth || e.clientX < 0 || e.clientX > window.innerWidth ||
@@ -525,15 +570,16 @@ function renderTabs() {
} }
}); });
el.onclick = () => setActiveTab(tab.id); el.addEventListener('click', () => setActiveTab(tab.id));
el.appendChild(closeBtn);
frag.appendChild(el); frag.appendChild(el);
}); });
// add the “+” at the end // Dedicated new-tab button at end
const plus = document.createElement('div'); const plus = document.createElement('button');
plus.className = 'tab'; plus.className = 'new-tab-button';
plus.title = 'New tab';
plus.setAttribute('aria-label', 'New tab');
plus.textContent = '+'; plus.textContent = '+';
plus.onclick = () => createTab(); plus.addEventListener('click', () => createTab());
frag.appendChild(plus); frag.appendChild(plus);
tabBarEl.innerHTML = ''; // clear once tabBarEl.innerHTML = ''; // clear once
+72 -32
View File
@@ -7,23 +7,19 @@ html, body {
font-family: 'Segoe UI', sans-serif; font-family: 'Segoe UI', sans-serif;
} }
/* TAB STRIP */
#tab-bar { #tab-bar {
display: flex; display: flex;
padding-left: 80px; /* leave room for macOS traffic lights */ align-items: flex-end;
overflow-x: auto; /* allow scrolling when many tabs */ gap: 2px;
/* custom scrollbar styling */ 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-color: #444 #2a2a3c; /* thumb and track for Firefox */
scrollbar-width: thin; /* slimmer track */ scrollbar-width: thin; /* slimmer track */
} }
#tab-bar > * {
flex: 1 1 0;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* NAVBAR LAYOUT */ /* NAVBAR LAYOUT */
#nav { #nav {
display: flex; display: flex;
@@ -175,45 +171,89 @@ html, body {
/* TABS */ /* TABS */
.tab { .tab {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
padding: 6px 8px; padding: 4px 8px; /* slimmer padding */
margin: 4px 4px 0 0; margin: 0;
background: #222; height: 28px; /* reduce overall tab height */
border-radius: 8px 8px 0 0; 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; cursor: pointer;
transition: background 0.2s, flex 0.2s; user-select: none;
min-width: 80px; /* prevent tabs from getting too small */ 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 { .tab:hover {
background: #333; background: linear-gradient(180deg, #2a2c33 0%, #23252b 100%);
} }
.tab.active { .tab.active {
background: #444; color: #fff;
font-weight: bold; background: linear-gradient(180deg, #2f323a 0%, #2a2c33 100%);
flex: 3 1 0; /* increased grow factor for larger active tab */ box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 0 0 1px #3a3d46 inset;
min-width: 120px; /* larger min width for the active tab */
} }
.tab img { .tab .tab-favicon {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-right: 6px;
border-radius: 2px; border-radius: 2px;
flex: 0 0 auto;
} }
.tab button { .tab .tab-title {
margin-left: 8px; flex: 1 1 auto;
background: none; overflow: hidden;
border: none; white-space: nowrap;
color: #f55; text-overflow: ellipsis;
font-weight: bold; font-size: 12px;
cursor: pointer;
} }
.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 */
.zoom-controls { .zoom-controls {
display: flex; display: flex;