Skip to content

Commit

Permalink
use input to capture key press on android (#1853)
Browse files Browse the repository at this point in the history
* Add input to grid so that when a cell is selected the keyboard appears for andriod, ios and on desktop
  • Loading branch information
oliverabrahams authored Dec 17, 2024
1 parent 568cfc0 commit 79cbb46
Showing 1 changed file with 80 additions and 62 deletions.
142 changes: 80 additions & 62 deletions libs/@guardian/react-crossword/src/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<SVGSVGElement>(null);
// do not call focus() on this element as it will trigger the selection menu on safari
const gridWrapperRef = useRef<HTMLDivElement>(null);
const workingDirectionRef = useRef<Direction>('across');
const inputRef = useRef<HTMLInputElement>(null);

const [cheatMode, cheatStyles] = useCheatMode(gridRef);

Expand Down Expand Up @@ -166,12 +168,47 @@ export const Grid = () => {
return;
}, []);

const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>): void => {
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}

if (!currentCell) {
return;
}
Expand Down Expand Up @@ -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<HTMLDivElement>) => {
// 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.
Expand Down Expand Up @@ -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);
Expand All @@ -387,16 +376,17 @@ export const Grid = () => {

return (
<div
contentEditable={true}
suppressContentEditableWarning={true}
ref={gridWrapperRef}
css={css`
position: relative;
cursor: pointer;
caret-color: transparent;
width: 100%;
max-width: ${width}px;
max-height: ${height}px;
// This is to prevent the default blue highlight on click on andriod
-webkit-tap-highlight-color: transparent;
`}
onClick={selectClickedCell}
tabIndex={-1}
>
<svg
Expand All @@ -406,6 +396,7 @@ export const Grid = () => {
`,
cheatStyles,
]}
id={getId('crossword-grid')}
ref={gridRef}
viewBox={`0 0 ${width} ${height}`}
tabIndex={-1}
Expand Down Expand Up @@ -453,8 +444,35 @@ export const Grid = () => {
/>
))
}
{currentCell && <FocusIndicator currentCell={currentCell} />}
{currentCell && document.activeElement?.id === inputRef.current?.id && (
<FocusIndicator currentCell={currentCell} />
)}
</svg>
<input
ref={inputRef}
value={inputValue}
autoCapitalize={'characters'}
id={getId('overlay-input')}
type="text"
pattern={'^[A-Za-zÀ-ÿ0-9]$'}
onKeyDown={handleKeyDown}
onChange={handleChange}
tabIndex={0}
css={css`
position: absolute;
pointer-events: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
`}
autoComplete="off"
spellCheck="false"
autoCorrect="off"
aria-hidden="false"
aria-label={`Type letter for crossword cell x ${currentCell?.x}, y ${currentCell?.y}`}
/>
</div>
);
};

0 comments on commit 79cbb46

Please sign in to comment.