From 79cbb4626c8071d06baf6140b6b96b97ddbb3438 Mon Sep 17 00:00:00 2001 From: Oliver Abrahams Date: Tue, 17 Dec 2024 10:58:42 +0000 Subject: [PATCH] use input to capture key press on android (#1853) * Add input to grid so that when a cell is selected the keyboard appears for andriod, ios and on desktop --- .../react-crossword/src/components/Grid.tsx | 142 ++++++++++-------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/libs/@guardian/react-crossword/src/components/Grid.tsx b/libs/@guardian/react-crossword/src/components/Grid.tsx index 648d88955..7409fbb82 100644 --- a/libs/@guardian/react-crossword/src/components/Grid.tsx +++ b/libs/@guardian/react-crossword/src/components/Grid.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; -import { memo, useCallback, useEffect, useRef } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import type { ChangeEvent, KeyboardEvent, MouseEvent } from 'react'; import type { Coords, Separator, Theme } from '../@types/crossword'; import type { Direction } from '../@types/Direction'; import { useCurrentCell } from '../context/CurrentCell'; @@ -100,16 +101,17 @@ const FocusIndicator = ({ export const Grid = () => { const theme = useTheme(); - const { cells, separators, entries, dimensions } = useData(); + const { cells, separators, entries, dimensions, getId } = useData(); const { progress } = useProgress(); const { updateCell } = useUpdateCell(); const { currentCell, setCurrentCell } = useCurrentCell(); const { currentEntryId, setCurrentEntryId } = useCurrentClue(); + const [inputValue, setInputValue] = useState(''); const gridRef = useRef(null); - // do not call focus() on this element as it will trigger the selection menu on safari const gridWrapperRef = useRef(null); const workingDirectionRef = useRef('across'); + const inputRef = useRef(null); const [cheatMode, cheatStyles] = useCheatMode(gridRef); @@ -166,12 +168,47 @@ export const Grid = () => { return; }, []); + const handleChange = useCallback( + (event: ChangeEvent) => { + if (isUndefined(currentCell)) { + return; + } + const direction = currentEntryId?.includes('across') ? 'across' : 'down'; + const key = event.target.value.toUpperCase(); + const value = cheatMode + ? cells.getByCoords({ x: currentCell.x, y: currentCell.y })?.solution + : keyDownRegex.test(key) && key.toUpperCase(); + + if (value) { + // This mimics moving to a new input cell after typing a letter. + // This is needed for a quirk in the Android keyboard. + // It stores typed text even if it is cleared by react + // and the backspace key does not work as expected. + inputRef.current?.blur(); + inputRef.current?.focus(); + + 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 }); + } + } + setInputValue(''); + }, + [cells, cheatMode, currentCell, currentEntryId, moveFocus, updateCell], + ); + const handleKeyDown = useCallback( - (event: KeyboardEvent): void => { + (event: KeyboardEvent): void => { if (event.ctrlKey || event.altKey || event.metaKey) { return; } - if (!currentCell) { return; } @@ -217,50 +254,20 @@ export const Grid = () => { } break; } - default: { - 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; - } - } + default: + preventDefault = false; break; - } } if (preventDefault) { event.preventDefault(); } }, - [ - currentCell, - currentEntryId, - moveFocus, - handleTab, - updateCell, - cheatMode, - cells, - ], + [currentCell, currentEntryId, moveFocus, handleTab, updateCell], ); const selectClickedCell = useCallback( - (event: MouseEvent) => { + (event: MouseEvent) => { // The 'g' elements in the grid SVG are the cells, and we have set // data-x and data-y attributes on them to represent their position // in the grid. @@ -355,29 +362,11 @@ export const Grid = () => { // Set the new current cell and entry: setCurrentCell({ x: clickedCellX, y: clickedCellY }); setCurrentEntryId(newEntryId); + inputRef.current?.focus(); }, [cells, currentCell, currentEntryId, setCurrentCell, setCurrentEntryId], ); - useEffect(() => { - const preventDefault = (event: Event) => { - event.preventDefault(); - }; - - const gridWrapper = gridWrapperRef.current; - gridWrapper?.addEventListener('beforeinput', preventDefault); - gridWrapper?.addEventListener('click', selectClickedCell); - gridWrapper?.addEventListener('keydown', handleKeyDown); - gridWrapper?.addEventListener('selectstart', preventDefault); - - return () => { - gridWrapper?.removeEventListener('beforeinput', preventDefault); - gridWrapper?.removeEventListener('click', selectClickedCell); - gridWrapper?.removeEventListener('keydown', handleKeyDown); - gridWrapper?.removeEventListener('selectstart', preventDefault); - }; - }, [handleKeyDown, selectClickedCell]); - const height = theme.gridCellSize * dimensions.rows + theme.gridGutterSize * (dimensions.rows + 1); @@ -387,16 +376,17 @@ export const Grid = () => { return (
{ `, cheatStyles, ]} + id={getId('crossword-grid')} ref={gridRef} viewBox={`0 0 ${width} ${height}`} tabIndex={-1} @@ -453,8 +444,35 @@ export const Grid = () => { /> )) } - {currentCell && } + {currentCell && document.activeElement?.id === inputRef.current?.id && ( + + )} +
); };