Skip to content

Commit 1ff9a14

Browse files
TrueNorth49claude
andauthored
[MC-460-A] fix(frontend): scroll active concept into view in Compare/Tags too (#650)
The sidebar auto-scroll effect only depended on selectedRealizationKey. Annotate navigates by realization (which sets that key), but Compare/Tags navigate by concept via goToConceptOffset, which clears selectedRealizationKey and only updates conceptId. The effect bailed on the null key, so the concept menu never followed the selection outside Annotate. Fall back to the active concept's parent row ([data-testid=concept-parent-button-N]) when no realization is selected, and add conceptId to the dependency array. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 08d9f27 commit 1ff9a14

2 files changed

Lines changed: 52 additions & 6 deletions

File tree

src/ParseUI.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4225,6 +4225,47 @@ describe("ParseUI", () => {
42254225
await waitFor(() => expect(mockSetActiveConcept).toHaveBeenCalledWith("2"));
42264226
});
42274227

4228+
it("scrolls the active concept row into view when navigating concepts in Compare mode", async () => {
4229+
// Compare/Tags navigation clears selectedRealizationKey, so the sidebar
4230+
// auto-scroll must fall back to the active concept's parent row — otherwise
4231+
// it would only ever fire on Annotate (regression: MC-460).
4232+
window.localStorage.setItem("parse.currentMode", "compare");
4233+
mockConfig = {
4234+
...mockConfig!,
4235+
concepts: [
4236+
{ id: "1", label: "water" },
4237+
{ id: "2", label: "hair" },
4238+
{ id: "3", label: "fire" },
4239+
],
4240+
};
4241+
4242+
const originalScrollIntoView = Element.prototype.scrollIntoView;
4243+
const scrollSpy = vi.fn();
4244+
Element.prototype.scrollIntoView = scrollSpy;
4245+
try {
4246+
render(<ParseUI />);
4247+
4248+
const sidebar = await screen.findByTestId("concept-sidebar");
4249+
fireEvent.click(within(sidebar).getByRole("button", { name: /hair/i }));
4250+
expect(screen.getByPlaceholderText(/Ask PARSE AI about hair/i)).toBeTruthy();
4251+
scrollSpy.mockClear();
4252+
4253+
// No realization is selected in Compare mode, yet ArrowDown to "fire"
4254+
// should still scroll the concept-parent-button for the active concept.
4255+
fireEvent.keyDown(window, { key: "ArrowDown" });
4256+
expect(await screen.findByPlaceholderText(/Ask PARSE AI about fire/i)).toBeTruthy();
4257+
4258+
await waitFor(() => {
4259+
const scrolledActiveRow = scrollSpy.mock.instances.some(
4260+
(instance) => (instance as HTMLElement)?.getAttribute?.("data-testid") === "concept-parent-button-3",
4261+
);
4262+
expect(scrolledActiveRow).toBe(true);
4263+
});
4264+
} finally {
4265+
Element.prototype.scrollIntoView = originalScrollIntoView;
4266+
}
4267+
});
4268+
42284269
it("persists compare notes per concept via localStorage without requiring a blur", () => {
42294270
const { unmount } = render(<ParseUI />);
42304271
const notesField = screen.getByPlaceholderText(/Add observations, etymological notes, or questions for review/i);

src/ParseUI.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,13 +1809,18 @@ export function ParseUI() {
18091809
}, [currentMode, goToConceptOffset, goToRealizationOffset, selectedRealizationKey, dispatchSelectedSidebarDelete]);
18101810

18111811
useEffect(() => {
1812-
if (!selectedRealizationKey) return;
1813-
const activeChip = Array.from(document.querySelectorAll<HTMLElement>('[data-realization-key]'))
1814-
.find((element) => element.getAttribute('data-realization-key') === selectedRealizationKey);
1815-
if (typeof activeChip?.scrollIntoView === 'function') {
1816-
activeChip.scrollIntoView({ block: 'nearest' });
1812+
// Keep the active sidebar entry in view. Annotate navigates by realization
1813+
// (selectedRealizationKey); Compare/Tags navigate by concept and clear that
1814+
// key, so fall back to the active concept's parent row there — otherwise the
1815+
// auto-scroll would only ever fire on Annotate.
1816+
const activeElement = selectedRealizationKey
1817+
? Array.from(document.querySelectorAll<HTMLElement>('[data-realization-key]'))
1818+
.find((element) => element.getAttribute('data-realization-key') === selectedRealizationKey)
1819+
: document.querySelector<HTMLElement>(`[data-testid="concept-parent-button-${conceptId}"]`);
1820+
if (typeof activeElement?.scrollIntoView === 'function') {
1821+
activeElement.scrollIntoView({ block: 'nearest' });
18171822
}
1818-
}, [selectedRealizationKey]);
1823+
}, [selectedRealizationKey, conceptId]);
18191824

18201825
const toggleSpeaker = (s: string) => {
18211826
if (currentMode === 'annotate') {

0 commit comments

Comments
 (0)