Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9cb5557
feat(ui): add book counts to status tabs
djdembeck Feb 17, 2026
3225d2e
fix(i18n): use pluralize filter for grammatical correctness in aria-l…
djdembeck Feb 17, 2026
03f8c2c
fix(ui): prevent description text from being cut off
djdembeck Feb 17, 2026
9fa10d8
feat(search): improve filename normalization for better auto results
djdembeck Feb 18, 2026
4d0c1b0
feat(importer): improve template accessibility and CSS
djdembeck Feb 18, 2026
79ab01f
fix(search): update regex patterns for narrator and year removal
djdembeck Feb 18, 2026
2325908
fix(accessibility): add keyboard navigation for book tabs and fix emp…
djdembeck Feb 18, 2026
156c4c3
Merge branch 'develop' into feature/issue-159-search-string-cleanup
djdembeck Feb 18, 2026
01af28b
fix: accessibility and regex improvements
djdembeck Feb 18, 2026
64a1cf1
fix(book_tabs): add defensive null checks and fix HTML attribute quoting
djdembeck Feb 18, 2026
c10cb28
feat(book_tabs): add keyboard navigation for tabs
djdembeck Feb 18, 2026
f8d3fbb
fix(book_tabs): add user-initiated flag to openTab and null-check tabId
djdembeck Mar 2, 2026
ecc29d3
fix(book_tabs): safer event handling and aria-controls fallback
djdembeck Mar 2, 2026
4c1a8e4
fix(book_tabs): normalize tab IDs and fix aria-controls lookup
djdembeck Mar 2, 2026
d8d180b
fix(tabs): validate tab elements before clearing panes and add compre…
djdembeck Mar 2, 2026
71f9724
fix(tabs): normalize tabId and refactor tests to use initializeTabs
djdembeck Mar 2, 2026
ed5607b
fix(book_tabs): guard against blank tab IDs and prevent duplicate key…
djdembeck Mar 2, 2026
6385d30
fix: trim whitespace before stripping hash in tab ID normalization
djdembeck Mar 2, 2026
b1179fb
fix(book_tabs): add doc parameter to openTab and handleKeyDown for te…
djdembeck Mar 2, 2026
0fdc124
fix(book_tabs): prevent duplicate warnings when data-default normaliz…
djdembeck Mar 2, 2026
4daa75c
fix(book_tabs): refactor fallback tab resolution into helper, handle …
djdembeck Mar 2, 2026
82bb534
fix(book_tabs): validate default tab exists and handle empty aria-con…
djdembeck Mar 3, 2026
35760cc
fix(book_tabs): add defensive guard for event.preventDefault in handl…
djdembeck Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions importer/templates/book_tabs.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{% extends "base.html" %}
{% block content %}
<div class="box">
<div class="tabs is-fullwidth is-toggle" style="background: white;" data-default={{ default_view }}>
<div class="tabs is-fullwidth is-toggle" style="background: white;" data-default="{{ default_view }}" role="tablist">
<ul>
<li class="tab is-active" id="done-tab" onclick="openTab('done')"><a aria-label="Done - {{ done_books|length }} book{{ done_books|length|pluralize }}">Done ({{ done_books|length }})</a></li>
<li class="tab" id="processing-tab" onclick="openTab('processing')"><a aria-label="Processing - {{ processing_books|length }} book{{ processing_books|length|pluralize }}">Processing ({{ processing_books|length }})</a></li>
<li class="tab" id="error-tab" onclick="openTab('error')"><a aria-label="Error - {{ error_books|length }} book{{ error_books|length|pluralize }}">Error ({{ error_books|length }})</a></li>
<li class="tab is-active" id="done-tab" onclick="openTab(event, 'done')"><a id="done-tab-anchor" role="tab" aria-selected="true" aria-controls="done" aria-label="Done - {{ done_books|length }} book{{ done_books|length|pluralize }}" href="#" tabindex="0">Done ({{ done_books|length }})</a></li>
<li class="tab" id="processing-tab" onclick="openTab(event, 'processing')"><a id="processing-tab-anchor" role="tab" aria-selected="false" aria-controls="processing" aria-label="Processing - {{ processing_books|length }} book{{ processing_books|length|pluralize }}" href="#" tabindex="-1">Processing ({{ processing_books|length }})</a></li>
<li class="tab" id="error-tab" onclick="openTab(event, 'error')"><a id="error-tab-anchor" role="tab" aria-selected="false" aria-controls="error" aria-label="Error - {{ error_books|length }} book{{ error_books|length|pluralize }}" href="#" tabindex="-1">Error ({{ error_books|length }})</a></li>
</ul>
</div>

