diff --git a/libs/@guardian/react-crossword/src/components/Grid.tsx b/libs/@guardian/react-crossword/src/components/Grid.tsx index 223f58f7e..3a30eb3ac 100644 --- a/libs/@guardian/react-crossword/src/components/Grid.tsx +++ b/libs/@guardian/react-crossword/src/components/Grid.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; import { memo, useCallback, useEffect, useRef } from 'react'; import type { Coords, Separator, Theme } from '../@types/crossword'; @@ -7,12 +8,11 @@ import { useCurrentClue } from '../context/CurrentClue'; import { useData } from '../context/Data'; import { useProgress } from '../context/Progress'; import { useTheme } from '../context/Theme'; +import { useCheatMode } from '../hooks/useCheatMode'; import { useUpdateCell } from '../hooks/useUpdateCell'; import { keyDownRegex } from '../utils/keydownRegex'; import { Cell } from './Cell'; -// define and cache the regex for valid keydown events - const getCellPosition = (index: number, { cellSize, gutter }: Theme) => index * (cellSize + gutter) + gutter; @@ -101,6 +101,8 @@ export const Grid = () => { const gridRef = useRef(null); const workingDirectionRef = useRef('across'); + const [cheatMode, cheatStyles] = useCheatMode(gridRef); + // keep workingDirectionRef.current up to date with the current entry useEffect(() => { if (currentEntryId) { @@ -208,20 +210,27 @@ export const Grid = () => { break; } default: { - if (currentEntryId && keyDownRegex.test(key)) { - updateCell({ - x: currentCell.x, - y: currentCell.y, - value: key.toUpperCase(), - }); - if (direction === 'across') { - moveFocus({ delta: { x: 1, y: 0 }, isTyping: true }); - } - if (direction === 'down') { - moveFocus({ delta: { x: 0, y: 1 }, isTyping: true }); + if (currentEntryId) { + const value = cheatMode + ? cells.getByCoords({ x: currentCell.x, y: currentCell.y }) + ?.solution + : keyDownRegex.test(key) && key.toUpperCase(); + + if (value) { + updateCell({ + x: currentCell.x, + y: currentCell.y, + value, + }); + if (direction === 'across') { + moveFocus({ delta: { x: 1, y: 0 }, isTyping: true }); + } + if (direction === 'down') { + moveFocus({ delta: { x: 0, y: 1 }, isTyping: true }); + } + } else { + preventDefault = false; } - } else { - preventDefault = false; } break; } @@ -231,7 +240,15 @@ export const Grid = () => { event.preventDefault(); } }, - [currentCell, currentEntryId, moveFocus, handleTab, updateCell], + [ + currentCell, + currentEntryId, + moveFocus, + handleTab, + updateCell, + cheatMode, + cells, + ], ); const selectClickedCell = useCallback( @@ -353,11 +370,14 @@ export const Grid = () => { return ( ) => { + const [konamiProgress, setKonamiProgress] = useState([]); + const [cheatMode, setCheatMode] = useState(false); + + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (cheatMode) { + return; + } + + if (konamiCode[konamiProgress.length] === event.key) { + setKonamiProgress([...konamiProgress, event.key]); + } else { + setKonamiProgress([]); + } + }, + [cheatMode, konamiProgress], + ); + + useEffect(() => { + if (konamiProgress.length === konamiCode.length) { + document.removeEventListener('keydown', onKeyDown); + setCheatMode(true); + ref.current?.classList.add('cheat-mode'); + } + }, [konamiProgress.length, onKeyDown, ref]); + + useEffect(() => { + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, [onKeyDown]); + + return [cheatMode, cheatStyles]; +};