Skip to content

Commit 677b7b0

Browse files
authored
Fix gloss-input focus steal on non-first morpheme form cell click (#135)
Also merge in `main` and fill test coverage.
1 parent 95387d8 commit 677b7b0

3 files changed

Lines changed: 39 additions & 4 deletions

File tree

src/__tests__/components/MorphemeBox.test.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// <reference types="@testing-library/jest-dom" />
44

55
import { useLocalizedStrings } from '@papi/frontend/react';
6-
import { render, screen } from '@testing-library/react';
6+
import { fireEvent, render, screen } from '@testing-library/react';
77
import userEvent from '@testing-library/user-event';
88
import type { MorphemeAnalysis, Token } from 'interlinearizer';
99
import * as AnalysisStore from '../../components/AnalysisStore';
@@ -118,6 +118,36 @@ describe('MorphemeBox', () => {
118118
expect(onEditBreakdown).toHaveBeenCalledTimes(1);
119119
});
120120

121+
it('calls onEditBreakdown when a non-first form cell is clicked', async () => {
122+
// Non-first cells are spans, not buttons, so they need their own coverage.
123+
const onEditBreakdown = jest.fn();
124+
renderBox({ onEditBreakdown });
125+
await userEvent.click(screen.getByText('-lo'));
126+
expect(onEditBreakdown).toHaveBeenCalledTimes(1);
127+
});
128+
129+
it('stops a non-first form cell mousedown from bubbling to an ancestor handler', () => {
130+
// Regression test: an ancestor onMouseDown (e.g. TokenChip's label) would otherwise focus the
131+
// gloss input, since a span (unlike a button) doesn't match its input/button guard.
132+
const onAncestorMouseDown = jest.fn();
133+
render(
134+
// Stand-in for an ancestor React handler, not real UI.
135+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
136+
<div onMouseDown={onAncestorMouseDown}>
137+
<MorphemeBox
138+
analysisLanguage="en"
139+
disabled={false}
140+
morphemes={MORPHEMES}
141+
onEditBreakdown={jest.fn()}
142+
popoverOpen={false}
143+
token={WORD_TOKEN}
144+
/>
145+
</div>,
146+
);
147+
fireEvent.mouseDown(screen.getByText('-lo'));
148+
expect(onAncestorMouseDown).not.toHaveBeenCalled();
149+
});
150+
121151
it('does not call onEditBreakdown when disabled', async () => {
122152
const onEditBreakdown = jest.fn();
123153
renderBox({ disabled: true, onEditBreakdown });

src/components/MorphemeBox.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,11 @@ export function MorphemeBox({
118118
key={m.id}
119119
aria-hidden="true"
120120
className={formClassName}
121-
style={formStyle}
122121
onClick={handleClick}
122+
// Not a button, so stop mousedown from bubbling to TokenChip's label handler (which
123+
// would otherwise focus the gloss input).
124+
onMouseDown={(e) => e.stopPropagation()}
125+
style={formStyle}
123126
>
124127
{m.form}
125128
</span>

src/components/TokenChip.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,10 @@ export function TokenChip({
167167
* The input is looked up by id rather than `querySelector('input')` because the morpheme gloss
168168
* inputs precede it inside the label when morphology is shown. A mouse-down on any input is left
169169
* to that input's own handling ({@link handleMouseDown} for the gloss input, which bubbles here
170-
* after already handling it); a mouse-down on a morpheme form cell or the unanalyzed "define"
171-
* trigger (all `button`s) is left to that button's own click handler, which opens the popover.
170+
* after already handling it); a mouse-down on the first morpheme form cell or the unanalyzed
171+
* "define" trigger (both real `button`s) is left to that button's own click handler. The
172+
* remaining morpheme form cells are `span`s that stop their own mousedown from bubbling here
173+
* instead (see {@link MorphemeBox}).
172174
*
173175
* @param e - The label's mouse-down event.
174176
*/

0 commit comments

Comments
 (0)