/* eslint-disable no-console */ import React, { useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import PropTypes from 'prop-types'; import produce from 'immer'; import { ThemeContext, ThemeProvider } from 'styled-components'; import { CrosswordContext, CrosswordContextType } from './context'; import { AnswerTuple, CluesData, CluesInput, cluesInputShapeOriginal, Direction, EnhancedProps, FocusHandler, GridPosition, GridData, UsedCellData, CellData, UnusedCellData, } from './types'; import { bothDirections, clearGuesses, createGridData, isAcross, loadGuesses, otherDirection, saveGuesses, } from './util'; const defaultStorageKey = 'guesses'; export const crosswordProviderPropTypes = { /** * clue/answer data; see Clue * input format for details. */ data: cluesInputShapeOriginal.isRequired, /** presentation values for the crossword; these override any values coming from a parent ThemeProvider context. */ theme: PropTypes.shape({ /** * whether to allow a non-square rendering * @since 5.1.0 */ allowNonSquare: PropTypes.bool, /** browser-width at which the clues go from showing beneath the grid to showing beside the grid */ columnBreakpoint: PropTypes.string, /** overall background color (fill) for the crossword grid; can be `'transparent'` to show through a page background image */ gridBackground: PropTypes.string, /** background for an answer cell */ cellBackground: PropTypes.string, /** border for an answer cell */ cellBorder: PropTypes.string, /** color for answer text (entered by the player) */ textColor: PropTypes.string, /** color for the across/down numbers in the grid */ numberColor: PropTypes.string, /** background color for the cell with focus, the one that the player is typing into */ focusBackground: PropTypes.string, /** * background color for the cells in the answer the player is working on, * helps indicate in which direction focus will be moving; also used as a * background on the active clue */ highlightBackground: PropTypes.string, }), /** whether to use browser storage to persist the player's work-in-progress */ useStorage: PropTypes.bool, /** * a custom storage key to use for persistence; defaults to "guesses" when not * provided */ storageKey: PropTypes.string, /** * callback function that fires when a player completes an answer, whether * correct or not; called with `(direction, number, correct, answer)` * arguments, where `direction` is `'across'` or `'down'`, `number` is the * clue number as text (like `'1'`), `correct` is whether the guessed answer * is correct and `answer` is the (actual and correct) answer itself * * @since 4.3.0 */ onAnswerComplete: PropTypes.func, /** * callback function that fires when a player answers a clue correctly; called * with `(direction, number, answer)` arguments, where `direction` is * `'across'` or `'down'`, `number` is the clue number as text (like `'1'`), * and `answer` is the answer itself * * @since 4.3.0; replacing `onCorrect` (to reduce ambiguity) */ onAnswerCorrect: PropTypes.func, /** * callback function that fires when a player answers a clue correctly; called * with `(direction, number, answer)` arguments, where `direction` is * `'across'` or `'down'`, `number` is the clue number as text (like `'1'`), * and `answer` is the answer itself * * @deprecated 4.3.0; being replaced by `onAnswerCorrect` (to reduce * ambiguity) */ onCorrect: PropTypes.func, /** * callback function that fires when a player answers a clue *in*correctly; * called with `(direction, number, answer)` arguments, where `direction` is * `'across'` or `'down'`, `number` is the clue number as text (like `'1'`), * and `answer` is the (actual and correct) answer itself * * @since 4.3.0 */ onAnswerIncorrect: PropTypes.func, /** * callback function that's called when a crossword is loaded, to batch up * correct answers loaded from storage; passed an array of the same values * that `onCorrect` would recieve */ onLoadedCorrect: PropTypes.func, /** * callback function that's called when the overall crossword is complete, * whether correct or not; called with `(correct)` argument, a boolean which * indicates whether the crossword is correct or not. */ onCrosswordComplete: PropTypes.func, /** * callback function that's called when the overall crossword is completely * correct (or not) * * NOTE: this will be deprecated for `onCrosswordComplete` in the future. */ onCrosswordCorrect: PropTypes.func, /** * callback function called when a cell changes (e.g. when the user types a * letter); called with `(row, col, char)` arguments, where the `row` and * `column` are the 0-based position of the cell, and `char` is the character * typed (already massaged into upper-case) */ onCellChange: PropTypes.func, /** * callback function called when a cell changes (e.g. when the user tap cell); called with `(row, col)` arguments, where the `row` and * `column` are the 0-based position of the cell, and `char` is the character * typed (already massaged into upper-case) */ onCellFocus: PropTypes.func, /** * callback function called when a clue is selected */ onClueSelected: PropTypes.func, children: PropTypes.node, }; export type CrosswordProviderProps = EnhancedProps< typeof crosswordProviderPropTypes, { /** * clue/answer data; see Clue * input format for details. */ data: CluesInput; /** * callback function that fires when a player completes an answer, whether * correct or not; called with `(direction, number, correct, answer)` * arguments, where `direction` is `'across'` or `'down'`, `number` is the * clue number as text (like `'1'`), `correct` is whether the guessed answer * is correct and `answer` is the (actual and correct) answer itself * * @since 4.3.0 */ onAnswerComplete?: ( direction: Direction, number: string, correct: boolean, answer: string ) => void; /** * callback function that fires when a player answers a clue correctly; * called with `(direction, number, answer)` arguments, where `direction` is * `'across'` or `'down'`, `number` is the clue number as text (like `'1'`), * and `answer` is the answer itself * * @since 4.3.0; replacing `onCorrect` (to reduce ambiguity) */ onAnswerCorrect?: ( direction: Direction, number: string, answer: string ) => void; /** * callback function that fires when a player answers a clue correctly; * called with `(direction, number, answer)` arguments, where `direction` is * `'across'` or `'down'`, `number` is the clue number as text (like `'1'`), * and `answer` is the answer itself * * NOTE: this is the original/previous name for what is now being called * `onAnswerCorrect` (to reduce ambiguity). It will be deprecated in the * future. */ onCorrect?: (direction: Direction, number: string, answer: string) => void; /** * callback function that fires when a player answers a clue *in*correctly; * called with `(direction, number, answer)` arguments, where `direction` is * `'across'` or `'down'`, `number` is the clue number as text (like `'1'`), * and `answer` is the (actual and correct) answer itself * * @since 4.3.0 */ onAnswerIncorrect?: ( direction: Direction, number: string, answer: string ) => void; /** * callback function that's called when a crossword is loaded, to batch up * correct answers loaded from storage; passed an array of the same values * that `onCorrect` would recieve */ onLoadedCorrect?: (loaded: AnswerTuple[]) => void; /** * callback function that's called when the overall crossword is complete, * whether correct or not; called with `(correct)` argument, a boolean which * indicates whether the crossword is correct or not. */ onCrosswordComplete?: (correct: boolean) => void; /** * callback function that's called when the overall crossword is completely * correct (or not) * * NOTE: this will be deprecated for `onCrosswordComplete` in the future. */ onCrosswordCorrect?: (isCorrect: boolean) => void; /** * callback function called when a cell changes (e.g. when the user types a * letter); called with `(row, col, char)` arguments, where the `row` and * `column` are the 0-based position of the cell, and `char` is the * character typed (already massaged into upper-case) */ onCellChange?: (row: number, col: number, char: string) => void; /** * callback function called when a cell changes (e.g. when the user tap cell); called with `(row, col)` arguments, where the `row` and * `column` are the 0-based position of the cell, and `char` is the character * typed (already massaged into upper-case) */ onCellFocus?: (selectedCell: any) => void; /** * callback function called when a clue is selected */ onClueSelected?: (direction: Direction, number: string) => void; } >; export interface CrosswordProviderImperative { /** * Sets focus to the crossword component. */ focus: () => void; /** * Resets the entire crossword; clearing all answers in the grid and * also any persisted data. */ reset: () => void; /** * Fills all the answers in the grid and calls the `onLoadedCorrect` * callback with _**every**_ answer. */ fillAllAnswers: () => void; /** * Returns whether the crossword is entirely correct or not. */ isCrosswordCorrect: () => boolean; /** * Sets the “guess” character for a specific grid position. * * @since 4.1.0 */ setGuess: (row: number, col: number, guess: string) => void; } const defaultTheme: CrosswordProviderProps['theme'] = { allowNonSquare: false, columnBreakpoint: '768px', gridBackground: 'rgb(0,0,0)', cellBackground: 'rgb(255,255,255)', cellBorder: 'rgb(0,0,0)', textColor: 'rgb(0,0,0)', numberColor: 'rgba(0,0,0, 0.25)', focusBackground: 'rgb(255,255,0)', highlightBackground: 'rgb(255,255,204)', }; /** * The fundamental logic and data management component for react-crossword. * Prior to 4.0, puzzle management was built into the `Crossword` component. As * of 4.0, the logic implementation has been refactored such that `Crossword` * leverages `CrosswordProvider` to do the heavy lifting. * * @since 4.0 */ const CrosswordProvider = React.forwardRef< CrosswordProviderImperative, CrosswordProviderProps >( ( { data, theme, onAnswerComplete, onAnswerCorrect, onCorrect, onAnswerIncorrect, onLoadedCorrect, onCrosswordComplete, onCrosswordCorrect, onCellChange, onCellFocus, onClueSelected, useStorage, storageKey, children, }, ref ) => { const contextTheme = useContext(ThemeContext); // The final theme is the merger of three values: the "theme" property // passed to the component (which takes precedence), any values from // ThemeContext, and finally the "defaultTheme" values fill in for any // needed ones that are missing. (We create this in standard last-one-wins // order in Javascript, of course.) const finalTheme = useMemo( () => ({ ...defaultTheme, ...contextTheme, ...theme }), [contextTheme, theme] ); // The original Crossword implementation used separate state to track size // and grid data, and conflated the clues-input-data-based grid data and the // player input guesses. Let's see if we can keep the clues-input and // player data segregated. const { rows, cols, gridData: masterGridData, clues: masterClues, } = useMemo( () => createGridData(data, finalTheme.allowNonSquare ?? false), [data, finalTheme.allowNonSquare] ); const [gridData, setGridData] = useState([]); const [clues, setClues] = useState(); // We can't seem to use state to track the registeredFocusHandler, because // there seems to be a delay in 'focus' being usable after it's set. We use // a Ref instead. const registeredFocusHandler = useRef(null); // interactive player state const [focused, setFocused] = useState(false); const [focusedRow, setFocusedRow] = useState(0); // rename to selectedRow? const [focusedCol, setFocusedCol] = useState(0); const [currentDirection, setCurrentDirection] = useState('across'); const [currentNumber, setCurrentNumber] = useState('1'); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [bulkChange, setBulkChange] = useState(null); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [checkQueue, setCheckQueue] = useState([]); // This *internal* getCellData assumes that it's only ever asked for a valid // cell (one that's used). const getCellData = useCallback( (row: number, col: number) => { if (row >= 0 && row < rows && col >= 0 && col < cols) { return gridData[row][col]; } // fake cellData to represent "out of bounds" return { row, col, used: false, outOfBounds: true } as GridPosition & UnusedCellData; }, [cols, gridData, rows] ); const setCellCharacter = useCallback( (row: number, col: number, char: string) => { const cell = getCellData(row, col); if (!cell.used) { throw new Error('unexpected setCellCharacter call'); } // If the character is already the cell's guess, there's nothing to do. // if (cell.guess === char) { // return; // } // update the gridData with the guess setGridData( produce((draft) => { (draft[row][col] as UsedCellData).guess = char; }) ); // push the row/col for checking! setCheckQueue( produce((draft) => { draft.push({ row, col }); }) ); if (onCellChange) { onCellChange(row, col, char); } }, [getCellData, onCellChange] ); const notifyAnswerComplete = useCallback( ( direction: Direction, number: string, correct: boolean, answer: string ) => { if (onAnswerComplete) { onAnswerComplete(direction, number, correct, answer); } if (correct) { if (onAnswerCorrect) { onAnswerCorrect(direction, number, answer); } // NOTE: onCorrect to be (eventually) deprecated if (onCorrect) { onCorrect(direction, number, answer); } } else if (onAnswerIncorrect) { onAnswerIncorrect(direction, number, answer); } }, [onAnswerComplete, onAnswerCorrect, onAnswerIncorrect, onCorrect] ); const checkCorrectness = useCallback( (row: number, col: number) => { const cell = getCellData(row, col); if (!cell.used) { // Because this is in an internal callback, and we only call it with a // valid cell (row/col), the throw line isn't testable... so we ignore // it. /* istanbul ignore next */ throw new Error('unexpected unused cell'); } // check all the cells for both across and down answers that use this // cell bothDirections.forEach((direction) => { const across = isAcross(direction); const number = cell[direction]; if (!number) { return; } const info = data[direction][number]; // We send correct/incorrect messages, but *only* if every cell in the // answer is filled out; there's no point in reporting "incorrect" // when the answer is simply incomplete. let complete = true; let correct = true; for (let i = 0; i < info.answer.length; i++) { const checkCell = getCellData( info.row + (across ? 0 : i), info.col + (across ? i : 0) ) as UsedCellData; if (!checkCell.guess) { complete = false; correct = false; break; } if (checkCell.guess !== checkCell.answer) { correct = false; } } // update the clue state setClues( produce((draft) => { if (draft) { const clueInfo = draft[direction].find( (i) => i.number === number ); if (clueInfo) { clueInfo.complete = complete; clueInfo.correct = correct; } } }) ); if (complete) { notifyAnswerComplete(direction, number, correct, info.answer); } }); }, [data, getCellData, notifyAnswerComplete] ); // Any time the checkQueue changes, call checkCorrectness! useEffect(() => { if (checkQueue.length === 0) { return; } checkQueue.forEach(({ row, col }) => checkCorrectness(row, col)); setCheckQueue([]); }, [checkQueue, checkCorrectness]); // Any time the clues change, determine if they are all complete/correct or // not. const { crosswordComplete, crosswordCorrect } = useMemo(() => { const complete = !!( clues && bothDirections.every((direction) => clues[direction].every((clueInfo) => clueInfo.complete) ) ); const correct = complete && !!( clues && bothDirections.every((direction) => clues[direction].every((clueInfo) => clueInfo.correct) ) ); // console.log('setting crossword correct', { clues, correct }); return { crosswordComplete: complete, crosswordCorrect: correct }; }, [clues]); // Let the consumer know everything's correct (or not) if they've asked to // be informed. useEffect(() => { if (crosswordComplete) { if (onCrosswordComplete) { onCrosswordComplete(crosswordCorrect); } if (onCrosswordCorrect) { onCrosswordCorrect(crosswordCorrect); } } }, [ crosswordComplete, crosswordCorrect, onCrosswordComplete, onCrosswordCorrect, ]); // focus and movement const focus = useCallback(() => { // console.log('CrosswordProvider.focus() called...'); // If there's a registered focus handler, use it! if (registeredFocusHandler.current) { // console.log('calling registered focus handler...'); registeredFocusHandler.current(); setFocused(true); } else { console.warn( 'CrosswordProvider: focus() has no registered handler to call!' ); } }, []); const moveTo = useCallback( (row: number, col: number, directionOverride?: Direction) => { let direction = directionOverride ?? currentDirection; const candidate = getCellData(row, col); if (!candidate.used) { return false; } // If we try to move to a cell with a direction it doesn't support, // switch to the other direction. There is no codepath that can test // this, though, as this callback isn't exposed, and we only call it in // ways that guarantee that direction is valid. if (!candidate[direction]) { /* istanbul ignore next */ direction = otherDirection(direction); } setFocusedRow(row); setFocusedCol(col); setCurrentDirection(direction); setCurrentNumber(candidate[direction] ?? ''); return candidate; }, [currentDirection, getCellData] ); const moveRelative = useCallback( (dRow: number, dCol: number) => { // We expect *only* one of dRow or dCol to have a non-zero value, and // that's the direction we will "prefer". If *both* are set (or zero), // we don't change the direction. let direction: Direction | undefined; if (dRow !== 0 && dCol === 0) { direction = 'down'; } else if (dRow === 0 && dCol !== 0) { direction = 'across'; } const cell = moveTo(focusedRow + dRow, focusedCol + dCol, direction); return cell; }, [focusedRow, focusedCol, moveTo] ); const moveForward = useCallback(() => { const across = isAcross(currentDirection); moveRelative(across ? 0 : 1, across ? 1 : 0); }, [currentDirection, moveRelative]); const moveBackward = useCallback(() => { const across = isAcross(currentDirection); moveRelative(across ? 0 : -1, across ? -1 : 0); }, [currentDirection, moveRelative]); // keyboard handling const handleSingleCharacter = useCallback( (char: string) => { setCellCharacter(focusedRow, focusedCol, char.toUpperCase()); moveForward(); }, [focusedRow, focusedCol, setCellCharacter, moveForward] ); // We use the keydown event for control/arrow keys, but not for textual // input, because it's hard to suss out when a key is "regular" or not. const handleInputKeyDown = useCallback< React.KeyboardEventHandler >( (event) => { // if ctrl, alt, or meta are down, ignore the event (let it bubble) if (event.ctrlKey || event.altKey || event.metaKey) { return; } let preventDefault = true; const { key } = event; // console.log('CROSSWORD KEYDOWN', event.key); // FUTURE: should we "jump" over black space? That might help some for // keyboard users. switch (key) { case 'ArrowUp': moveRelative(-1, 0); break; case 'ArrowDown': moveRelative(1, 0); break; case 'ArrowLeft': moveRelative(0, -1); break; case 'ArrowRight': moveRelative(0, 1); break; case ' ': // treat space like tab? case 'Tab': { const other = otherDirection(currentDirection); const cellData = getCellData( focusedRow, focusedCol ) as UsedCellData; if (cellData[other]) { setCurrentDirection(other); setCurrentNumber(cellData[other] ?? ''); } break; } // Backspace: delete the current cell, and move to the previous cell // Delete: delete the current cell, but don't move case 'Backspace': case 'Delete': { setCellCharacter(focusedRow, focusedCol, ''); if (key === 'Backspace') { moveBackward(); } break; } case 'Home': case 'End': { // move to beginning/end of this entry? const info = data[currentDirection][currentNumber]; const { answer: { length }, } = info; let { row, col } = info; if (key === 'End') { const across = isAcross(currentDirection); if (across) { col += length - 1; } else { row += length - 1; } } moveTo(row, col); break; } default: // It would be nice to handle "regular" characters with onInput, but // that is still experimental, so we can't count on it. Instead, we // assume that only "length 1" values are regular. if (key.length !== 1) { preventDefault = false; break; } handleSingleCharacter(key); break; } if (preventDefault) { event.preventDefault(); } }, [ moveRelative, handleSingleCharacter, currentDirection, getCellData, focusedRow, focusedCol, setCellCharacter, moveBackward, data, currentNumber, moveTo, ] ); const handleInputChange = useCallback< React.ChangeEventHandler >((event) => { event.preventDefault(); setBulkChange(event.target.value); }, []); useEffect(() => { if (!bulkChange) { return; } // handle bulkChange by updating a character at a time (this lets us // leverage the existing character-entry logic). handleSingleCharacter(bulkChange[0]); setBulkChange(bulkChange.length === 1 ? null : bulkChange.substring(1)); }, [bulkChange, handleSingleCharacter]); // When the clues *input* data changes, reset/reload the player data useEffect(() => { // deep-clone the grid data... const newGridData = masterGridData.map((row) => row.map((cell) => ({ ...cell })) ); // deep-clone the clue data... const newCluesData: CluesData = { across: masterClues.across.map((clue) => ({ ...clue })), down: masterClues.down.map((clue) => ({ ...clue })), }; if (useStorage) { loadGuesses(newGridData, storageKey || defaultStorageKey); } setClues(newCluesData); setGridData(newGridData); // Check all of the clues to see if any were correct... but only if we // loaded guesses. Since the current implementation relies on state, we // leverage the checkQueue to run through all the clues/guesses. // // Really, the ideal thing to do would be to write the checking-logic in a // way that it doesn't assume the data is already in state... that would // allow us to check everything directly, and simply set the same state // that checkCorrectness() does, *and* properly call onLoadedCorrect(). As // it is, this implementation can cause some answers to mentioned in // onCorrect() more than once (any time an across answer starts inside a // down answer, or vice versa.) if (useStorage) { setCheckQueue( bothDirections.flatMap((dir) => // simply use the row/col that starts each answer. newCluesData[dir].map(({ row, col }) => ({ row, col })) ) ); } // Should we start with 1-across highlighted/focused? // TODO: track input-field focus so we don't draw highlight when we're not // really focused, *and* use first actual clue (whether across or down?) setFocusedRow(0); setFocusedCol(0); setCurrentDirection('across'); setCurrentNumber('1'); }, [masterClues, masterGridData, storageKey, useStorage]); // save the guesses any time they change... useEffect(() => { if (gridData === null || !useStorage) { return; } saveGuesses(gridData, storageKey || defaultStorageKey); }, [gridData, storageKey, useStorage]); const handleCellClick = useCallback( (cellData: CellData) => { if (cellData.used) { const { row, col } = cellData; const other = otherDirection(currentDirection); // should this use moveTo? setFocusedRow(row); setFocusedCol(col); let direction = currentDirection; const cellDataAll = { ...data.across, ...data.down }; const cellAccross = cellData.across ?? ''; const cellDown = cellData.down ?? ''; const selectedCell = cellDataAll[cellAccross || cellDown]; // CHORE: when across and down it is not empty, then the default is across, make sure first? // We switch to the "other" direction if (a) the current direction // isn't available in the clicked cell, or (b) we're already focused // and the clicked cell is the focused cell, *and* the other direction // is available. if ( !cellData[currentDirection] || (focused && row === focusedRow && col === focusedCol && cellData[other]) ) { setCurrentDirection(other); direction = other; } setCurrentNumber(cellData[direction] ?? ''); if (onCellFocus) { onCellFocus({ ...selectedCell, currentRow: row, currentCol: col, }); } } focus(); }, [currentDirection, focus, focused, focusedCol, focusedRow] ); const handleInputClick = useCallback< React.MouseEventHandler >( (/* event */) => { // *don't* event.preventDefault(), because we want the input to actually // take focus // Like general cell-clicks, cliking on the input can change direction. // Unlike cell clicks, we *know* we're clicking on the already-focused // cell! const other = otherDirection(currentDirection); const cellData = getCellData(focusedRow, focusedCol) as UsedCellData; let direction = currentDirection; if (focused && cellData[other]) { setCurrentDirection(other); direction = other; } setCurrentNumber(cellData[direction] ?? ''); focus(); }, [currentDirection, focus, focused, focusedCol, focusedRow, getCellData] ); const handleClueSelected = useCallback( (direction: Direction, number: string) => { const info = clues?.[direction].find((clue) => clue.number === number); if (!info) { return; } // console.log('CrosswordProvider.handleClueSelected', { info }); // TODO: sanity-check info? moveTo(info.row, info.col, direction); focus(); if (onClueSelected) { onClueSelected(direction, number); } }, [clues, focus, moveTo, onClueSelected] ); const registerFocusHandler = useCallback( (focusHandler: FocusHandler | null) => { // console.log('CrosswordProvider.registerFocusHandler() called', { // name: focusHandler?.name ?? '(NULL)', // focusHandler, // }); // *If* registeredFocusHandler is implemented as state, realize that we // can't simply pass it to the setter... the useState React setter would // *invoke* the function and take the return value! So, we would have // to wrap it in a functional setter (setState(() => focusHandler)). // But, since we're using a Ref, this is just a simple assignment! registeredFocusHandler.current = focusHandler; }, [] ); // imperative commands... useImperativeHandle( ref, () => ({ /** * Sets focus to the crossword component. */ focus, /** * Resets the entire crossword; clearing all answers in the grid and * also any persisted data. */ reset: () => { setGridData( produce((draft) => { draft.forEach((rowData) => { rowData.forEach((cellData) => { if (cellData.used) { cellData.guess = ''; } }); }); }) ); setClues( produce((draft) => { bothDirections.forEach((direction) => { draft?.[direction]?.forEach((clueInfo) => { delete clueInfo.complete; delete clueInfo.correct; }); }); }) ); if (useStorage) { clearGuesses(storageKey || defaultStorageKey); } }, /** * Fills all the answers in the grid and calls the `onLoadedCorrect` * callback with _**every**_ answer. */ fillAllAnswers: () => { setGridData( produce((draft) => { draft.forEach((rowData) => { rowData.forEach((cellData) => { if (cellData.used) { cellData.guess = cellData.answer; } }); }); }) ); setClues( produce((draft) => { bothDirections.forEach((direction) => { draft?.[direction].forEach((clueInfo) => { clueInfo.complete = true; clueInfo.correct = true; }); }); }) ); // trigger onLoadedCorrect with every clue! if (onLoadedCorrect) { const loadedCorrect: AnswerTuple[] = []; bothDirections.forEach((direction) => { clues?.[direction].forEach(({ number, answer }) => { loadedCorrect.push([direction, number, answer]); }); }); onLoadedCorrect(loadedCorrect); } }, /** * Returns whether the crossword is entirely correct or not. */ isCrosswordCorrect: () => crosswordCorrect, /** * Sets the “guess” character for a specific grid position. * * @since 4.1.0 */ setGuess: (row: number, col: number, guess: string) => { // REVIEW: should we force-case this? setCellCharacter(row, col, guess.toUpperCase()); }, }), [ clues, crosswordCorrect, focus, onLoadedCorrect, setCellCharacter, storageKey, useStorage, ] ); const crosswordContext = useMemo( () => ({ rows, cols, gridData, clues, handleInputKeyDown, handleInputChange, handleCellClick, handleInputClick, handleClueSelected, registerFocusHandler, focused, selectedPosition: { row: focusedRow, col: focusedCol }, selectedDirection: currentDirection, selectedNumber: currentNumber, crosswordCorrect, }), [ rows, cols, gridData, clues, handleInputKeyDown, handleInputChange, handleCellClick, handleInputClick, handleClueSelected, registerFocusHandler, focused, focusedRow, focusedCol, currentDirection, currentNumber, crosswordCorrect, ] ); return ( {children} ); } ); export default CrosswordProvider; CrosswordProvider.displayName = 'CrosswordProvider'; CrosswordProvider.propTypes = crosswordProviderPropTypes; CrosswordProvider.defaultProps = { theme: undefined, useStorage: true, storageKey: undefined, onAnswerComplete: undefined, onAnswerCorrect: undefined, onCorrect: undefined, onAnswerIncorrect: undefined, onLoadedCorrect: undefined, onCrosswordComplete: undefined, onCrosswordCorrect: undefined, onCellChange: undefined, onCellFocus: undefined, onClueSelected: undefined, children: undefined, };