Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions src/ParseUI.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ParseUI />);

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(<ParseUI />);
const notesField = screen.getByPlaceholderText(/Add observations, etymological notes, or questions for review/i);
Expand Down
17 changes: 11 additions & 6 deletions src/ParseUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1808,13 +1808,18 @@ export function ParseUI() {
}, [currentMode, goToConceptOffset, goToRealizationOffset, selectedRealizationKey, dispatchSelectedSidebarDelete]);

useEffect(() => {
if (!selectedRealizationKey) return;
const activeChip = Array.from(document.querySelectorAll<HTMLElement>('[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<HTMLElement>('[data-realization-key]'))
.find((element) => element.getAttribute('data-realization-key') === selectedRealizationKey)
: document.querySelector<HTMLElement>(`[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') {
Expand Down
Loading