Skip to content

Commit 82f455c

Browse files
pftgclaude
andcommitted
fix: sidebar search filter now syncs with navigation and counter
Arrow keys, nav buttons, and the counter now operate on the filtered list instead of raw DATA indices. When searching, state.current jumps to the first visible match and navigation stays within filtered items. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b332495 commit 82f455c

1 file changed

Lines changed: 48 additions & 11 deletions

File tree

lib/capybara_screenshot_diff/reporters/templates/report.html.erb

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -735,12 +735,26 @@
735735
var opSlider = $('opacity-slider'), opVal = $('opacity-val');
736736
var swContainer = $('swipe-container'), swHandle = $('swipe-handle'), swClip = $('swipe-clip-box');
737737

738+
/* ── Filtered index list (kept in sync with search) ──────────────── */
739+
var filtered = [];
740+
741+
function rebuildFiltered() {
742+
var q = state.search.toLowerCase();
743+
filtered = [];
744+
DATA.forEach(function(item, idx) {
745+
if (!q || item.name.toLowerCase().indexOf(q) !== -1) filtered.push(idx);
746+
});
747+
/* If current item is no longer visible, jump to first match */
748+
if (filtered.length && filtered.indexOf(state.current) === -1) {
749+
state.current = filtered[0];
750+
}
751+
}
752+
738753
/* ── Sidebar ────────────────────────────────────────────────────────── */
739754
function buildSidebar() {
740755
sidebarList.innerHTML = '';
741-
var q = state.search.toLowerCase();
742-
DATA.forEach(function(item, idx) {
743-
if (q && item.name.toLowerCase().indexOf(q) === -1) return;
756+
filtered.forEach(function(idx) {
757+
var item = DATA[idx];
744758
var btn = document.createElement('button');
745759
btn.className = 'thumb-btn' + (idx === state.current ? ' active' : '');
746760
btn.setAttribute('title', item.name);
@@ -764,22 +778,39 @@
764778

765779
/* ── Selection ──────────────────────────────────────────────────────── */
766780
function selectItem(idx) {
767-
if (!DATA.length) return;
768-
idx = Math.max(0, Math.min(idx, DATA.length - 1));
781+
if (!filtered.length) return;
782+
/* Clamp to filtered range */
783+
var pos = filtered.indexOf(idx);
784+
if (pos === -1) {
785+
/* idx not in filtered list — find nearest */
786+
pos = Math.max(0, Math.min(idx, filtered.length - 1));
787+
idx = filtered[pos];
788+
}
769789
state.current = idx;
770790
renderMain();
771791
buildSidebar();
772792
var active = sidebarList.querySelector('.thumb-btn.active');
773793
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
774794
}
775795

796+
/* Navigate to next/prev within filtered list */
797+
function selectNext() {
798+
var pos = filtered.indexOf(state.current);
799+
if (pos < filtered.length - 1) selectItem(filtered[pos + 1]);
800+
}
801+
function selectPrev() {
802+
var pos = filtered.indexOf(state.current);
803+
if (pos > 0) selectItem(filtered[pos - 1]);
804+
}
805+
776806
/* ── Render ─────────────────────────────────────────────────────────── */
777807
function renderMain() {
778808
var item = DATA[state.current];
779809
if (!item) return;
780810

781811
topTitle.textContent = item.name;
782-
navCounter.textContent = (state.current + 1) + '/' + DATA.length;
812+
var pos = filtered.indexOf(state.current);
813+
navCounter.textContent = (pos + 1) + '/' + filtered.length;
783814
var hasDiff = item.diff || item.heatmap;
784815
topBadge.textContent = hasDiff ? 'changed' : 'pass';
785816
topBadge.className = 'diff-badge ' + (hasDiff ? 'diff-badge-fail' : 'diff-badge-pass');
@@ -822,8 +853,8 @@
822853
});
823854

824855
/* ── Nav buttons ───────────────────────────────────────────────────── */
825-
$('nav-prev').addEventListener('click', function() { selectItem(state.current - 1); });
826-
$('nav-next').addEventListener('click', function() { selectItem(state.current + 1); });
856+
$('nav-prev').addEventListener('click', selectPrev);
857+
$('nav-next').addEventListener('click', selectNext);
827858

828859
/* ── Overlay slider ────────────────────────────────────────────────── */
829860
opSlider.addEventListener('input', function() {
@@ -862,7 +893,12 @@
862893
document.addEventListener('touchend', function() { dragging = false; });
863894

864895
/* ── Search ────────────────────────────────────────────────────────── */
865-
searchEl.addEventListener('input', function() { state.search = this.value; buildSidebar(); });
896+
searchEl.addEventListener('input', function() {
897+
state.search = this.value;
898+
rebuildFiltered();
899+
buildSidebar();
900+
if (filtered.length) renderMain();
901+
});
866902

867903
/* ── Keyboard ──────────────────────────────────────────────────────── */
868904
document.addEventListener('keydown', function(e) {
@@ -871,8 +907,8 @@
871907
return;
872908
}
873909
switch (e.key) {
874-
case 'ArrowRight': case 'ArrowDown': e.preventDefault(); selectItem(state.current + 1); break;
875-
case 'ArrowLeft': case 'ArrowUp': e.preventDefault(); selectItem(state.current - 1); break;
910+
case 'ArrowRight': case 'ArrowDown': e.preventDefault(); selectNext(); break;
911+
case 'ArrowLeft': case 'ArrowUp': e.preventDefault(); selectPrev(); break;
876912
case '1': setMode('sidebyside'); break;
877913
case '2': setMode('overlay'); break;
878914
case '3': setMode('swipe'); break;
@@ -888,6 +924,7 @@
888924
topBadge.style.display = 'none';
889925
Object.keys(views).forEach(function(m) { views[m].style.display = 'none'; });
890926
} else {
927+
rebuildFiltered();
891928
buildSidebar();
892929
renderMain();
893930
}

0 commit comments

Comments
 (0)