Skip to content

Commit

Permalink
Merge a4c1d89 into f98e690
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverabrahams authored Jan 2, 2025
2 parents f98e690 + a4c1d89 commit be66a7a
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 85 deletions.
9 changes: 2 additions & 7 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"bumpVersionsWithWorkspaceProtocolOnly": false,
"ignore": [
"github-pages",
"@configs/*",
"coverage",
"storybooks",
"@guardian/react-crossword"
],
"useCalculatedVersion": true,
"ignore": ["github-pages", "@configs/*", "coverage", "storybooks"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
Expand Down
5 changes: 5 additions & 0 deletions .changeset/react-crozzword-next.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@guardian/react-crossword': major
---

to be completed...
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The following packages live in `libs/@guardian/*` and are published to NPM:
- [@guardian/libs](libs/@guardian/libs)
- [@guardian/newsletter-types](libs/@guardian/newsletter-types)
- [@guardian/prettier](libs/@guardian/prettier)
- [@guardian/react-crossword](libs/@guardian/react-crossword)
- [@guardian/source](libs/@guardian/source)
- [@guardian/source-development-kitchen](libs/@guardian/source-development-kitchen)
- [@guardian/tsconfig](libs/@guardian/tsconfig)
Expand Down
3 changes: 1 addition & 2 deletions libs/@guardian/react-crossword/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "@guardian/react-crossword",
"version": "3.0.0-alpha.0",
"private": true,
"version": "2.0.2",
"license": "Apache-2.0",
"sideEffects": false,
"type": "module",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { groupedClues as data } from '../../stories/formats/grouped-clues';
import { progress12Across } from '../../stories/formats/grouped-clues.progress';
import { ContextProvider } from '../context/ContextProvider';
import { ShowAnagramHelperProvider } from '../context/ShowAnagramHelper';
import { defaultTheme } from '../theme';
import { AnagramHelper } from './AnagramHelper';

Expand All @@ -16,7 +17,9 @@ const meta: Meta<typeof AnagramHelper> = {
selectedEntryId="12-across"
userProgress={progress12Across}
>
<Story />
<ShowAnagramHelperProvider userShowAnagramHelper={true}>
<Story />
</ShowAnagramHelperProvider>
</ContextProvider>
),
],
Expand All @@ -38,7 +41,9 @@ export const LongClue: Story = {
theme={defaultTheme}
selectedEntryId="7-across"
>
<Story />
<ShowAnagramHelperProvider userShowAnagramHelper={true}>
<Story />
</ShowAnagramHelperProvider>
</ContextProvider>
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const AnagramHelper = () => {
const [solving, setSolving] = useState(false);
const [shuffledLetters, setShuffledLetters] = useState<string[]>([]);
const theme = useTheme();
const { setShowAnagramHelper } = useShowAnagramHelper();
const { setShowAnagramHelper, showAnagramHelper } = useShowAnagramHelper();
const { entries, cells } = useData();
const { currentEntryId } = useCurrentClue();
const { progress } = useProgress();
Expand Down Expand Up @@ -57,11 +57,14 @@ export const AnagramHelper = () => {
reset();
}, [reset]);

if (!showAnagramHelper) {
return null;
}

return (
<div
css={css`
position: fixed;
overflow: auto;
position: absolute;
width: 100%;
height: 100%;
top: 0;
Expand Down
52 changes: 45 additions & 7 deletions libs/@guardian/react-crossword/src/components/Clues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Direction } from '../@types/Direction';
import { useCurrentCell } from '../context/CurrentCell';
import { useCurrentClue } from '../context/CurrentClue';
import { useData } from '../context/Data';
import { useFocus } from '../context/Focus';
import { useProgress } from '../context/Progress';
import { Clue } from './Clue';

Expand Down Expand Up @@ -40,6 +41,7 @@ const Label = memo(({ direction }: { direction: Direction }) => {
export const Clues = ({ direction, Header }: Props) => {
const { entries, getId, cells } = useData();
const { progress } = useProgress();
const { currentFocus, focusOn } = useFocus();
const { currentEntryId, setCurrentEntryId } = useCurrentClue();
const { setCurrentCell } = useCurrentCell();

Expand All @@ -61,14 +63,24 @@ export const Clues = ({ direction, Header }: Props) => {

const cluesRef = useRef<HTMLDivElement | null>(null);

const selectClue = useCallback(
const handleClick = useCallback(
(entry: CAPIEntry) => {
if (!(currentFocus === 'grid') && entry.id === currentEntryId) {
focusOn('grid');
}
setCurrentEntryId(entry.id);
setCurrentCell(
cells.getByCoords({ x: entry.position.x, y: entry.position.y }),
);
},
[cells, setCurrentCell, setCurrentEntryId],
[
currentFocus,
currentEntryId,
setCurrentEntryId,
setCurrentCell,
cells,
focusOn,
],
);

/**
Expand Down Expand Up @@ -107,18 +119,34 @@ export const Clues = ({ direction, Header }: Props) => {
setCurrentCluesEntriesIndex(cluesEntries.length - 1);
event.preventDefault();
break;
case 'Enter':
case ' ':
if (cluesEntries[currentCluesEntriesIndex]) {
handleClick(cluesEntries[currentCluesEntriesIndex]);
}
event.preventDefault();
break;
}
},
[cluesEntries],
[cluesEntries, currentCluesEntriesIndex, handleClick],
);

// Call `setCurrentEntryId` if `currentCluesEntriesIndex` changes
// Call `setCurrentEntryId` and `setCurrentCell` if `currentCluesEntriesIndex` changes
useEffect(() => {
const entry = cluesEntries[currentCluesEntriesIndex];
if (entry) {
setCurrentEntryId(entry.id);
setCurrentCell(
cells.getByCoords({ x: entry.position.x, y: entry.position.y }),
);
}
}, [currentCluesEntriesIndex, cluesEntries, setCurrentEntryId]);
}, [
currentCluesEntriesIndex,
cluesEntries,
setCurrentEntryId,
setCurrentCell,
cells,
]);

// Add event listeners
useEffect(() => {
Expand All @@ -133,6 +161,12 @@ export const Clues = ({ direction, Header }: Props) => {
};
}, [handleKeyDown, handleFocus]);

useEffect(() => {
if (currentFocus === direction) {
cluesRef.current?.focus();
}
}, [currentFocus, direction]);

return (
<div>
{Header ? (
Expand All @@ -144,7 +178,7 @@ export const Clues = ({ direction, Header }: Props) => {
)}

<div
tabIndex={0}
tabIndex={-1}
id={getId(`${direction}-hints`)}
role="listbox"
aria-labelledby={getId(`${direction}-label`)}
Expand Down Expand Up @@ -190,7 +224,11 @@ export const Clues = ({ direction, Header }: Props) => {
tabIndex={-1}
role="option"
aria-selected={isSelected}
onClick={() => selectClue(entry)}
onClick={(event) => {
event.preventDefault();
focusOn(direction);
handleClick(entry);
}}
/>
);
})}
Expand Down
89 changes: 59 additions & 30 deletions libs/@guardian/react-crossword/src/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { css } from '@emotion/react';
import { isUndefined } from '@guardian/libs';
import { space } from '@guardian/source/foundations';
import type { ButtonProps } from '@guardian/source/react-components';
import type { MouseEvent } from 'react';
import { cloneElement, useCallback, useEffect, useRef, useState } from 'react';
import type { Cell, Progress } from '../@types/crossword';
import type { EntryID } from '../@types/Entry';
import { useCurrentClue } from '../context/CurrentClue';
import { useData } from '../context/Data';
import { useFocus } from '../context/Focus';
import { useProgress } from '../context/Progress';
import { useShowAnagramHelper } from '../context/ShowAnagramHelper';
import { useTheme } from '../context/Theme';
Expand Down Expand Up @@ -240,7 +242,8 @@ const controlsGroupStyle = css`
`;

export const Controls = () => {
const { solutionAvailable } = useData();
const { solutionAvailable, getId } = useData();
const { currentFocus, focusOn } = useFocus();
const { currentEntryId } = useCurrentClue();

// Controls is a div[role=menu], split into two div[role=group]s containing
Expand Down Expand Up @@ -275,11 +278,6 @@ export const Controls = () => {
// We manually manage focus within the [role=menu].
//
// This is done by a `useEffect` that runs when the focused index/group changes.
//
// However, we only want to focus a control after user input (i.e. not when the
// component first renders), so we set this to true when the user first navigates
// using the arrow keys.
const [shouldSetFocus, setShouldSetFocus] = useState(false);

// We need to know how many controls are in each group, so we can manage the
// focused index. To to this, we store them here to two arrays and filter out
Expand All @@ -301,8 +299,6 @@ export const Controls = () => {

const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
setShouldSetFocus(true);

switch (event.key) {
case 'ArrowLeft':
if (focusedGroup === 'clues') {
Expand Down Expand Up @@ -355,45 +351,81 @@ export const Controls = () => {
}
},
[
focusedGroup,
disableClueControls,
cluesControls.length,
gridControls.length,
disableClueControls,
focusedGroup,
],
);

const handleClick = useCallback(
(event: MouseEvent) => {
if (!(event.target instanceof HTMLElement)) {
return;
}

const button = event.target.closest('button');
// split the button id to get the index and group of the button
if (button) {
const [group, , index] = button.id.split('-');
const numberIndex = Number(index);
if (isNaN(numberIndex)) {
return;
}
if (group === 'clues') {
setFocusedClueControlIndex(numberIndex);
setFocusedGroup('clues');
}
if (group === 'grid') {
setFocusedGridControlIndex(numberIndex);
setFocusedGroup('grid');
}
focusOn('controls');
}
},
[focusOn],
);

useEffect(() => {
// Only set focus after user input
if (shouldSetFocus) {
(
controlsRef.current?.querySelector(
'[tabindex="0"]',
) as HTMLElement | null
)?.focus();
setFocusedGroup(disableClueControls ? 'grid' : 'clues');
}, [disableClueControls]);

useEffect(() => {
if (currentFocus === 'controls') {
document
.getElementById(
getId(
`${focusedGroup}-control-${focusedGroup === 'grid' ? focusedGridControlIndex : focusedClueControlIndex}`,
),
)
?.focus();
}
}, [
shouldSetFocus,
focusedGroup,
currentFocus,
focusedClueControlIndex,
focusedGridControlIndex,
focusedGroup,
getId,
]);

useEffect(() => {
const controls = controlsRef.current;

if (!controls) {
return;
}

controls.addEventListener('keydown', handleKeyDown);

return () => {
controls.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);

return (
<div role="menu" ref={controlsRef} aria-label="Crossword controls">
<div
role="menu"
ref={controlsRef}
onClick={handleClick}
aria-label="Crossword controls"
>
<div
aria-label="Clue controls"
role="group"
Expand All @@ -402,13 +434,11 @@ export const Controls = () => {
>
{cluesControls.map((child, index) => {
if (child) {
const isTabTarget =
focusedGroup === 'clues' && focusedClueControlIndex === index;

return cloneElement(child, {
key: index,
id: getId(`clues-control-${index}`),
disabled: disableClueControls,
tabIndex: isTabTarget ? 0 : -1,
tabIndex: -1,
role: 'menuitem',
});
}
Expand All @@ -423,11 +453,10 @@ export const Controls = () => {
>
{gridControls.map((child, index) => {
if (child) {
const isTabTarget =
focusedGroup === 'grid' && focusedGridControlIndex === index;
return cloneElement(child, {
key: index,
tabIndex: isTabTarget ? 0 : -1,
id: getId(`grid-control-${index}`),
tabIndex: -1,
role: 'menuitem',
});
}
Expand Down
Loading

0 comments on commit be66a7a

Please sign in to comment.