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 });
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.
*/