Skip to content

Commit e9cd471

Browse files
Minor adjustments
1 parent 352637a commit e9cd471

5 files changed

Lines changed: 52 additions & 6 deletions

File tree

REVIEW.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Before reporting any documentation as missing, open the file and confirm the JSD
1818

1919
Keyboard accessibility is planned but not yet implemented. Do not flag missing `tabIndex` attributes, absent `aria-*` roles, or gaps in focus management as issues — these will be addressed in a dedicated pass once the core interaction model is stable.
2020

21+
## Buttons inside the TokenChip label
22+
23+
The `<label>` in [src/components/TokenChip.tsx](src/components/TokenChip.tsx) contains a `<button>` (the morpheme trigger) when morphology is shown, which technically violates the HTML content model for `<label>` (no labelable descendants other than the labeled control). This is **intentional and already handled**: the explicit `htmlFor` binding to the gloss input takes precedence over implicit control resolution in all browsers, and the label's mouse-down handler explicitly routes focus around inputs and buttons. The comment above the label in that file documents the reasoning. Do not flag this as a spec violation or suggest restructuring the markup — moving the morpheme row outside the label would break click-to-focus on the chip body.
24+
2125
## Mock cleanup in tests
2226

2327
[jest.config.ts](jest.config.ts) sets both `resetMocks: true` and `restoreMocks: true`. This means every `jest.spyOn(...)` is automatically restored to its original implementation after each test — tests do **not** need a manual `mockRestore()` or `jest.restoreAllMocks()` in `afterEach` for spies. Do not flag spies as leaking or suggest adding cleanup for them.

__mocks__/platform-bible-react.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,11 @@ export function PopoverAnchor({
365365
* @param props - Component props.
366366
* @param props.children - Panel content.
367367
* @param props.className - CSS class names forwarded to the div.
368-
* @param props.onEscapeKeyDown - Called when Escape is pressed inside the content.
369-
* @param props.onInteractOutside - Called when the sentinel outside button is clicked.
368+
* @param props.onEscapeKeyDown - Called with the native `KeyboardEvent` when Escape is pressed
369+
* inside the content, matching Radix's signature.
370+
* @param props.onInteractOutside - Called with a `CustomEvent` carrying the original pointer event
371+
* in `detail.originalEvent` when the sentinel outside button is clicked, matching the shape of
372+
* Radix's `PointerDownOutsideEvent`.
370373
* @param props.onOpenAutoFocus - Called once on mount with a plain `Event`.
371374
* @param props.onClick - Click handler forwarded to the div.
372375
* @param props.onMouseDown - Mouse-down handler forwarded to the div.
@@ -385,8 +388,8 @@ export function PopoverContent({
385388
className?: string;
386389
align?: 'start' | 'center' | 'end';
387390
sideOffset?: number;
388-
onEscapeKeyDown?: () => void;
389-
onInteractOutside?: () => void;
391+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
392+
onInteractOutside?: (event: CustomEvent) => void;
390393
onOpenAutoFocus?: (event: Event) => void;
391394
onClick?: MouseEventHandler<HTMLDivElement>;
392395
onMouseDown?: MouseEventHandler<HTMLDivElement>;
@@ -403,7 +406,7 @@ export function PopoverContent({
403406
data-testid="popover-content"
404407
onClick={onClick}
405408
onKeyDown={(e) => {
406-
if (e.key === 'Escape') onEscapeKeyDown?.();
409+
if (e.key === 'Escape') onEscapeKeyDown?.(e.nativeEvent);
407410
}}
408411
onMouseDown={onMouseDown}
409412
>
@@ -412,7 +415,13 @@ export function PopoverContent({
412415
<button
413416
data-testid="popover-outside"
414417
type="button"
415-
onClick={() => onInteractOutside()}
418+
onClick={(e) =>
419+
onInteractOutside(
420+
new CustomEvent('dismissableLayer.pointerDownOutside', {
421+
detail: { originalEvent: e.nativeEvent },
422+
}),
423+
)
424+
}
416425
>
417426
outside
418427
</button>

src/__tests__/components/PhraseBox.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ describe('PhraseBox', () => {
336336
if (box instanceof HTMLElement) box.focus();
337337
await userEvent.keyboard('{Enter}');
338338

339+
expect(screen.getByRole('textbox', { name: 'Gloss for Hello' })).toHaveFocus();
339340
expect(screen.getByRole('textbox', { name: 'Gloss for morpheme Hello' })).not.toHaveFocus();
340341
});
341342

src/__tests__/components/TokenChip.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,31 @@ describe('TokenChip', () => {
559559
expect(focusSpy).not.toHaveBeenCalled();
560560
});
561561

562+
it('does not reopen the popover when showMorphology is toggled off and back on', async () => {
563+
const { rerender } = render(
564+
<AnalysisStoreProvider analysisLanguage="und">
565+
<TokenChip {...requiredProps()} showMorphology />
566+
</AnalysisStoreProvider>,
567+
);
568+
await userEvent.click(
569+
screen.getByRole('button', { name: 'Define morpheme breakdown for hello' }),
570+
);
571+
expect(screen.getByTestId('morpheme-popover')).toBeInTheDocument();
572+
// Toggling morphology off unmounts the popover tree; the open state must not survive and
573+
// resurrect the popover when morphology comes back.
574+
rerender(
575+
<AnalysisStoreProvider analysisLanguage="und">
576+
<TokenChip {...requiredProps()} showMorphology={false} />
577+
</AnalysisStoreProvider>,
578+
);
579+
rerender(
580+
<AnalysisStoreProvider analysisLanguage="und">
581+
<TokenChip {...requiredProps()} showMorphology />
582+
</AnalysisStoreProvider>,
583+
);
584+
expect(screen.queryByTestId('morpheme-popover')).not.toBeInTheDocument();
585+
});
586+
562587
it('closes the popover via onClose', async () => {
563588
render(
564589
<AnalysisStoreProvider analysisLanguage="und">

src/components/TokenChip.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ export function TokenChip({
7676
setDraft(committedGloss);
7777
}, [committedGloss]);
7878

79+
// The popover tree unmounts with the morpheme row when showMorphology turns off, but this state
80+
// lives on the chip and would survive — silently reopening the popover when morphology is shown
81+
// again. Clear it so hiding morphology also closes the popover.
82+
useEffect(() => {
83+
if (!showMorphology) setPopoverOpen(false);
84+
}, [showMorphology]);
85+
7986
const handleMouseDown: MouseEventHandler<HTMLInputElement> = (e) => {
8087
// Prevent the browser's built-in focus-and-scroll so only the React-controlled
8188
// smooth scrollIntoView fires. We re-focus manually with preventScroll instead.

0 commit comments

Comments
 (0)