diff --git a/src/ParseUI.test.tsx b/src/ParseUI.test.tsx
index 1242f608..e4b309ba 100644
--- a/src/ParseUI.test.tsx
+++ b/src/ParseUI.test.tsx
@@ -4225,6 +4225,47 @@ describe("ParseUI", () => {
await waitFor(() => expect(mockSetActiveConcept).toHaveBeenCalledWith("2"));
});
+ it("scrolls the active concept row into view when navigating concepts in Compare mode", async () => {
+ // Compare/Tags navigation clears selectedRealizationKey, so the sidebar
+ // auto-scroll must fall back to the active concept's parent row — otherwise
+ // it would only ever fire on Annotate (regression: MC-460).
+ window.localStorage.setItem("parse.currentMode", "compare");
+ mockConfig = {
+ ...mockConfig!,
+ concepts: [
+ { id: "1", label: "water" },
+ { id: "2", label: "hair" },
+ { id: "3", label: "fire" },
+ ],
+ };
+
+ const originalScrollIntoView = Element.prototype.scrollIntoView;
+ const scrollSpy = vi.fn();
+ Element.prototype.scrollIntoView = scrollSpy;
+ try {
+ render();
+
+ const sidebar = await screen.findByTestId("concept-sidebar");
+ fireEvent.click(within(sidebar).getByRole("button", { name: /hair/i }));
+ expect(screen.getByPlaceholderText(/Ask PARSE AI about hair/i)).toBeTruthy();
+ scrollSpy.mockClear();
+
+ // No realization is selected in Compare mode, yet ArrowDown to "fire"
+ // should still scroll the concept-parent-button for the active concept.
+ fireEvent.keyDown(window, { key: "ArrowDown" });
+ expect(await screen.findByPlaceholderText(/Ask PARSE AI about fire/i)).toBeTruthy();
+
+ await waitFor(() => {
+ const scrolledActiveRow = scrollSpy.mock.instances.some(
+ (instance) => (instance as HTMLElement)?.getAttribute?.("data-testid") === "concept-parent-button-3",
+ );
+ expect(scrolledActiveRow).toBe(true);
+ });
+ } finally {
+ Element.prototype.scrollIntoView = originalScrollIntoView;
+ }
+ });
+
it("persists compare notes per concept via localStorage without requiring a blur", () => {
const { unmount } = render();
const notesField = screen.getByPlaceholderText(/Add observations, etymological notes, or questions for review/i);
diff --git a/src/ParseUI.tsx b/src/ParseUI.tsx
index 544b48ef..5be61421 100644
--- a/src/ParseUI.tsx
+++ b/src/ParseUI.tsx
@@ -1808,13 +1808,18 @@ export function ParseUI() {
}, [currentMode, goToConceptOffset, goToRealizationOffset, selectedRealizationKey, dispatchSelectedSidebarDelete]);
useEffect(() => {
- if (!selectedRealizationKey) return;
- const activeChip = Array.from(document.querySelectorAll('[data-realization-key]'))
- .find((element) => element.getAttribute('data-realization-key') === selectedRealizationKey);
- if (typeof activeChip?.scrollIntoView === 'function') {
- activeChip.scrollIntoView({ block: 'nearest' });
+ // Keep the active sidebar entry in view. Annotate navigates by realization
+ // (selectedRealizationKey); Compare/Tags navigate by concept and clear that
+ // key, so fall back to the active concept's parent row there — otherwise the
+ // auto-scroll would only ever fire on Annotate.
+ const activeElement = selectedRealizationKey
+ ? Array.from(document.querySelectorAll('[data-realization-key]'))
+ .find((element) => element.getAttribute('data-realization-key') === selectedRealizationKey)
+ : document.querySelector(`[data-testid="concept-parent-button-${conceptId}"]`);
+ if (typeof activeElement?.scrollIntoView === 'function') {
+ activeElement.scrollIntoView({ block: 'nearest' });
}
- }, [selectedRealizationKey]);
+ }, [selectedRealizationKey, conceptId]);
const toggleSpeaker = (s: string) => {
if (currentMode === 'annotate') {