From 247bbabb9c9678b551fdb625c2a50887c7780840 Mon Sep 17 00:00:00 2001 From: Brian Smiley Date: Tue, 7 Jan 2025 00:46:47 -0500 Subject: [PATCH 1/3] feat: improve keyboard navigation - Make tab key navigate between incomplete clues - Keeps spacebar behavior to handle direction swapping --- src/CrosswordProvider.tsx | 73 ++++++++++- src/__test__/CrosswordProvider.test.tsx | 164 +++++++++++++++++++----- 2 files changed, 200 insertions(+), 37 deletions(-) diff --git a/src/CrosswordProvider.tsx b/src/CrosswordProvider.tsx index 8d8633c..a79e6fd 100644 --- a/src/CrosswordProvider.tsx +++ b/src/CrosswordProvider.tsx @@ -702,9 +702,8 @@ const CrosswordProvider = React.forwardRef< case 'ArrowRight': moveRelative(0, 1); break; - - case ' ': // treat space like tab? - case 'Tab': { + // Spacebard switches direction if there is a clue + case ' ': { const other = otherDirection(currentDirection); const cellData = getCellData( focusedRow, @@ -716,6 +715,74 @@ const CrosswordProvider = React.forwardRef< } break; } + // Tab should go to the next clue in the current direction that is not complete, or to the first clue in the other direciton that is not complete + case 'Tab': { + const other = otherDirection(currentDirection); + let target = null; + let targetDirection = currentDirection; + + // Find next incomplete clue in current direction + const currentClues = clues?.[currentDirection] || []; + const currentClueIndex = currentClues.findIndex( + (c) => c.number === currentNumber + ); + + // Look for incomplete clues after current position + const nextIncomplete = currentClues + .slice(currentClueIndex + 1) + .find((c) => !c.complete); + + if (nextIncomplete) { + target = nextIncomplete; + } else { + // Look for incomplete clues in other direction + const otherClues = clues?.[other] || []; + const firstIncomplete = otherClues.find((c) => !c.complete); + + if (firstIncomplete) { + target = firstIncomplete; + targetDirection = other; + } else { + // Look for incomplete clues before current position in original direction + const wrappedIncomplete = currentClues + .slice(0, currentClueIndex) + .find((c) => !c.complete); + + if (wrappedIncomplete) { + target = wrappedIncomplete; + } + } + } + + if (target) { + // Find first empty cell in the target clue + const info = data[targetDirection][target.number]; + const { row, col, answer } = info; + const across = isAcross(targetDirection); + let foundEmpty = false; + + for (let i = 0; i < answer.length; i++) { + const checkRow = row + (across ? 0 : i); + const checkCol = col + (across ? i : 0); + const cell = getCellData(checkRow, checkCol) as UsedCellData; + + if (!cell.guess) { + // Found first empty cell, move to it + moveTo(checkRow, checkCol, targetDirection); + foundEmpty = true; + break; + } + } + + // If we haven't found an empty cell, move to start of clue + if (!foundEmpty) { + moveTo(row, col, targetDirection); + } + } + + event.preventDefault(); + break; + } // Backspace: delete the current cell, and move to the previous cell // Delete: delete the current cell, but don't move diff --git a/src/__test__/CrosswordProvider.test.tsx b/src/__test__/CrosswordProvider.test.tsx index 24fbee4..b72af52 100644 --- a/src/__test__/CrosswordProvider.test.tsx +++ b/src/__test__/CrosswordProvider.test.tsx @@ -165,39 +165,6 @@ describe('keyboard navigation', () => { expect(y).toBe('20.125'); }); - it('tab switches direction (across to down)', async () => { - const { getByLabelText, getByText, user } = setup( - - ); - const input = getByLabelText('crossword-input'); - - await user.click(getByLabelText('clue-1-across')); - fireEvent.keyDown(input, { key: 'End' }); - - fireEvent.keyDown(input, { key: 'Tab' }); // switches to 2-down - fireEvent.keyDown(input, { key: 'End' }); - fireEvent.keyDown(input, { key: 'X' }); - const { x, y } = posForText(getByText('X')); - expect(x).toBe('20.125'); - expect(y).toBe('20.125'); - }); - - it('tab switches direction (down to across)', async () => { - const { getByLabelText, getByText, user } = setup( - - ); - const input = getByLabelText('crossword-input'); - - await user.click(getByLabelText('clue-2-down')); - - fireEvent.keyDown(input, { key: 'Tab' }); // switches to 1-across - fireEvent.keyDown(input, { key: 'Home' }); - fireEvent.keyDown(input, { key: 'X' }); - const { x, y } = posForText(getByText('X')); - expect(x).toBe('0.125'); - expect(y).toBe('0.125'); - }); - it('space switches direction (across to down)', async () => { const { getByLabelText, getByText, user } = setup( @@ -511,7 +478,7 @@ describe('onAnswerComplete', () => { expect(onAnswerCorrect).toBeCalledTimes(0); - fireEvent.keyDown(input, { key: 'Tab' }); // switches to 2-down + fireEvent.keyDown(input, { key: ' ' }); // switches to 2-down (changed from Tab) expect(onAnswerCorrect).toBeCalledTimes(0); }); }); @@ -941,3 +908,132 @@ function posForText(textEl: HTMLElement) { const rect = textEl!.parentElement!.firstChild! as SVGRectElement; return { x: rect.getAttribute('x'), y: rect.getAttribute('y') }; } + +describe('tab navigation', () => { + it('tab moves to next incomplete clue in current direction', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Start at 1-across + await user.click(getByLabelText('clue-1-across')); + + // Fill in first answer + await user.type(input, 'TWO', { skipClick: true }); + + // Tab should move to next incomplete clue (Across 3: "NO") + fireEvent.keyDown(input, { key: 'Tab' }); + + // Type X at the new position to verify we moved + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); + + // Should be at start of "NO" clue + expect(x).toBe('20.125'); + expect(y).toBe('10.125'); // Second row + }); + + it('tab wraps to first incomplete clue in other direction when no more in current direction', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Start at 1-across and complete it + await user.click(getByLabelText('clue-2-down')); + await user.type(input, 'ONE', { skipClick: true }); + + fireEvent.keyDown(input, { key: 'Tab' }); // tab to 1-across hopefully + + // Type X at the new position + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); + + // Should be at start of first across clue (1-across, which starts at third column) + expect(x).toBe('0.125'); + expect(y).toBe('0.125'); + }); + + it('tab does nothing when all clues are complete', async () => { + const { getByLabelText, user } = setup(); + const input = getByLabelText('crossword-input'); + + // Start at and complete 1-across + await user.click(getByLabelText('clue-1-across')); + await user.type(input, 'TWO', { skipClick: true }); + + // Complete 3-across + fireEvent.keyDown(input, { key: 'Tab' }); + await user.type(input, 'NO', { skipClick: true }); + + // Move to and complete 2-down + fireEvent.keyDown(input, { key: 'Tab' }); + await user.type(input, 'ONE', { skipClick: true }); + + // Record position before Tab + const beforeTab = { + row: input.getAttribute('data-row'), + col: input.getAttribute('data-col'), + }; + + // Tab should do nothing since all clues are complete + fireEvent.keyDown(input, { key: 'Tab' }); + + // Position should remain unchanged + expect(input.getAttribute('data-row')).toBe(beforeTab.row); + expect(input.getAttribute('data-col')).toBe(beforeTab.col); + }); + + it('tab moves to first empty cell in partially filled across clue', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Start at 1-across and partially fill it (T_O) + await user.click(getByLabelText('clue-1-across')); + await user.type(input, 'T', { skipClick: true }); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + await user.type(input, 'O', { skipClick: true }); + + // Move away and back with tab + fireEvent.keyDown(input, { key: 'Tab' }); // tabs over to 3-across + fireEvent.keyDown(input, { key: 'Tab' }); // tabs over to 2-down + fireEvent.keyDown(input, { key: 'Tab' }); // tabs back to 1-across + + // Type X at the new position to verify we're at the empty middle cell + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); + + // Should be at middle cell of TWO + expect(x).toBe('10.125'); + expect(y).toBe('0.125'); + }); + + it('tab moves to first empty cell in partially filled down clue', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Start at 2-down and partially fill it (ON_) + await user.click(getByLabelText('clue-2-down')); + await user.type(input, 'O', { skipClick: true }); + await user.type(input, 'N', { skipClick: true }); + + // Move away and back with tab + fireEvent.keyDown(input, { key: 'Tab' }); // tabs to 1-across + fireEvent.keyDown(input, { key: 'Tab' }); // tabs to 3-across + fireEvent.keyDown(input, { key: 'Tab' }); // tabs back to partially completed 2-down + + // Type X at the new position to verify we're at the empty end + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); + + // Should be at middle cell of ONE + expect(x).toBe('20.125'); + expect(y).toBe('20.125'); + }); +}); From 0467f745f4a72f013704c70175bb7c597beb8180 Mon Sep 17 00:00:00 2001 From: Brian Smiley Date: Tue, 7 Jan 2025 03:24:41 -0500 Subject: [PATCH 2/3] feat: implement jump to next open cell functionality - Refactors tab functionality to reusable "jumpToNextOpenCell" function --- src/CrosswordProvider.tsx | 130 +++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/src/CrosswordProvider.tsx b/src/CrosswordProvider.tsx index a79e6fd..cc506cb 100644 --- a/src/CrosswordProvider.tsx +++ b/src/CrosswordProvider.tsx @@ -660,6 +660,70 @@ const CrosswordProvider = React.forwardRef< moveRelative(across ? 0 : -1, across ? -1 : 0); }, [currentDirection, moveRelative]); + const jumpToNextOpenCell = useCallback(() => { + const other = otherDirection(currentDirection); + let target = null; + let targetDirection = currentDirection; + + // Find next incomplete clue in current direction + const currentClues = clues?.[currentDirection] || []; + const currentClueIndex = currentClues.findIndex( + (c) => c.number === currentNumber + ); + + // Look for incomplete clues after current position + const nextIncomplete = currentClues + .slice(currentClueIndex + 1) + .find((c) => !c.complete); + + if (nextIncomplete) { + target = nextIncomplete; + } else { + // Look for incomplete clues in other direction + const otherClues = clues?.[other] || []; + const firstIncomplete = otherClues.find((c) => !c.complete); + + if (firstIncomplete) { + target = firstIncomplete; + targetDirection = other; + } else { + // Look for incomplete clues before current position in original direction + const wrappedIncomplete = currentClues + .slice(0, currentClueIndex) + .find((c) => !c.complete); + + if (wrappedIncomplete) { + target = wrappedIncomplete; + } + } + } + + if (target) { + // Find first empty cell in the target clue + const info = data[targetDirection][target.number]; + const { row, col, answer } = info; + const across = isAcross(targetDirection); + let foundEmpty = false; + + for (let i = 0; i < answer.length; i++) { + const checkRow = row + (across ? 0 : i); + const checkCol = col + (across ? i : 0); + const cell = getCellData(checkRow, checkCol) as UsedCellData; + + if (!cell.guess) { + // Found first empty cell, move to it + moveTo(checkRow, checkCol, targetDirection); + foundEmpty = true; + break; + } + } + + // If we haven't found an empty cell, move to start of clue + if (!foundEmpty) { + moveTo(row, col, targetDirection); + } + } + }, [currentDirection, clues, currentNumber, getCellData, moveTo]); // keyboard handling const handleSingleCharacter = useCallback( (char: string) => { @@ -715,71 +779,9 @@ const CrosswordProvider = React.forwardRef< } break; } - // Tab should go to the next clue in the current direction that is not complete, or to the first clue in the other direciton that is not complete + // Tab jumps to the next open cell in the next incomplete clue case 'Tab': { - const other = otherDirection(currentDirection); - let target = null; - let targetDirection = currentDirection; - - // Find next incomplete clue in current direction - const currentClues = clues?.[currentDirection] || []; - const currentClueIndex = currentClues.findIndex( - (c) => c.number === currentNumber - ); - - // Look for incomplete clues after current position - const nextIncomplete = currentClues - .slice(currentClueIndex + 1) - .find((c) => !c.complete); - - if (nextIncomplete) { - target = nextIncomplete; - } else { - // Look for incomplete clues in other direction - const otherClues = clues?.[other] || []; - const firstIncomplete = otherClues.find((c) => !c.complete); - - if (firstIncomplete) { - target = firstIncomplete; - targetDirection = other; - } else { - // Look for incomplete clues before current position in original direction - const wrappedIncomplete = currentClues - .slice(0, currentClueIndex) - .find((c) => !c.complete); - - if (wrappedIncomplete) { - target = wrappedIncomplete; - } - } - } - - if (target) { - // Find first empty cell in the target clue - const info = data[targetDirection][target.number]; - const { row, col, answer } = info; - const across = isAcross(targetDirection); - let foundEmpty = false; - - for (let i = 0; i < answer.length; i++) { - const checkRow = row + (across ? 0 : i); - const checkCol = col + (across ? i : 0); - const cell = getCellData(checkRow, checkCol) as UsedCellData; - - if (!cell.guess) { - // Found first empty cell, move to it - moveTo(checkRow, checkCol, targetDirection); - foundEmpty = true; - break; - } - } - - // If we haven't found an empty cell, move to start of clue - if (!foundEmpty) { - moveTo(row, col, targetDirection); - } - } - + jumpToNextOpenCell(); event.preventDefault(); break; } From 54370471319de29ecb3109f4c5e2dae0203303fd Mon Sep 17 00:00:00 2001 From: Brian Smiley Date: Tue, 7 Jan 2025 04:47:30 -0500 Subject: [PATCH 3/3] feat: adds CWProvider prop option autoAdvanceOnClueComplete -If enabled, filling in the last cell in a clue jumps to appropriate next cell -Adds this prop as true in Crossword.md example for demonstration --- src/Crossword.md | 2 +- src/CrosswordProvider.tsx | 87 ++++++++++++++- src/__test__/CrosswordProvider.test.tsx | 139 +++++++++++++++++------- 3 files changed, 186 insertions(+), 42 deletions(-) diff --git a/src/Crossword.md b/src/Crossword.md index 3e6cce0..f95945b 100644 --- a/src/Crossword.md +++ b/src/Crossword.md @@ -144,6 +144,6 @@ const data = { };
- +
; ``` diff --git a/src/CrosswordProvider.tsx b/src/CrosswordProvider.tsx index cc506cb..86cafcd 100644 --- a/src/CrosswordProvider.tsx +++ b/src/CrosswordProvider.tsx @@ -166,6 +166,11 @@ export const crosswordProviderPropTypes = { */ onClueSelected: PropTypes.func, + /** + * whether to automatically advance to the next incomplete clue (or jump to the first incomplete cell in the current clue) when entering the final character of a clue + */ + autoJumpFromClueEnd: PropTypes.bool, + children: PropTypes.node, }; @@ -179,6 +184,11 @@ export type CrosswordProviderProps = EnhancedProps< */ data: CluesInput; + /** + * whether to automatically advance to the next incomplete clue (or jump to the first incomplete cell in the current clue) when entering the final character of a clue + */ + autoJumpFromClueEnd?: boolean; + /** * callback function that fires when a player completes an answer, whether * correct or not; called with `(direction, number, correct, answer)` @@ -342,6 +352,7 @@ const CrosswordProvider = React.forwardRef< onClueSelected, useStorage, storageKey, + autoJumpFromClueEnd, children, }, ref @@ -659,7 +670,9 @@ const CrosswordProvider = React.forwardRef< const across = isAcross(currentDirection); moveRelative(across ? 0 : -1, across ? -1 : 0); }, [currentDirection, moveRelative]); - + /** Advances to the next open cell; wraps around to clues in the other direction, then earlier clues in the current direction + * If all clues are complete, jumps to the first cell of the next clue + */ const jumpToNextOpenCell = useCallback(() => { const other = otherDirection(currentDirection); let target = null; @@ -723,14 +736,81 @@ const CrosswordProvider = React.forwardRef< moveTo(row, col, targetDirection); } } + // If all clues are complete, jump to the first cell of the next clue + else if (currentClueIndex + 1 < currentClues.length) { + // Find next clue in current direction + const nextClue = currentClues[currentClueIndex + 1]; + // Move to first cell of next clue + const info = data[currentDirection][nextClue.number]; + moveTo(info.row, info.col, currentDirection); + } else { + // If no next clue in current direction, try other direction + const otherClues = clues?.[other] || []; + if (otherClues.length > 0) { + const firstClue = otherClues[0]; + const info = data[other][firstClue.number]; + moveTo(info.row, info.col, other); + } + } }, [currentDirection, clues, currentNumber, getCellData, moveTo]); // keyboard handling const handleSingleCharacter = useCallback( (char: string) => { setCellCharacter(focusedRow, focusedCol, char.toUpperCase()); - moveForward(); + + // Only check for auto-advance if the feature is enabled + if (autoJumpFromClueEnd) { + // Check if we're on the last cell of the current clue + const info = data[currentDirection][currentNumber]; + const { row, col, answer } = info; + const across = isAcross(currentDirection); + + // Calculate our position within the clue + const cluePos = across ? focusedCol - col : focusedRow - row; + + // If we're on the last cell of the clue + if (cluePos === answer.length - 1) { + // Check if current clue is complete + let isComplete = true; + for (let i = 0; i < answer.length - 1; i++) { + const checkRow = row + (across ? 0 : i); + const checkCol = col + (across ? i : 0); + const cell = getCellData(checkRow, checkCol) as UsedCellData; + + if (!cell.guess) { + isComplete = false; + // Jump back to first open cell in the current clue + moveTo(checkRow, checkCol, currentDirection); + break; + } + } + + if (isComplete) { + // If complete, jump to next incomplete clue + jumpToNextOpenCell(); + } + } else { + // Not at end of clue, just move forward + moveForward(); + } + } else { + // If auto-advance is disabled, just move forward + moveForward(); + } }, - [focusedRow, focusedCol, setCellCharacter, moveForward] + [ + focusedRow, + focusedCol, + setCellCharacter, + moveForward, + jumpToNextOpenCell, + currentDirection, + currentNumber, + data, + getCellData, + moveTo, + autoJumpFromClueEnd, + ] ); // We use the keydown event for control/arrow keys, but not for textual @@ -1202,5 +1282,6 @@ CrosswordProvider.defaultProps = { onCrosswordCorrect: undefined, onCellChange: undefined, onClueSelected: undefined, + autoJumpFromClueEnd: false, children: undefined, }; diff --git a/src/__test__/CrosswordProvider.test.tsx b/src/__test__/CrosswordProvider.test.tsx index b72af52..90f5f46 100644 --- a/src/__test__/CrosswordProvider.test.tsx +++ b/src/__test__/CrosswordProvider.test.tsx @@ -408,6 +408,72 @@ describe('keyboard navigation', () => { expect(x).toBe('20.125'); expect(y).toBe('0.125'); }); + + describe('auto-advance on clue completion', () => { + it('advances to next incomplete clue when completing an across clue', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Start at 1-across + await user.click(getByLabelText('clue-1-across')); + + // Type TWO, should auto-advance to 3-across + await user.type(input, 'TWO', { skipClick: true }); + + // Type X to verify position + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); + + // Should be at start of "NO" clue + expect(x).toBe('20.125'); + expect(y).toBe('10.125'); // Second row + }); + + it('advances to next incomplete clue when completing a down clue', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Start at 2-down + await user.click(getByLabelText('clue-2-down')); + + // Type ONE, should auto-advance to 1-across + await user.type(input, 'ONE', { skipClick: true }); + + // Type X to verify position + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); + + // Should be at start of first across clue + expect(x).toBe('0.125'); + expect(y).toBe('0.125'); + }); + + it('progresses to the next clue once the grid is full', async () => { + const { getByLabelText, getByText, user } = setup( + + ); + const input = getByLabelText('crossword-input'); + + // Complete the puzzle + await user.click(getByLabelText('clue-1-across')); + await user.type(input, 'TWONOE', { skipClick: true }); + // Type X to verify position at 0,0 and Y where we should wrap to clue 3-across + await user.type(input, 'XIIY', { skipClick: true }); + const { x, y } = posForText(getByText('X')); + + // Should have wrapped back to start position + expect(x).toBe('0.125'); + expect(y).toBe('0.125'); + + const { x: x2, y: y2 } = posForText(getByText('Y')); + expect(x2).toBe('20.125'); + expect(y2).toBe('10.125'); + }); + }); }); describe('onAnswerComplete', () => { @@ -920,11 +986,9 @@ describe('tab navigation', () => { await user.click(getByLabelText('clue-1-across')); // Fill in first answer - await user.type(input, 'TWO', { skipClick: true }); - - // Tab should move to next incomplete clue (Across 3: "NO") + await user.type(input, 'TW', { skipClick: true }); + // Tab over to 3-across fireEvent.keyDown(input, { key: 'Tab' }); - // Type X at the new position to verify we moved fireEvent.keyDown(input, { key: 'X' }); const { x, y } = posForText(getByText('X')); @@ -940,9 +1004,9 @@ describe('tab navigation', () => { ); const input = getByLabelText('crossword-input'); - // Start at 1-across and complete it + // Start at 2-down and begin filling it in await user.click(getByLabelText('clue-2-down')); - await user.type(input, 'ONE', { skipClick: true }); + await user.type(input, 'ON', { skipClick: true }); fireEvent.keyDown(input, { key: 'Tab' }); // tab to 1-across hopefully @@ -955,8 +1019,10 @@ describe('tab navigation', () => { expect(y).toBe('0.125'); }); - it('tab does nothing when all clues are complete', async () => { - const { getByLabelText, user } = setup(); + it('tab goes to first cell of next clue when all clues are complete', async () => { + const { getByLabelText, getByText, user } = setup( + + ); const input = getByLabelText('crossword-input'); // Start at and complete 1-across @@ -964,25 +1030,23 @@ describe('tab navigation', () => { await user.type(input, 'TWO', { skipClick: true }); // Complete 3-across - fireEvent.keyDown(input, { key: 'Tab' }); await user.type(input, 'NO', { skipClick: true }); // Move to and complete 2-down - fireEvent.keyDown(input, { key: 'Tab' }); - await user.type(input, 'ONE', { skipClick: true }); + await user.type(input, 'E', { skipClick: true }); - // Record position before Tab - const beforeTab = { - row: input.getAttribute('data-row'), - col: input.getAttribute('data-col'), - }; + // Go back to 1-across + await user.click(getByLabelText('clue-1-across')); - // Tab should do nothing since all clues are complete + // Tape should bring us to the first cell of 3-across fireEvent.keyDown(input, { key: 'Tab' }); + // Type X at the new position + fireEvent.keyDown(input, { key: 'X' }); + const { x, y } = posForText(getByText('X')); - // Position should remain unchanged - expect(input.getAttribute('data-row')).toBe(beforeTab.row); - expect(input.getAttribute('data-col')).toBe(beforeTab.col); + // Should be at start of 3-across in r2c3 + expect(x).toBe('20.125'); + expect(y).toBe('10.125'); }); it('tab moves to first empty cell in partially filled across clue', async () => { @@ -991,25 +1055,23 @@ describe('tab navigation', () => { ); const input = getByLabelText('crossword-input'); - // Start at 1-across and partially fill it (T_O) + // Start at 3-across and fill in N + await user.click(getByLabelText('clue-3-across')); + await user.type(input, 'N', { skipClick: true }); + + // Move to 1-across await user.click(getByLabelText('clue-1-across')); - await user.type(input, 'T', { skipClick: true }); - fireEvent.keyDown(input, { key: 'ArrowRight' }); - fireEvent.keyDown(input, { key: 'ArrowRight' }); - await user.type(input, 'O', { skipClick: true }); - // Move away and back with tab - fireEvent.keyDown(input, { key: 'Tab' }); // tabs over to 3-across - fireEvent.keyDown(input, { key: 'Tab' }); // tabs over to 2-down - fireEvent.keyDown(input, { key: 'Tab' }); // tabs back to 1-across + // Tab should bring us to empty O in 3-across + fireEvent.keyDown(input, { key: 'Tab' }); - // Type X at the new position to verify we're at the empty middle cell + // Type X at the new position to verify location fireEvent.keyDown(input, { key: 'X' }); const { x, y } = posForText(getByText('X')); - // Should be at middle cell of TWO - expect(x).toBe('10.125'); - expect(y).toBe('0.125'); + // Should be at second cell of NO (3-across) + expect(x).toBe('30.125'); + expect(y).toBe('10.125'); }); it('tab moves to first empty cell in partially filled down clue', async () => { @@ -1023,12 +1085,13 @@ describe('tab navigation', () => { await user.type(input, 'O', { skipClick: true }); await user.type(input, 'N', { skipClick: true }); - // Move away and back with tab - fireEvent.keyDown(input, { key: 'Tab' }); // tabs to 1-across - fireEvent.keyDown(input, { key: 'Tab' }); // tabs to 3-across - fireEvent.keyDown(input, { key: 'Tab' }); // tabs back to partially completed 2-down + // Go to 1-across + await user.click(getByLabelText('clue-1-across')); + // Tab twice to go to 3-across then 2-down + fireEvent.keyDown(input, { key: 'Tab' }); + fireEvent.keyDown(input, { key: 'Tab' }); - // Type X at the new position to verify we're at the empty end + // Type X at the new position to verify we're at the empty end of clue 2-down fireEvent.keyDown(input, { key: 'X' }); const { x, y } = posForText(getByText('X'));