Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Before reporting any documentation as missing, open the file and confirm the JSD

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.

## Buttons inside the TokenChip label

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.

## Mock cleanup in tests

[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.
Expand Down
126 changes: 124 additions & 2 deletions __mocks__/platform-bible-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* without extra transform configuration. This stub provides the subset used by the extension.
*/

import { forwardRef } from 'react';
import type { ReactElement, ReactNode } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import type { MouseEventHandler, ReactElement, ReactNode } from 'react';

export interface MenuItemContainingCommand {
label: `%${string}%`;
Expand Down Expand Up @@ -323,6 +323,128 @@ export function Switch({
);
}

/**
* Stub popover root that renders its children unconditionally. The extension conditionally mounts
* the content component while open (so its draft state re-initializes per open), so visibility
* needs no simulation here.
*
* @param props - Component props.
* @param props.children - Anchor and (while open) content elements.
* @returns The children unchanged.
*/
export function Popover({
children,
}: Readonly<{ children?: ReactNode; open?: boolean; modal?: boolean }>): ReactElement {
return <>{children}</>;
}

/**
* Stub popover anchor that renders its children as-is, matching the real component's `asChild`
* pass-through behavior.
*
* @param props - Component props.
* @param props.children - The element the popover is anchored to.
* @returns The children unchanged.
*/
export function PopoverAnchor({
children,
}: Readonly<{ children?: ReactNode; asChild?: boolean }>): ReactElement {
return <>{children}</>;
}

/**
* Stub popover content rendered as a plain `<div data-testid="popover-content">`. The real
* component implements positioning, portaling, and dismissal internally; this stub exposes the
* dismissal callbacks so tests can simulate them:
*
* - `onOpenAutoFocus` is invoked once on mount (mirroring Radix's open auto-focus event).
* - An Escape keydown anywhere inside the content invokes `onEscapeKeyDown`.
* - A sentinel `data-testid="popover-outside"` button invokes `onInteractOutside` on click,
* simulating a pointer interaction outside the popover.
* - A sentinel `data-testid="popover-close"` button invokes `onCloseAutoFocus` on click,
* simulating Radix's focus-restoration event fired as the popover closes.
*
* @param props - Component props.
* @param props.children - Panel content.
* @param props.className - CSS class names forwarded to the div.
* @param props.onEscapeKeyDown - Called with the native `KeyboardEvent` when Escape is pressed
* inside the content, matching Radix's signature.
* @param props.onInteractOutside - Called with a `CustomEvent` carrying the original pointer event
* in `detail.originalEvent` when the sentinel outside button is clicked, matching the shape of
* Radix's `PointerDownOutsideEvent`.
* @param props.onOpenAutoFocus - Called once on mount with a plain `Event`.
* @param props.onCloseAutoFocus - Called with a plain `Event` when the sentinel close button is
* clicked, mirroring Radix's close-time focus-restoration event.
* @param props.onClick - Click handler forwarded to the div.
* @param props.onMouseDown - Mouse-down handler forwarded to the div.
* @returns A `<div data-testid="popover-content">` with the panel content and sentinel controls.
*/
export function PopoverContent({
children,
className,
onEscapeKeyDown,
onInteractOutside,
onOpenAutoFocus,
onCloseAutoFocus,
onClick,
onMouseDown,
}: Readonly<{
children?: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end';
sideOffset?: number;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onInteractOutside?: (event: CustomEvent) => void;
onOpenAutoFocus?: (event: Event) => void;
onCloseAutoFocus?: (event: Event) => void;
onClick?: MouseEventHandler<HTMLDivElement>;
onMouseDown?: MouseEventHandler<HTMLDivElement>;
}>): ReactElement {
// Capture the mount-time callback so the simulation fires exactly once, like the real event.
const openAutoFocusRef = useRef(onOpenAutoFocus);
useEffect(() => {
openAutoFocusRef.current?.(new Event('openAutoFocus'));
}, []);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className={className}
data-testid="popover-content"
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Escape') onEscapeKeyDown?.(e.nativeEvent);
}}
onMouseDown={onMouseDown}
>
{children}
{onInteractOutside && (
<button
data-testid="popover-outside"
type="button"
onClick={(e) =>
onInteractOutside(
new CustomEvent('dismissableLayer.pointerDownOutside', {
detail: { originalEvent: e.nativeEvent },
}),
)
}
>
outside
</button>
)}
{onCloseAutoFocus && (
<button
data-testid="popover-close"
type="button"
onClick={() => onCloseAutoFocus(new Event('closeAutoFocus'))}
>
close
</button>
)}
</div>
);
}

/**
* Stub label rendered as a native `<label>` element.
*
Expand Down
10 changes: 10 additions & 0 deletions contributions/localizedStrings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
"%interlinearizer_projectSettings_simplifyPhrasesDescription%": "Hide interactive controls (split, unlink, remove-token) on phrases that are not currently focused, leaving only their style change on hover",
"%interlinearizer_projectSettings_chapterLabelInVerse%": "Show Chapter in Verse Label",
"%interlinearizer_projectSettings_chapterLabelInVerseDescription%": "Mark chapter boundaries by labeling the first verse of each chapter as chapter:verse instead of showing an inline chapter header above it",
"%interlinearizer_viewOption_showMorphology%": "Show morphology",
"%interlinearizer_projectSettings_showMorphology%": "Show Morphology",
"%interlinearizer_projectSettings_showMorphologyDescription%": "Display morpheme breakdown and per-morpheme glosses beneath each word token",
"%interlinearizer_morphemeEditor_splitLabel%": "Split into morphemes",
"%interlinearizer_morphemeEditor_delete%": "Delete",
"%interlinearizer_morphemeEditor_cancel%": "Cancel",
"%interlinearizer_morphemeEditor_done%": "Done",
"%interlinearizer_morphemeGloss_label%": "Gloss for morpheme {form}",
"%interlinearizer_tokenChip_editMorphemes%": "Edit morpheme breakdown for {token}",
"%interlinearizer_tokenChip_defineMorphemes%": "Define morpheme breakdown for {token}",
"%interlinearizer_linkButton_crossSegmentDisabledTooltip%": "Cross-segment phrases are not supported. This link button is outside the current segment.",

"%interlinearizer_modal_create_title%": "Create Interlinear Project",
Expand Down
5 changes: 5 additions & 0 deletions contributions/projectSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
"label": "%interlinearizer_projectSettings_chapterLabelInVerse%",
"description": "%interlinearizer_projectSettings_chapterLabelInVerseDescription%",
"default": false
},
"interlinearizer.showMorphology": {
"label": "%interlinearizer_projectSettings_showMorphology%",
"description": "%interlinearizer_projectSettings_showMorphologyDescription%",
"default": false
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"bara",
"baselining",
"BBCCCVVV",
"believ",
"clickability",
"cullable",
"deconflict",
Expand Down Expand Up @@ -63,6 +64,7 @@
"struc",
"Stylesheet",
"typedefs",
"unanalyzed",
"unhover",
"unobserves",
"unphrased",
Expand Down
Loading
Loading