Skip to content

Commit 4d15a22

Browse files
Split TokenChip into word and inert variants; convert PhraseBox to label
- Extract `InertTokenChip` / `MemoizedInertTokenChip` for non-word tokens, removing the punctuation branch from `TokenChip` so it only handles words - Convert `PhraseBox` from a `<button>` to a `<label>` so clicking anywhere on the phrase focuses the first gloss input natively - Rename prop `onClick` → `onFocusPhrase` on `PhraseBox` to reflect the narrowed trigger (gloss-input focus only, not arbitrary clicks) - Update `ContinuousView` and `SegmentView` to import `MemoizedInertTokenChip` and pass `onFocusPhrase` instead of `onClick` - Update all test mocks and assertions to match the new component shapes
1 parent eccd6dc commit 4d15a22

8 files changed

Lines changed: 137 additions & 198 deletions

File tree

src/__tests__/components/ContinuousView.test.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,30 @@ jest.mock('../../components/GlossStore', () => ({
2525
/** Render options that wrap every test render in a `GlossStoreProvider`. */
2626
const withGlossStore = { wrapper: GlossStoreProvider };
2727

28+
jest.mock('../../components/TokenChip', () => ({
29+
__esModule: true,
30+
MemoizedInertTokenChip({ token }: Readonly<{ token: Token }>) {
31+
return <span>{token.surfaceText}</span>;
32+
},
33+
}));
34+
2835
jest.mock('../../components/PhraseBox', () => ({
2936
__esModule: true,
3037
default: ({
31-
isFocused = false,
3238
index,
33-
onClick,
39+
isFocused = false,
40+
onFocusPhrase,
3441
tokens,
3542
}: Readonly<{
36-
isFocused: boolean;
3743
index?: number;
38-
onClick?: (index?: number) => void;
44+
isFocused: boolean;
45+
onFocusPhrase?: (index?: number) => void;
3946
tokens: Token[];
4047
}>) => (
4148
<button
4249
data-focus-state={isFocused ? 'focused' : 'default'}
4350
data-phrase-box="true"
44-
onClick={() => onClick?.(index)}
51+
onClick={() => onFocusPhrase?.(index)}
4552
type="button"
4653
>
4754
{tokens.map((t) => (
@@ -51,12 +58,6 @@ jest.mock('../../components/PhraseBox', () => ({
5158
),
5259
}));
5360

54-
jest.mock('../../components/TokenChip', () => ({
55-
__esModule: true,
56-
default: ({ token }: { token: Token }) => <span>{token.surfaceText}</span>,
57-
TokenChip: ({ token }: { token: Token }) => <span>{token.surfaceText}</span>,
58-
}));
59-
6061
// ---------------------------------------------------------------------------
6162
// Test fixtures
6263
// ---------------------------------------------------------------------------
@@ -301,8 +302,8 @@ function makeLargeBook(count: number): Book {
301302
{
302303
id: `large-tok-${i}`,
303304
surfaceText: `word${i}`,
304-
writingSystem: 'en' as const,
305-
type: 'word' as const,
305+
writingSystem: 'en',
306+
type: 'word',
306307
charStart: 0,
307308
charEnd: String(`word${i}`).length,
308309
},
@@ -322,14 +323,14 @@ const scrollIntoViewMock = jest.fn();
322323
* @returns An object containing all required ContinuousView props set to no-op stubs.
323324
*/
324325
function requiredProps(): {
325-
activeVerse: ScriptureRef;
326326
activePhraseIndex: undefined;
327+
activeVerse: ScriptureRef;
327328
onFocusPhraseIndexChange: jest.Mock;
328329
onVerseChange: jest.Mock;
329330
} {
330331
return {
331-
activeVerse: { book: 'GEN', chapter: 1, verse: 1 },
332332
activePhraseIndex: undefined,
333+
activeVerse: { book: 'GEN', chapter: 1, verse: 1 },
333334
onFocusPhraseIndexChange: jest.fn(),
334335
onVerseChange: jest.fn(),
335336
};
@@ -375,11 +376,11 @@ describe('ContinuousView initial render', () => {
375376
expect(screen.getByRole('button', { name: 'Next token' })).toBeInTheDocument();
376377
});
377378

378-
it('renders a non-word token via TokenChip within the strip', () => {
379+
it('renders a non-word token via InertTokenChip within the strip', () => {
379380
// makeMixedBook: GEN 1:1 has a word token; GEN 1:2 has a punctuation token
380381
render(<ContinuousView book={makeMixedBook()} {...requiredProps()} />, withGlossStore);
381382

382-
// Both the word chip ("In") and the punctuation chip (".") must appear
383+
// Both the word chip ("In") and the inert chip (".") must appear
383384
expect(screen.getByText('In')).toBeInTheDocument();
384385
expect(screen.getByText('.')).toBeInTheDocument();
385386
});

src/__tests__/components/PhraseBox.test.tsx

Lines changed: 20 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const TEST_TOKEN_2 = {
101101
type PhraseBoxTestProps = {
102102
index: number | undefined;
103103
isFocused: boolean;
104-
onClick: (index?: number) => void;
104+
onFocusPhrase: (index?: number) => void;
105105
tokens: (Token & { type: 'word' })[];
106106
};
107107

@@ -115,22 +115,21 @@ function requiredProps(): PhraseBoxTestProps {
115115
return {
116116
index: undefined,
117117
isFocused: false,
118-
onClick: jest.fn(),
118+
onFocusPhrase: jest.fn(),
119119
tokens: [TEST_TOKEN],
120120
};
121121
}
122122

123123
describe('PhraseBox', () => {
124-
it('renders as a button', () => {
124+
it('renders as a label', () => {
125125
render(
126126
<GlossStoreProvider>
127127
<PhraseBox {...requiredProps()} />
128128
</GlossStoreProvider>,
129129
);
130130

131131
const phraseBox = document.querySelector('[data-phrase-box="true"]');
132-
expect(phraseBox?.tagName).toBe('BUTTON');
133-
expect(phraseBox).toHaveAttribute('type', 'button');
132+
expect(phraseBox?.tagName).toBe('LABEL');
134133
});
135134

136135
it('renders one TokenChip per token in the tokens array', () => {
@@ -144,18 +143,17 @@ describe('PhraseBox', () => {
144143
expect(screen.getByTestId('token-token-2')).toBeInTheDocument();
145144
});
146145

147-
it('calls onClick when button is clicked', async () => {
148-
const mockOnClick = jest.fn();
146+
it('clicking the outer container focuses the first gloss input', async () => {
149147
render(
150148
<GlossStoreProvider>
151-
<PhraseBox {...requiredProps()} onClick={mockOnClick} />
149+
<PhraseBox {...requiredProps()} tokens={[TEST_TOKEN, TEST_TOKEN_2]} />
152150
</GlossStoreProvider>,
153151
);
154152

155-
const button = screen.getByRole('button');
156-
await userEvent.click(button);
153+
const phraseBox = document.querySelector('[data-phrase-box="true"]');
154+
await userEvent.click(phraseBox ?? document.body);
157155

158-
expect(mockOnClick).toHaveBeenCalledTimes(1);
156+
expect(screen.getByRole('textbox', { name: 'Gloss for Hello' })).toHaveFocus();
159157
});
160158

161159
it('applies focused border and background when isFocused is true', () => {
@@ -186,27 +184,15 @@ describe('PhraseBox', () => {
186184
expect(phraseBox).toHaveClass('tw:bg-muted/20');
187185
});
188186

189-
it('button always has cursor-pointer and text-left classes', () => {
187+
it('phrase box does not override cursor on gap areas', () => {
190188
render(
191189
<GlossStoreProvider>
192190
<PhraseBox {...requiredProps()} isFocused />
193191
</GlossStoreProvider>,
194192
);
195193

196-
const button = screen.getByRole('button');
197-
expect(button).toHaveClass('tw:cursor-pointer');
198-
expect(button).toHaveClass('tw:text-left');
199-
});
200-
201-
it('button always has hover styling classes', () => {
202-
render(
203-
<GlossStoreProvider>
204-
<PhraseBox {...requiredProps()} />
205-
</GlossStoreProvider>,
206-
);
207-
208-
const button = screen.getByRole('button');
209-
expect(button).toHaveClass('tw:hover:bg-muted/30');
194+
const phraseBox = document.querySelector('[data-phrase-box="true"]');
195+
expect(phraseBox).not.toHaveClass('tw:cursor-text');
210196
});
211197

212198
it('renders tokens in the order they appear in the tokens array', () => {
@@ -257,38 +243,28 @@ describe('PhraseBox', () => {
257243
expect(spy).toHaveBeenNthCalledWith(2, 'token-1', 'hi');
258244
});
259245

260-
it('calls onClick with index when a gloss input receives focus', async () => {
261-
const handleClick = jest.fn();
246+
it('calls onFocusPhrase with index when a gloss input receives focus', async () => {
247+
const handleFocus = jest.fn();
262248
render(
263249
<GlossStoreProvider>
264-
<PhraseBox {...requiredProps()} onClick={handleClick} index={2} />
250+
<PhraseBox {...requiredProps()} onFocusPhrase={handleFocus} index={2} />
265251
</GlossStoreProvider>,
266252
);
267253

268254
await userEvent.click(screen.getByRole('textbox', { name: 'Gloss for Hello' }));
269255

270-
expect(handleClick).toHaveBeenCalledWith(2);
271-
});
272-
273-
it('button always has tabIndex -1 so tab focus goes only to gloss inputs', () => {
274-
render(
275-
<GlossStoreProvider>
276-
<PhraseBox {...requiredProps()} />
277-
</GlossStoreProvider>,
278-
);
279-
280-
expect(screen.getByRole('button')).toHaveAttribute('tabindex', '-1');
256+
expect(handleFocus).toHaveBeenCalledWith(2);
281257
});
282258

283-
it('button always has padding and gap spacing classes', () => {
259+
it('phrase box always has padding and gap spacing classes', () => {
284260
render(
285261
<GlossStoreProvider>
286262
<PhraseBox {...requiredProps()} />
287263
</GlossStoreProvider>,
288264
);
289265

290-
const button = screen.getByRole('button');
291-
expect(button).toHaveClass('tw:px-1');
292-
expect(button).toHaveClass('tw:py-0.5');
266+
const phraseBox = document.querySelector('[data-phrase-box="true"]');
267+
expect(phraseBox).toHaveClass('tw:px-1');
268+
expect(phraseBox).toHaveClass('tw:py-0.5');
293269
});
294270
});

src/__tests__/components/SegmentView.test.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,31 @@ jest.mock('../../components/GlossStore', () => ({
2222
useGlossDispatch: () => () => {},
2323
}));
2424

25+
jest.mock('../../components/TokenChip', () => ({
26+
__esModule: true,
27+
MemoizedInertTokenChip({ token }: Readonly<{ token: Token }>) {
28+
return <span>{token.surfaceText}</span>;
29+
},
30+
}));
31+
2532
jest.mock('../../components/PhraseBox', () => ({
2633
__esModule: true,
2734
default: ({
2835
index,
2936
isFocused = false,
30-
onClick,
37+
onFocusPhrase,
3138
tokens,
3239
}: Readonly<{
3340
index?: number;
3441
isFocused: boolean;
35-
onClick?: (index?: number) => void;
42+
onFocusPhrase?: (index?: number) => void;
3643
tokens: Token[];
3744
}>) => (
3845
<span data-focus-state={isFocused ? 'focused' : 'default'}>
3946
{tokens.map((t) => (
4047
<span key={t.id}>
41-
{onClick ? (
42-
<button onClick={() => onClick(index)} type="button">
48+
{onFocusPhrase ? (
49+
<button onClick={() => onFocusPhrase(index)} type="button">
4350
{t.surfaceText}
4451
</button>
4552
) : (
@@ -51,12 +58,6 @@ jest.mock('../../components/PhraseBox', () => ({
5158
),
5259
}));
5360

54-
jest.mock('../../components/TokenChip', () => ({
55-
__esModule: true,
56-
default: ({ token }: Readonly<{ token: Token }>) => <span>{token.surfaceText}</span>,
57-
TokenChip: ({ token }: Readonly<{ token: Token }>) => <span>{token.surfaceText}</span>,
58-
}));
59-
6061
/** A word token segment. */
6162
const WORD_SEGMENT: Segment = {
6263
id: 'GEN 1:1',

0 commit comments

Comments
 (0)