From 423885cc8ef05cbd2b015808f2afbe205380bd24 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 1 Jul 2026 17:46:06 -0400 Subject: [PATCH 1/2] Fix non-first morpheme form cell stealing focus to gloss input Clicking a span (non-first) form cell in MorphemeBox bubbled its mousedown up to TokenChip's label handler, which only recognizes input/button targets and so treated the span as unhandled, focusing the gloss input before the popover opened. --- src/components/MorphemeBox.tsx | 5 ++++- src/components/TokenChip.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/MorphemeBox.tsx b/src/components/MorphemeBox.tsx index 059d107c..789209b6 100644 --- a/src/components/MorphemeBox.tsx +++ b/src/components/MorphemeBox.tsx @@ -118,8 +118,11 @@ export function MorphemeBox({ key={m.id} aria-hidden="true" className={formClassName} - style={formStyle} onClick={handleClick} + // Not a button, so stop mousedown from bubbling to TokenChip's label handler (which + // would otherwise focus the gloss input). + onMouseDown={(e) => e.stopPropagation()} + style={formStyle} > {m.form} diff --git a/src/components/TokenChip.tsx b/src/components/TokenChip.tsx index abea5929..0ec99ad8 100644 --- a/src/components/TokenChip.tsx +++ b/src/components/TokenChip.tsx @@ -167,8 +167,10 @@ export function TokenChip({ * The input is looked up by id rather than `querySelector('input')` because the morpheme gloss * inputs precede it inside the label when morphology is shown. A mouse-down on any input is left * to that input's own handling ({@link handleMouseDown} for the gloss input, which bubbles here - * after already handling it); a mouse-down on a morpheme form cell or the unanalyzed "define" - * trigger (all `button`s) is left to that button's own click handler, which opens the popover. + * after already handling it); a mouse-down on the first morpheme form cell or the unanalyzed + * "define" trigger (both real `button`s) is left to that button's own click handler. The + * remaining morpheme form cells are `span`s that stop their own mousedown from bubbling here + * instead (see {@link MorphemeBox}). * * @param e - The label's mouse-down event. */ From eb80eba0da7273eafc8037ddf5adcdbd6f6a58b7 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 1 Jul 2026 17:46:15 -0400 Subject: [PATCH 2/2] Add MorphemeBox coverage for non-first form cell clicks Prior tests only exercised the first (button) form cell; the span cells' click and mousedown-bubbling behavior had no coverage. --- src/__tests__/components/MorphemeBox.test.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/__tests__/components/MorphemeBox.test.tsx b/src/__tests__/components/MorphemeBox.test.tsx index 68ea8ac9..f108d819 100644 --- a/src/__tests__/components/MorphemeBox.test.tsx +++ b/src/__tests__/components/MorphemeBox.test.tsx @@ -3,7 +3,7 @@ /// import { useLocalizedStrings } from '@papi/frontend/react'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { MorphemeAnalysis, Token } from 'interlinearizer'; import * as AnalysisStore from '../../components/AnalysisStore'; @@ -118,6 +118,36 @@ describe('MorphemeBox', () => { expect(onEditBreakdown).toHaveBeenCalledTimes(1); }); + it('calls onEditBreakdown when a non-first form cell is clicked', async () => { + // Non-first cells are spans, not buttons, so they need their own coverage. + const onEditBreakdown = jest.fn(); + renderBox({ onEditBreakdown }); + await userEvent.click(screen.getByText('-lo')); + expect(onEditBreakdown).toHaveBeenCalledTimes(1); + }); + + it('stops a non-first form cell mousedown from bubbling to an ancestor handler', () => { + // Regression test: an ancestor onMouseDown (e.g. TokenChip's label) would otherwise focus the + // gloss input, since a span (unlike a button) doesn't match its input/button guard. + const onAncestorMouseDown = jest.fn(); + render( + // Stand-in for an ancestor React handler, not real UI. + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ +
, + ); + fireEvent.mouseDown(screen.getByText('-lo')); + expect(onAncestorMouseDown).not.toHaveBeenCalled(); + }); + it('does not call onEditBreakdown when disabled', async () => { const onEditBreakdown = jest.fn(); renderBox({ disabled: true, onEditBreakdown });