<div class="tab-content">
<div class="tab-pane" id="done">
<div class="tab-pane" id="done" role="tabpanel" aria-labelledby="done-tab-anchor">
{% include "book_list.html" with books=done_books %}
</div>
<div class="tab-pane" id="processing" style="display: none;">
<div class="tab-pane" id="processing" role="tabpanel" aria-labelledby="processing-tab-anchor" style="display: none;">
{% include "book_list.html" with books=processing_books %}
</div>
<div class="tab-pane" id="error" style="display: none;">
<div class="tab-pane" id="error" role="tabpanel" aria-labelledby="error-tab-anchor" style="display: none;">
{% include "book_list.html" with books=error_books %}
</div>
</div>
Expand Down
148 changes: 139 additions & 9 deletions importer/templates/book_tabs.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,149 @@
function openTab(tabId) {
const tabLinks = document.querySelectorAll(".tab");
function openTab(event, tabId, userInitiated = false, doc = document) {
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}

const normalizedId = tabId && tabId.toString().trim().replace(/^#/, '');

if (!normalizedId) {
console.warn(`Tab ID is blank or whitespace-only: '${tabId}'`);
return;
}

const tabElem = doc.getElementById(normalizedId);
const tabButton = doc.getElementById(`${normalizedId}-tab`);
if (!tabElem) {
console.warn(`Tab element with id '${normalizedId}' not found`);
return;
}
if (!tabButton) {
console.warn(`Tab button with id '${normalizedId}-tab' not found`);
return;
}

const tabLinks = doc.querySelectorAll(".tab");
tabLinks.forEach(tab => {
tab.classList.remove("is-active");
});

const tabPanes = document.querySelectorAll(".tab-pane");
const tabPanes = doc.querySelectorAll(".tab-pane");
tabPanes.forEach(pane => {
pane.style.display = "none";
});

document.getElementById(tabId).style.display = "block";
document.getElementById(`${tabId}-tab`).classList.add("is-active");
tabElem.style.display = "block";
tabButton.classList.add("is-active");

const tabAnchors = doc.querySelectorAll('.tab a[role="tab"]');
tabAnchors.forEach(anchor => {
const anchorControls = anchor.getAttribute('aria-controls');
const normalizedAnchorControls = anchorControls && anchorControls.trim().replace(/^#/, '');
if (normalizedAnchorControls === normalizedId) {
anchor.setAttribute('aria-selected', 'true');
anchor.setAttribute('tabindex', '0');
if (userInitiated) {
anchor.focus();
}
} else {
anchor.setAttribute('aria-selected', 'false');
anchor.setAttribute('tabindex', '-1');
}
});
}

function handleKeyDown(event, doc = document) {
const tabAnchors = Array.from(doc.querySelectorAll('.tab a[role="tab"]'));
const currentIndex = tabAnchors.indexOf(doc.activeElement);

if (currentIndex === -1 || tabAnchors.length === 0) return;

let nextIndex = currentIndex;

switch (event.key) {
case 'ArrowLeft':
nextIndex = currentIndex > 0 ? currentIndex - 1 : tabAnchors.length - 1;
break;
case 'ArrowRight':
nextIndex = currentIndex < tabAnchors.length - 1 ? currentIndex + 1 : 0;
break;
case 'Home':
nextIndex = 0;
break;
case 'End':
nextIndex = tabAnchors.length - 1;
break;
default:
return;
}

event.preventDefault();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const nextTab = tabAnchors[nextIndex];
const tabIdRaw = nextTab.getAttribute('aria-controls');
if (tabIdRaw) {
const tabId = tabIdRaw.trim().replace(/^#/, "");
openTab({ preventDefault: () => {} }, tabId, true, doc);
} else {
console.warn(`Tab anchor at index ${nextIndex} missing aria-controls attribute`);
}
}

function initializeTabs(doc = document) {
const tabsContainer = doc.querySelector(".tabs");
let defaultTab;

if (tabsContainer && tabsContainer.dataset.default) {
defaultTab = tabsContainer.dataset.default.trim().replace(/^#/, "");
// Guard against blank or whitespace-only values after normalization
if (!defaultTab) {
console.warn("data-default normalized to empty, using fallback tab");
}
}
if (!defaultTab) {
if (!tabsContainer) {
console.warn(".tabs container not found, using fallback tab");
} else {
console.warn("data-default attribute missing or empty, using fallback tab");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const firstTabButton = doc.querySelector(".tab");
if (firstTabButton) {
let ariaControls = firstTabButton.getAttribute("aria-controls");
if (!ariaControls) {
const anchorWithControls = firstTabButton.querySelector('[aria-controls]');
if (anchorWithControls) {
ariaControls = anchorWithControls.getAttribute("aria-controls");
}
}
if (ariaControls) {
defaultTab = ariaControls.trim().replace(/^#/, "");
} else {
const parsed = firstTabButton.id.replace("-tab", "");
defaultTab = parsed || "done";
}
} else {
defaultTab = "done";
}
}
openTab({ preventDefault: () => {} }, defaultTab, false, doc);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const tabAnchors = doc.querySelectorAll('.tab a[role="tab"]');
tabAnchors.forEach(anchor => {
if (!anchor.dataset.keydownBound) {
anchor.addEventListener('keydown', (e) => handleKeyDown(e, doc));
anchor.dataset.keydownBound = 'true';
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return defaultTab;
}

window.addEventListener('load', function () {
const defaultTab = document.querySelector(".tabs").dataset.default
openTab(defaultTab);
});
// Browser-only initialization (skipped during Node.js testing)
if (typeof window !== 'undefined') {
window.addEventListener('load', function () {
initializeTabs(document);
});
}

// Export for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = { openTab, handleKeyDown, initializeTabs };
}
Loading