Skip to content

Commit

Permalink
Add check to cell and a display to show if the cell is correct (#1941)
Browse files Browse the repository at this point in the history
* Add check to cell and a display to show if the cell is correct with a strike through if not correct
  • Loading branch information
oliverabrahams authored Feb 12, 2025
1 parent 5b0294e commit f846b5c
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 44 deletions.
2 changes: 2 additions & 0 deletions libs/@guardian/react-crossword/src/@types/crossword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export type Theme = {
gridGutterSize: number;
/** The length of one side of a cell on on the grid */
gridCellSize: number;
/** The colour of the strike through on an incorrect Cell*/
gridCellStrikeThrough: string;

/** The main text colour (grid text, clues etc) */
textColor: string;
Expand Down
11 changes: 11 additions & 0 deletions libs/@guardian/react-crossword/src/components/Cell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ export const WithNumber: Story = {
},
};

export const WithIncorrect: Story = {
args: {
...args,
isIncorrect: true,
data: {
...args.data,
number: 1,
},
},
};

export const Progress: Story = {
args: {
...args,
Expand Down
18 changes: 17 additions & 1 deletion libs/@guardian/react-crossword/src/components/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type BaseCellProps = {
isConnected?: boolean;
/** is the cell for the selected clue? */
isSelected?: boolean;
/** is the cell checked and incorrect */
isIncorrect?: boolean;
/** is the cell the current cell? */
isCurrentCell?: boolean;
/** callback for keydown event */
Expand All @@ -34,6 +36,7 @@ const CellComponent = ({
guess = '',
isBlackCell,
isConnected,
isIncorrect = false,
isSelected,
isCurrentCell,
handleKeyDown,
Expand All @@ -43,6 +46,9 @@ const CellComponent = ({
const theme = useTheme();
const { getId } = useData();
const cellRef = useRef<null | SVGGElement>(null);
const cellDescription = data.description
? (isIncorrect ? 'Incorrect letter. ' : '') + data.description
: '';

const backgroundColor = isBlackCell
? 'transparent'
Expand Down Expand Up @@ -82,6 +88,16 @@ const CellComponent = ({
role="presentation"
css={cellStyles}
/>
{isIncorrect && (
<line
x1={x}
y1={y}
x2={x + theme.gridCellSize}
y2={y + theme.gridCellSize}
strokeWidth="2"
stroke={theme.gridCellStrikeThrough}
/>
)}
{!isBlackCell && (
<>
{data.number && (
Expand Down Expand Up @@ -120,7 +136,7 @@ const CellComponent = ({
onInput={handleInput}
tabIndex={isCurrentCell ? 0 : -1}
aria-label="Crossword cell"
aria-description={data.description ?? ''}
aria-description={cellDescription}
css={css`
width: 100%;
height: 100%;
Expand Down
63 changes: 27 additions & 36 deletions libs/@guardian/react-crossword/src/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isUndefined } from '@guardian/libs';
import { space } from '@guardian/source/foundations';
import type { ButtonProps } from '@guardian/source/react-components';
import { cloneElement, useCallback, useEffect, useRef, useState } from 'react';
import type { Cell, Progress } from '../@types/crossword';
import type { Progress } from '../@types/crossword';
import type { EntryID } from '../@types/Entry';
import { useCurrentClue } from '../context/CurrentClue';
import { useData } from '../context/Data';
Expand Down Expand Up @@ -70,33 +70,29 @@ const ClearClue = (props: ButtonProps) => {
const CheckClue = (props: ButtonProps) => {
const { cells } = useData();
const { progress } = useProgress();
const { updateCell } = useUpdateCell();
const { setValidAnswers } = useValidAnswers();
const { setValidAnswers, setInvalidCellAnswers } = useValidAnswers();
const { currentEntryId } = useCurrentClue();

const checkCell = useCallback(
(cell: Cell) => {
const currentProgress = progress[cell.x]?.[cell.y];
return !!(currentProgress && currentProgress === cell.solution);
},
[progress],
);

const check = useCallback(() => {
if (!currentEntryId) {
return;
}
let entryIsCorrect = true;
for (const cell of cells.values()) {
if (cell.group?.includes(currentEntryId)) {
if (!checkCell(cell)) {
updateCell({
x: cell.x,
y: cell.y,
value: '',
const currentProgress = progress[cell.x]?.[cell.y];
if (
cell.group?.includes(currentEntryId) &&
currentProgress &&
currentProgress !== cell.solution
) {
if (currentProgress !== '') {
setInvalidCellAnswers((prevState) => {
const newInvalidCellAnswers = new Set(prevState);
newInvalidCellAnswers.add(`x${cell.x}y${cell.y}`);
return newInvalidCellAnswers;
});
entryIsCorrect = false;
}
entryIsCorrect = false;
}
}
if (entryIsCorrect) {
Expand All @@ -105,7 +101,7 @@ const CheckClue = (props: ButtonProps) => {
return newValidAnswers.add(currentEntryId);
});
}
}, [cells, checkCell, currentEntryId, updateCell, setValidAnswers]);
}, [currentEntryId, cells, progress, setInvalidCellAnswers, setValidAnswers]);

return (
<ClueButton onClick={check} data-link-name="Check this" {...props}>
Expand Down Expand Up @@ -158,30 +154,25 @@ const AnagramHelper = (props: ButtonProps) => {
const CheckGrid = (props: ButtonProps) => {
const { progress } = useProgress();
const { cells, entries } = useData();
const { updateCell } = useUpdateCell();
const { setValidAnswers } = useValidAnswers();

const checkCell = useCallback(
(cell: Cell) => {
const currentProgress = progress[cell.x]?.[cell.y];
return !!(currentProgress && currentProgress === cell.solution);
},
[progress],
);
const { setValidAnswers, setInvalidCellAnswers } = useValidAnswers();

const check = useCallback(() => {
const allEntries = entries.keys();
const invalidAnswers: Set<EntryID> = new Set();
for (const cell of cells.values()) {
const currentProgress = progress[cell.x]?.[cell.y];
if (!cell.group) {
continue;
}
if (!checkCell(cell)) {
updateCell({
x: cell.x,
y: cell.y,
value: '',
});

if (!isUndefined(currentProgress) && currentProgress !== cell.solution) {
if (currentProgress !== '') {
setInvalidCellAnswers((prevState) => {
const newInvalidCellAnswers = new Set(prevState);
newInvalidCellAnswers.add(`x${cell.x}y${cell.y}`);
return newInvalidCellAnswers;
});
}
for (const entryId of cell.group) {
invalidAnswers.add(entryId);
}
Expand All @@ -191,7 +182,7 @@ const CheckGrid = (props: ButtonProps) => {
[...allEntries].filter((x) => !invalidAnswers.has(x)),
);
setValidAnswers(validAnswers);
}, [cells, checkCell, entries, updateCell, setValidAnswers]);
}, [entries, setValidAnswers, cells, progress, setInvalidCellAnswers]);

return (
<CrosswordButton onClick={check} data-link-name="Check all" {...props}>
Expand Down
3 changes: 3 additions & 0 deletions libs/@guardian/react-crossword/src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useCurrentClue } from '../context/CurrentClue';
import { useData } from '../context/Data';
import { useProgress } from '../context/Progress';
import { useTheme } from '../context/Theme';
import { useValidAnswers } from '../context/ValidAnswers';
import { useCheatMode } from '../hooks/useCheatMode';
import { useUpdateCell } from '../hooks/useUpdateCell';
import { keyDownRegex } from '../utils/keydownRegex';
Expand Down Expand Up @@ -125,6 +126,7 @@ const FocusIndicator = ({

export const Grid = () => {
const theme = useTheme();
const { invalidCellAnswers } = useValidAnswers();
const { cells, separators, entries, dimensions, getId } = useData();
const { progress } = useProgress();
const { updateCell } = useUpdateCell();
Expand Down Expand Up @@ -503,6 +505,7 @@ export const Grid = () => {
isConnected={isConnected}
isBlackCell={isBlackCell}
isCurrentCell={isCurrentCell}
isIncorrect={invalidCellAnswers.has(`x${cell.x}y${cell.y}`)}
role="cell"
data-x={cell.x}
data-y={cell.y}
Expand Down
14 changes: 13 additions & 1 deletion libs/@guardian/react-crossword/src/context/ValidAnswers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { createContext, type ReactNode, useContext, useState } from 'react';
import type { EntryID } from '../@types/Entry';

type ValidAnswers = Set<EntryID>;
type InvalidCellAnswers = Set<`x${number}y${number}`>;

type ValidAnswersContext = {
validAnswers: ValidAnswers;
invalidCellAnswers: InvalidCellAnswers;
setValidAnswers: Dispatch<SetStateAction<ValidAnswers>>;
setInvalidCellAnswers: Dispatch<SetStateAction<InvalidCellAnswers>>;
};

const validAnswersContext = createContext<ValidAnswersContext | undefined>(
Expand All @@ -20,12 +23,21 @@ export const ValidAnswersProvider = ({
validAnswers?: ValidAnswers;
children: ReactNode;
}) => {
const [invalidCellAnswers, setInvalidCellAnswers] =
useState<InvalidCellAnswers>(new Set());
const [validAnswers, setValidAnswers] = useState<ValidAnswers>(
userValidAnswers ?? new Set(),
);

return (
<validAnswersContext.Provider value={{ validAnswers, setValidAnswers }}>
<validAnswersContext.Provider
value={{
validAnswers,
setValidAnswers,
invalidCellAnswers,
setInvalidCellAnswers,
}}
>
{children}
</validAnswersContext.Provider>
);
Expand Down
5 changes: 3 additions & 2 deletions libs/@guardian/react-crossword/src/hooks/useClearUserInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { getNewProgress } from '../utils/getNewProgress';
export const useClearUserInput = () => {
const { updateProgress } = useProgress();
const { dimensions } = useData();
const { setValidAnswers } = useValidAnswers();
const { setValidAnswers, setInvalidCellAnswers } = useValidAnswers();

const clearUserInput = useCallback(() => {
updateProgress(getNewProgress(dimensions));
setValidAnswers(new Set());
}, [updateProgress, setValidAnswers, dimensions]);
setInvalidCellAnswers(new Set());
}, [dimensions, setInvalidCellAnswers, setValidAnswers, updateProgress]);
return { clearUserInput };
};
24 changes: 20 additions & 4 deletions libs/@guardian/react-crossword/src/hooks/useUpdateCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { useProgress } from '../context/Progress';
import { useValidAnswers } from '../context/ValidAnswers';

export const useUpdateCell = () => {
const { updateProgress, progress } = useProgress();
const { setValidAnswers } = useValidAnswers();
const { progress, updateProgress } = useProgress();
const { setValidAnswers, invalidCellAnswers, setInvalidCellAnswers } =
useValidAnswers();

const { cells } = useData();

const updateCell = useCallback(
({ x, y, value }: Coords & { value: string }) => {
const cell = cells.getByCoords({ x, y });
const cellGroup = cell?.group;

// blank cells have no group
if (isUndefined(cellGroup)) {
return;
Expand All @@ -31,6 +32,14 @@ export const useUpdateCell = () => {
newProgress[x][y] = value;
updateProgress(newProgress);

if (invalidCellAnswers.has(`x${x}y${y}`)) {
setInvalidCellAnswers((prevState) => {
const newInvalidCellAnswers = new Set(prevState);
newInvalidCellAnswers.delete(`x${x}y${y}`);
return newInvalidCellAnswers;
});
}

setValidAnswers((prev) => {
const newSet = new Set(prev);
for (const entryId of cellGroup) {
Expand All @@ -39,7 +48,14 @@ export const useUpdateCell = () => {
return newSet;
});
},
[cells, progress, updateProgress, setValidAnswers],
[
cells,
invalidCellAnswers,
progress,
setInvalidCellAnswers,
setValidAnswers,
updateProgress,
],
);
return { updateCell };
};
1 change: 1 addition & 0 deletions libs/@guardian/react-crossword/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const defaultTheme: Theme = {
gridPrintBackgroundColor: palette.neutral[46],
gridGutterSize: 1,
gridCellSize: 32,
gridCellStrikeThrough: palette.neutral[73],

textColor: palette.neutral[7],
focusColor: palette.focus[400],
Expand Down

0 comments on commit f846b5c

Please sign in to comment.