@scrabble-solver/scrabble-solver 2.11.7 → 2.11.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +7 -7
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/eslint/.cache_8dgz12 +1 -1
- package/.next/cache/next-server.js.nft.json +1 -1
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/131.js +153 -115
- package/.next/server/chunks/277.js +1430 -691
- package/.next/server/chunks/44.js +3 -0
- package/.next/server/chunks/50.js +20 -78
- package/.next/server/chunks/865.js +153 -115
- package/.next/server/chunks/911.js +14 -14
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages/_app.js +8 -0
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +44 -11
- package/.next/server/pages/index.html +1 -1
- package/.next/server/pages/index.js +169 -15
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/static/9oRWxnZ1xFLSs55FJtiYi/_buildManifest.js +1 -0
- package/.next/static/chunks/pages/{404-ca203fa27afc37d8.js → 404-b4b5ce15153d4825.js} +1 -1
- package/.next/static/chunks/pages/_app-b0231bed954dd413.js +28 -0
- package/.next/static/chunks/pages/index-4e8566409753e1c3.js +1 -0
- package/.next/static/css/60e8258da7362a1a.css +1 -0
- package/.next/static/css/fcc46fec97b11afc.css +2 -0
- package/.next/trace +52 -50
- package/package.json +14 -13
- package/src/components/Board/Board.module.scss +18 -4
- package/src/components/Board/Board.tsx +145 -76
- package/src/components/Board/BoardPure.tsx +32 -40
- package/src/components/Board/components/Actions/Actions.module.scss +6 -17
- package/src/components/Board/components/Actions/Actions.tsx +36 -18
- package/src/components/Board/components/Cell/Cell.module.scss +12 -13
- package/src/components/Board/components/Cell/Cell.tsx +53 -3
- package/src/components/Board/components/InputPrompt/InputPrompt.module.scss +47 -0
- package/src/components/Board/components/InputPrompt/InputPrompt.tsx +81 -0
- package/src/components/Board/components/InputPrompt/index.ts +1 -0
- package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss +21 -0
- package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +34 -0
- package/src/components/Board/components/ToggleDirectionButton/index.ts +1 -0
- package/src/components/Board/components/index.ts +2 -0
- package/src/components/Board/hooks/index.ts +4 -0
- package/src/components/Board/hooks/useBackgroundImage.tsx +13 -9
- package/src/components/Board/hooks/useBoardStyle.ts +27 -0
- package/src/components/Board/hooks/useFloatingActions.ts +22 -0
- package/src/components/Board/hooks/useFloatingFocus.ts +10 -0
- package/src/components/Board/hooks/useFloatingInputPrompt.ts +19 -0
- package/src/components/Board/hooks/useGrid.ts +19 -2
- package/src/components/Key/Key.module.scss +7 -11
- package/src/components/NavButtons/NavButtons.tsx +2 -2
- package/src/components/Rack/Rack.module.scss +6 -6
- package/src/components/Rack/Rack.tsx +102 -24
- package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +22 -0
- package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +89 -0
- package/src/components/Rack/components/InputPrompt/index.ts +1 -0
- package/src/components/Rack/components/RackTile/RackTile.module.scss +11 -0
- package/src/components/Rack/{RackTile.tsx → components/RackTile/RackTile.tsx} +59 -9
- package/src/components/Rack/components/RackTile/index.ts +1 -0
- package/src/components/Rack/components/index.ts +2 -0
- package/src/components/Radio/Radio.module.scss +0 -8
- package/src/components/Solver/Solver.module.scss +0 -20
- package/src/components/Solver/Solver.tsx +2 -4
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -10
- package/src/components/Solver/components/index.ts +0 -1
- package/src/components/Tile/Tile.module.scss +5 -0
- package/src/components/Tile/Tile.tsx +8 -6
- package/src/components/Tile/TilePure.tsx +8 -0
- package/src/hooks/useAppLayout.ts +3 -1
- package/src/hooks/useLocalStorage.ts +8 -0
- package/src/i18n/de.json +6 -1
- package/src/i18n/en.json +6 -1
- package/src/i18n/es.json +6 -1
- package/src/i18n/fa.json +6 -1
- package/src/i18n/fr.json +6 -1
- package/src/i18n/pl.json +6 -1
- package/src/icons/Keyboard.svg +4 -3
- package/src/icons/KeyboardFill.svg +4 -0
- package/src/icons/index.ts +1 -0
- package/src/lib/extractCharacters.test.ts +26 -0
- package/src/lib/extractCharacters.ts +11 -9
- package/src/lib/extractCharactersByCase.test.ts +31 -0
- package/src/lib/extractCharactersByCase.ts +31 -0
- package/src/lib/index.ts +4 -1
- package/src/lib/isCtrl.ts +7 -0
- package/src/lib/isUpperCase.ts +7 -0
- package/src/modals/KeyMapModal/KeyMapModal.tsx +20 -4
- package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss +10 -4
- package/src/modals/SettingsModal/SettingsModal.tsx +5 -1
- package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss +12 -0
- package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +55 -0
- package/src/modals/SettingsModal/components/InputModeSetting/index.ts +1 -0
- package/src/modals/SettingsModal/components/InputModeSetting/lib.ts +13 -0
- package/src/modals/SettingsModal/components/InputModeSetting/types.ts +7 -0
- package/src/modals/SettingsModal/components/index.ts +1 -0
- package/src/state/localStorage.ts +10 -1
- package/src/state/selectors.ts +2 -0
- package/src/state/slices/settingsInitialState.ts +4 -1
- package/src/state/slices/settingsSlice.ts +6 -1
- package/src/styles/mixins.scss +1 -0
- package/src/styles/variables.scss +2 -0
- package/src/types/index.ts +7 -0
- package/.next/static/chunks/pages/_app-e89a3c225b87516a.js +0 -28
- package/.next/static/chunks/pages/index-58744f49bf6b891f.js +0 -1
- package/.next/static/css/34adfcf12a7d9bb6.css +0 -1
- package/.next/static/css/edaeaa48321b4cf2.css +0 -2
- package/.next/static/uhB6d-q63uRC6RubwepLq/_buildManifest.js +0 -1
- package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +0 -7
- package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.tsx +0 -53
- package/src/components/Solver/components/FloatingSolveButton/index.ts +0 -1
- /package/.next/static/{uhB6d-q63uRC6RubwepLq → 9oRWxnZ1xFLSs55FJtiYi}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
/* eslint-disable max-lines, max-statements */
|
|
2
|
+
|
|
3
|
+
import { FloatingPortal, autoUpdate, useFloating } from '@floating-ui/react';
|
|
1
4
|
import classNames from 'classnames';
|
|
2
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ChangeEvent,
|
|
7
|
+
ClipboardEvent,
|
|
8
|
+
FunctionComponent,
|
|
9
|
+
createRef,
|
|
10
|
+
useCallback,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import useOnclickOutside from 'react-cool-onclickoutside';
|
|
3
16
|
import { useDispatch } from 'react-redux';
|
|
4
17
|
|
|
18
|
+
import { useAppLayout } from 'hooks';
|
|
5
19
|
import { LOCALE_FEATURES } from 'i18n';
|
|
6
20
|
import {
|
|
7
21
|
createArray,
|
|
@@ -9,12 +23,21 @@ import {
|
|
|
9
23
|
extractCharacters,
|
|
10
24
|
extractInputValue,
|
|
11
25
|
getTileSizes,
|
|
26
|
+
isCtrl,
|
|
12
27
|
zipCharactersAndTiles,
|
|
13
28
|
} from 'lib';
|
|
14
|
-
import {
|
|
15
|
-
|
|
29
|
+
import {
|
|
30
|
+
rackSlice,
|
|
31
|
+
selectConfig,
|
|
32
|
+
selectInputMode,
|
|
33
|
+
selectLocale,
|
|
34
|
+
selectRack,
|
|
35
|
+
selectResultCandidateTiles,
|
|
36
|
+
useTypedSelector,
|
|
37
|
+
} from 'state';
|
|
38
|
+
|
|
39
|
+
import { InputPrompt, RackTile } from './components';
|
|
16
40
|
import styles from './Rack.module.scss';
|
|
17
|
-
import RackTile from './RackTile';
|
|
18
41
|
|
|
19
42
|
interface Props {
|
|
20
43
|
className?: string;
|
|
@@ -26,13 +49,24 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
|
|
|
26
49
|
const config = useTypedSelector(selectConfig);
|
|
27
50
|
const locale = useTypedSelector(selectLocale);
|
|
28
51
|
const rack = useTypedSelector(selectRack);
|
|
52
|
+
const inputMode = useTypedSelector(selectInputMode);
|
|
53
|
+
const { rackHeight } = useAppLayout();
|
|
29
54
|
const resultCandidateTiles = useTypedSelector(selectResultCandidateTiles);
|
|
30
55
|
const tiles = useMemo(() => zipCharactersAndTiles(rack, resultCandidateTiles), [rack, resultCandidateTiles]);
|
|
31
56
|
const tilesCount = tiles.length;
|
|
32
57
|
const tilesRefs = useMemo(() => createArray(tilesCount).map(() => createRef<HTMLInputElement>()), [tilesCount]);
|
|
33
58
|
const activeIndexRef = useRef<number>();
|
|
59
|
+
const [hasFocus, setHasFocus] = useState(false);
|
|
60
|
+
const [input, setInput] = useState('');
|
|
34
61
|
const { direction } = LOCALE_FEATURES[locale];
|
|
35
62
|
const { tileFontSize } = getTileSizes(tileSize);
|
|
63
|
+
const showInputPrompt = inputMode === 'touchscreen' && hasFocus;
|
|
64
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
65
|
+
|
|
66
|
+
useOnclickOutside(() => setHasFocus(false), {
|
|
67
|
+
ignoreClass: [InputPrompt.styles.form, InputPrompt.styles.input],
|
|
68
|
+
refs: [ref],
|
|
69
|
+
});
|
|
36
70
|
|
|
37
71
|
const changeActiveIndex = useCallback(
|
|
38
72
|
(offset: number) => {
|
|
@@ -74,6 +108,16 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
|
|
|
74
108
|
[changeActiveIndex, config, dispatch],
|
|
75
109
|
);
|
|
76
110
|
|
|
111
|
+
const handleFocus = useCallback(() => {
|
|
112
|
+
setHasFocus(true);
|
|
113
|
+
floatingInputPrompt.refs.setPositionReference(ref.current);
|
|
114
|
+
const characters: string[] = rack.filter((character) => character !== null) as string[];
|
|
115
|
+
const uppercasedDigraphs = characters.map((character) => {
|
|
116
|
+
return character.length > 1 ? character.toLocaleUpperCase(locale) : character;
|
|
117
|
+
});
|
|
118
|
+
setInput(uppercasedDigraphs.join(''));
|
|
119
|
+
}, [rack, ref]);
|
|
120
|
+
|
|
77
121
|
const handleKeyDown = useMemo(() => {
|
|
78
122
|
return createKeyboardNavigation({
|
|
79
123
|
onArrowLeft: (event) => {
|
|
@@ -100,7 +144,9 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
|
|
|
100
144
|
changeActiveIndex(1);
|
|
101
145
|
},
|
|
102
146
|
onKeyDown: (event) => {
|
|
103
|
-
if (event
|
|
147
|
+
if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
|
|
148
|
+
changeActiveIndex(1);
|
|
149
|
+
} else if (event.currentTarget.value === event.key) {
|
|
104
150
|
// change event did not fire because the same character was typed over the current one
|
|
105
151
|
// but we still want to move the caret
|
|
106
152
|
event.preventDefault();
|
|
@@ -111,26 +157,58 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
|
|
|
111
157
|
});
|
|
112
158
|
}, [changeActiveIndex, config, direction]);
|
|
113
159
|
|
|
160
|
+
const floatingInputPrompt = useFloating({
|
|
161
|
+
placement: 'bottom-start',
|
|
162
|
+
whileElementsMounted: autoUpdate,
|
|
163
|
+
});
|
|
164
|
+
|
|
114
165
|
return (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
<>
|
|
167
|
+
<div
|
|
168
|
+
className={classNames(styles.rack, className, {
|
|
169
|
+
[styles.hidden]: showInputPrompt,
|
|
170
|
+
})}
|
|
171
|
+
ref={ref}
|
|
172
|
+
style={{ fontSize: tileFontSize }}
|
|
173
|
+
onPaste={handlePaste}
|
|
174
|
+
>
|
|
175
|
+
{tiles.map(({ character, tile }, index) => (
|
|
176
|
+
<RackTile
|
|
177
|
+
activeIndexRef={activeIndexRef}
|
|
178
|
+
character={character}
|
|
179
|
+
className={classNames(styles.tile, {
|
|
180
|
+
[styles.sharpLeft]: index !== 0,
|
|
181
|
+
[styles.sharpRight]: index !== tiles.length - 1,
|
|
182
|
+
})}
|
|
183
|
+
index={index}
|
|
184
|
+
inputRef={tilesRefs[index]}
|
|
185
|
+
key={index}
|
|
186
|
+
size={tileSize}
|
|
187
|
+
tile={tile}
|
|
188
|
+
onChange={handleChange}
|
|
189
|
+
onKeyDown={handleKeyDown}
|
|
190
|
+
onFocus={handleFocus}
|
|
191
|
+
/>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{showInputPrompt && (
|
|
196
|
+
<FloatingPortal>
|
|
197
|
+
<InputPrompt
|
|
198
|
+
ref={floatingInputPrompt.refs.setFloating}
|
|
199
|
+
style={{
|
|
200
|
+
position: floatingInputPrompt.strategy,
|
|
201
|
+
top: floatingInputPrompt.y ? floatingInputPrompt.y - rackHeight : 0,
|
|
202
|
+
left: floatingInputPrompt.x ?? 0,
|
|
203
|
+
}}
|
|
204
|
+
value={input}
|
|
205
|
+
onBlur={() => setHasFocus(false)}
|
|
206
|
+
onChange={(event) => setInput(event.target.value)}
|
|
207
|
+
onSubmit={() => setHasFocus(false)}
|
|
208
|
+
/>
|
|
209
|
+
</FloatingPortal>
|
|
210
|
+
)}
|
|
211
|
+
</>
|
|
134
212
|
);
|
|
135
213
|
};
|
|
136
214
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@import 'styles/mixins';
|
|
2
|
+
|
|
3
|
+
.form {
|
|
4
|
+
@include focus-effect;
|
|
5
|
+
|
|
6
|
+
border-radius: var(--border--radius);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.input {
|
|
10
|
+
@include text-input;
|
|
11
|
+
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 0;
|
|
14
|
+
right: 0;
|
|
15
|
+
bottom: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
border-radius: var(--border--radius);
|
|
20
|
+
border: none;
|
|
21
|
+
box-shadow: var(--box-shadow);
|
|
22
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* eslint-disable max-lines, max-statements */
|
|
2
|
+
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import {
|
|
5
|
+
CSSProperties,
|
|
6
|
+
ChangeEventHandler,
|
|
7
|
+
FormEventHandler,
|
|
8
|
+
forwardRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import { useDispatch } from 'react-redux';
|
|
14
|
+
|
|
15
|
+
import { useAppLayout } from 'hooks';
|
|
16
|
+
import { extractCharactersByCase } from 'lib';
|
|
17
|
+
import { rackSlice, selectConfig, useTranslate, useTypedSelector } from 'state';
|
|
18
|
+
|
|
19
|
+
import styles from './InputPrompt.module.scss';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
className?: string;
|
|
23
|
+
style?: CSSProperties;
|
|
24
|
+
value: string;
|
|
25
|
+
onBlur: () => void;
|
|
26
|
+
onChange: ChangeEventHandler<HTMLInputElement>;
|
|
27
|
+
onSubmit: FormEventHandler<HTMLFormElement>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const InputPrompt = forwardRef<HTMLFormElement, Props>(
|
|
31
|
+
({ className, style, value, onBlur, onChange, onSubmit, ...props }, ref) => {
|
|
32
|
+
const dispatch = useDispatch();
|
|
33
|
+
const translate = useTranslate();
|
|
34
|
+
const { rackHeight, rackWidth } = useAppLayout();
|
|
35
|
+
const config = useTypedSelector(selectConfig);
|
|
36
|
+
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
|
|
37
|
+
|
|
38
|
+
const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
|
39
|
+
(event) => {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
const charactersByCase = extractCharactersByCase(config, value);
|
|
42
|
+
const characters = Array.from({ length: config.maximumCharactersCount }, (_, index) => {
|
|
43
|
+
return typeof charactersByCase[index] === 'string' ? charactersByCase[index] : null;
|
|
44
|
+
});
|
|
45
|
+
dispatch(rackSlice.actions.changeCharacters({ characters, index: 0 }));
|
|
46
|
+
onSubmit(event);
|
|
47
|
+
},
|
|
48
|
+
[config, value, onSubmit],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (inputRef) {
|
|
53
|
+
inputRef.focus();
|
|
54
|
+
inputRef.select();
|
|
55
|
+
}
|
|
56
|
+
}, [inputRef]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<form
|
|
60
|
+
className={classNames(styles.form, className)}
|
|
61
|
+
ref={ref}
|
|
62
|
+
style={{
|
|
63
|
+
width: rackWidth,
|
|
64
|
+
height: rackHeight,
|
|
65
|
+
...style,
|
|
66
|
+
}}
|
|
67
|
+
onSubmit={handleSubmit}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<input
|
|
71
|
+
autoCapitalize="none"
|
|
72
|
+
autoComplete="off"
|
|
73
|
+
autoCorrect="off"
|
|
74
|
+
className={styles.input}
|
|
75
|
+
placeholder={translate('rack.touchscreen.placeholder')}
|
|
76
|
+
ref={setInputRef}
|
|
77
|
+
spellCheck={false}
|
|
78
|
+
value={value}
|
|
79
|
+
onBlur={onBlur}
|
|
80
|
+
onChange={onChange}
|
|
81
|
+
/>
|
|
82
|
+
</form>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export default Object.assign(InputPrompt, {
|
|
88
|
+
styles,
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './InputPrompt';
|
|
@@ -4,29 +4,33 @@ import classNames from 'classnames';
|
|
|
4
4
|
import {
|
|
5
5
|
ChangeEvent,
|
|
6
6
|
ChangeEventHandler,
|
|
7
|
+
FocusEventHandler,
|
|
7
8
|
FunctionComponent,
|
|
8
9
|
KeyboardEventHandler,
|
|
10
|
+
MouseEventHandler,
|
|
9
11
|
MutableRefObject,
|
|
10
12
|
RefObject,
|
|
13
|
+
TouchEventHandler,
|
|
11
14
|
useCallback,
|
|
12
15
|
useMemo,
|
|
13
16
|
} from 'react';
|
|
14
17
|
import { useDispatch } from 'react-redux';
|
|
15
18
|
|
|
16
|
-
import { createKeyboardNavigation, extractCharacters, extractInputValue } from 'lib';
|
|
19
|
+
import { createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
|
|
17
20
|
import {
|
|
18
21
|
rackSlice,
|
|
19
22
|
selectCharacterIsValid,
|
|
20
23
|
selectCharacterPoints,
|
|
21
24
|
selectConfig,
|
|
25
|
+
selectInputMode,
|
|
22
26
|
selectLocale,
|
|
23
27
|
useTranslate,
|
|
24
28
|
useTypedSelector,
|
|
25
29
|
} from 'state';
|
|
26
30
|
|
|
27
|
-
import Tile from '
|
|
31
|
+
import Tile from '../../../Tile';
|
|
28
32
|
|
|
29
|
-
import styles from './
|
|
33
|
+
import styles from './RackTile.module.scss';
|
|
30
34
|
|
|
31
35
|
interface Props {
|
|
32
36
|
activeIndexRef: MutableRefObject<number | undefined>;
|
|
@@ -38,6 +42,7 @@ interface Props {
|
|
|
38
42
|
tile: TileModel | null;
|
|
39
43
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
|
40
44
|
onKeyDown: KeyboardEventHandler<HTMLInputElement>;
|
|
45
|
+
onFocus: () => void;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
const RackTile: FunctionComponent<Props> = ({
|
|
@@ -50,17 +55,28 @@ const RackTile: FunctionComponent<Props> = ({
|
|
|
50
55
|
tile,
|
|
51
56
|
onChange,
|
|
52
57
|
onKeyDown,
|
|
58
|
+
onFocus,
|
|
53
59
|
}) => {
|
|
54
60
|
const dispatch = useDispatch();
|
|
55
61
|
const translate = useTranslate();
|
|
56
62
|
const locale = useTypedSelector(selectLocale);
|
|
57
63
|
const config = useTypedSelector(selectConfig);
|
|
64
|
+
const inputMode = useTypedSelector(selectInputMode);
|
|
58
65
|
const points = useTypedSelector((state) => selectCharacterPoints(state, character));
|
|
59
66
|
const isValid = useTypedSelector((state) => selectCharacterIsValid(state, character));
|
|
60
67
|
|
|
61
|
-
const handleFocus = useCallback(
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
const handleFocus: FocusEventHandler<HTMLInputElement> = useCallback(
|
|
69
|
+
(event) => {
|
|
70
|
+
if (inputMode === 'touchscreen') {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
event.target.blur();
|
|
73
|
+
onFocus();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
activeIndexRef.current = index;
|
|
77
|
+
},
|
|
78
|
+
[activeIndexRef, index, inputMode, onFocus],
|
|
79
|
+
);
|
|
64
80
|
|
|
65
81
|
const handleChange = useCallback(
|
|
66
82
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
@@ -80,17 +96,49 @@ const RackTile: FunctionComponent<Props> = ({
|
|
|
80
96
|
event.preventDefault();
|
|
81
97
|
dispatch(rackSlice.actions.changeCharacter({ character: null, index }));
|
|
82
98
|
},
|
|
83
|
-
onKeyDown
|
|
99
|
+
onKeyDown: (event) => {
|
|
100
|
+
if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
const twoTilesCharacter = config.getTwoCharacterTileByPrefix(event.key);
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
105
|
+
dispatch(rackSlice.actions.changeCharacter({ character: twoTilesCharacter!, index }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onKeyDown(event);
|
|
109
|
+
},
|
|
84
110
|
});
|
|
85
111
|
}, [index, onKeyDown]);
|
|
86
112
|
|
|
113
|
+
const handleMouseDown: MouseEventHandler<HTMLInputElement> = useCallback(
|
|
114
|
+
(event) => {
|
|
115
|
+
if (inputMode === 'touchscreen') {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onFocus();
|
|
120
|
+
},
|
|
121
|
+
[inputMode, onFocus],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const handleTouchStart: TouchEventHandler<HTMLInputElement> = useCallback(
|
|
125
|
+
(event) => {
|
|
126
|
+
if (inputMode === 'touchscreen') {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onFocus();
|
|
131
|
+
},
|
|
132
|
+
[inputMode, onFocus],
|
|
133
|
+
);
|
|
134
|
+
|
|
87
135
|
return (
|
|
88
136
|
<Tile
|
|
89
137
|
aria-label={translate('rack.tile.location', {
|
|
90
138
|
index: (index + 1).toLocaleString(locale),
|
|
91
139
|
})}
|
|
92
|
-
autoFocus={index === 0}
|
|
93
|
-
className={classNames(styles.
|
|
140
|
+
autoFocus={inputMode === 'keyboard' && index === 0}
|
|
141
|
+
className={classNames(styles.rackTile, className)}
|
|
94
142
|
character={character === null ? undefined : character}
|
|
95
143
|
highlighted={tile !== null}
|
|
96
144
|
inputRef={inputRef}
|
|
@@ -105,6 +153,8 @@ const RackTile: FunctionComponent<Props> = ({
|
|
|
105
153
|
onChange={handleChange}
|
|
106
154
|
onFocus={handleFocus}
|
|
107
155
|
onKeyDown={handleKeyDown}
|
|
156
|
+
onMouseDown={handleMouseDown}
|
|
157
|
+
onTouchStart={handleTouchStart}
|
|
108
158
|
/>
|
|
109
159
|
);
|
|
110
160
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './RackTile';
|
|
@@ -15,13 +15,6 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
|
|
|
15
15
|
transition: var(--transition);
|
|
16
16
|
cursor: pointer;
|
|
17
17
|
|
|
18
|
-
&:hover,
|
|
19
|
-
&.checked {
|
|
20
|
-
.icon {
|
|
21
|
-
box-shadow: var(--box-shadow);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
&.checked {
|
|
26
19
|
.icon {
|
|
27
20
|
&::after {
|
|
@@ -48,7 +41,6 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
|
|
|
48
41
|
height: $radio-box-size;
|
|
49
42
|
border-radius: 50%;
|
|
50
43
|
border: $radio-inner-border solid var(--color--primary);
|
|
51
|
-
box-shadow: var(--box-shadow--null);
|
|
52
44
|
pointer-events: none;
|
|
53
45
|
|
|
54
46
|
&::after {
|
|
@@ -113,23 +113,3 @@
|
|
|
113
113
|
.emptyState {
|
|
114
114
|
margin-top: var(--spacing);
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
.solve {
|
|
118
|
-
--spacing: var(--spacing--l);
|
|
119
|
-
|
|
120
|
-
position: fixed;
|
|
121
|
-
bottom: var(--spacing);
|
|
122
|
-
z-index: 1;
|
|
123
|
-
|
|
124
|
-
@include media('<xs') {
|
|
125
|
-
--spacing: var(--spacing--m);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
[dir='ltr'] & {
|
|
129
|
-
right: var(--spacing);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
[dir='rtl'] & {
|
|
133
|
-
left: var(--spacing);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -22,7 +22,7 @@ import DictionaryInput from '../DictionaryInput';
|
|
|
22
22
|
import Rack from '../Rack';
|
|
23
23
|
import Results from '../Results';
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { ResultCandidatePicker } from './components';
|
|
26
26
|
import styles from './Solver.module.scss';
|
|
27
27
|
|
|
28
28
|
interface Props {
|
|
@@ -34,7 +34,7 @@ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
|
|
|
34
34
|
const dispatch = useDispatch();
|
|
35
35
|
const translate = useTranslate();
|
|
36
36
|
const isTouchDevice = useIsTouchDevice();
|
|
37
|
-
const { maxControlsWidth, showCompactControls,
|
|
37
|
+
const { maxControlsWidth, showCompactControls, tileSize } = useAppLayout();
|
|
38
38
|
const error = useTypedSelector(selectSolveError);
|
|
39
39
|
const isOutdated = useTypedSelector(selectAreResultsOutdated);
|
|
40
40
|
const resultCandidate = useTypedSelector(selectResultCandidate);
|
|
@@ -134,8 +134,6 @@ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
|
|
|
134
134
|
)}
|
|
135
135
|
</div>
|
|
136
136
|
</div>
|
|
137
|
-
|
|
138
|
-
{showFloatingSolveButton && <FloatingSolveButton className={styles.solve} onClick={handleSubmit} />}
|
|
139
137
|
</div>
|
|
140
138
|
);
|
|
141
139
|
};
|
|
@@ -2,7 +2,6 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { FunctionComponent, HTMLProps, MouseEventHandler } from 'react';
|
|
3
3
|
import { useDispatch } from 'react-redux';
|
|
4
4
|
|
|
5
|
-
import { useAppLayout } from 'hooks';
|
|
6
5
|
import { ChevronDown, ChevronLeft, ChevronRight } from 'icons';
|
|
7
6
|
import {
|
|
8
7
|
resultsSlice,
|
|
@@ -38,7 +37,6 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
|
|
|
38
37
|
const isPreviousDisabled = !results || index <= 0 || disabled;
|
|
39
38
|
const isNextDisabled = !results || index >= results.length - 1 || disabled;
|
|
40
39
|
const bothEnabled = !isPreviousDisabled && !isNextDisabled;
|
|
41
|
-
const { showFloatingSolveButton } = useAppLayout();
|
|
42
40
|
|
|
43
41
|
const handleNextClick = () => {
|
|
44
42
|
if (!isNextDisabled) {
|
|
@@ -91,14 +89,8 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
|
|
|
91
89
|
{!resultCandidate && <div className={styles.word}> </div>}
|
|
92
90
|
|
|
93
91
|
<div className={styles.iconContainer}>
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
{!showFloatingSolveButton && (
|
|
97
|
-
<>
|
|
98
|
-
{isLoading && <Spinner className={styles.loading} />}
|
|
99
|
-
{!isLoading && <ChevronDown className={styles.icon} />}
|
|
100
|
-
</>
|
|
101
|
-
)}
|
|
92
|
+
{isLoading && <Spinner className={styles.loading} />}
|
|
93
|
+
{!isLoading && <ChevronDown className={styles.icon} />}
|
|
102
94
|
</div>
|
|
103
95
|
</button>
|
|
104
96
|
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
transition-property: background-color, color, box-shadow;
|
|
18
18
|
user-select: none;
|
|
19
19
|
|
|
20
|
+
@include media('<xs') {
|
|
21
|
+
--border--radius: 3px;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
&.points1 {
|
|
21
25
|
--background-color: var(--color--yellow);
|
|
22
26
|
}
|
|
@@ -83,6 +87,7 @@
|
|
|
83
87
|
color: transparent;
|
|
84
88
|
caret-color: transparent;
|
|
85
89
|
font-size: 16px; // prevent iOS from automatically zooming in on focus
|
|
90
|
+
outline: none;
|
|
86
91
|
|
|
87
92
|
&::selection {
|
|
88
93
|
--background--color: transparent;
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
FocusEventHandler,
|
|
6
6
|
FunctionComponent,
|
|
7
7
|
KeyboardEventHandler,
|
|
8
|
+
MouseEventHandler,
|
|
8
9
|
Ref,
|
|
10
|
+
TouchEventHandler,
|
|
9
11
|
useCallback,
|
|
10
12
|
useEffect,
|
|
11
13
|
useMemo,
|
|
@@ -37,6 +39,8 @@ interface Props {
|
|
|
37
39
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
38
40
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
39
41
|
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
|
|
42
|
+
onMouseDown?: MouseEventHandler<HTMLInputElement>;
|
|
43
|
+
onTouchStart?: TouchEventHandler<HTMLInputElement>;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const Tile: FunctionComponent<Props> = ({
|
|
@@ -57,6 +61,8 @@ const Tile: FunctionComponent<Props> = ({
|
|
|
57
61
|
onChange,
|
|
58
62
|
onFocus = noop,
|
|
59
63
|
onKeyDown = noop,
|
|
64
|
+
onMouseDown = noop,
|
|
65
|
+
onTouchStart = noop,
|
|
60
66
|
}) => {
|
|
61
67
|
const locale = useTypedSelector(selectLocale);
|
|
62
68
|
const { animateTile, showTilePoints } = useAppLayout();
|
|
@@ -77,12 +83,6 @@ const Tile: FunctionComponent<Props> = ({
|
|
|
77
83
|
[onKeyDown],
|
|
78
84
|
);
|
|
79
85
|
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (autoFocus && ref.current) {
|
|
82
|
-
ref.current.focus();
|
|
83
|
-
}
|
|
84
|
-
}, [autoFocus, ref]);
|
|
85
|
-
|
|
86
86
|
useEffect(() => {
|
|
87
87
|
if (!ref.current?.parentElement || !character || !animateTile) {
|
|
88
88
|
return;
|
|
@@ -117,6 +117,8 @@ const Tile: FunctionComponent<Props> = ({
|
|
|
117
117
|
onChange={onChange}
|
|
118
118
|
onFocus={onFocus}
|
|
119
119
|
onKeyDown={handleKeyDown}
|
|
120
|
+
onMouseDown={onMouseDown}
|
|
121
|
+
onTouchStart={onTouchStart}
|
|
120
122
|
/>
|
|
121
123
|
);
|
|
122
124
|
};
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
FunctionComponent,
|
|
7
7
|
KeyboardEventHandler,
|
|
8
8
|
memo,
|
|
9
|
+
MouseEventHandler,
|
|
9
10
|
Ref,
|
|
11
|
+
TouchEventHandler,
|
|
10
12
|
} from 'react';
|
|
11
13
|
|
|
12
14
|
import { ExclamationSquareFill } from 'icons';
|
|
@@ -34,6 +36,8 @@ interface Props {
|
|
|
34
36
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
35
37
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
36
38
|
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
|
|
39
|
+
onMouseDown?: MouseEventHandler<HTMLInputElement>;
|
|
40
|
+
onTouchStart?: TouchEventHandler<HTMLInputElement>;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const TilePure: FunctionComponent<Props> = ({
|
|
@@ -57,6 +61,8 @@ const TilePure: FunctionComponent<Props> = ({
|
|
|
57
61
|
onChange,
|
|
58
62
|
onFocus,
|
|
59
63
|
onKeyDown,
|
|
64
|
+
onMouseDown,
|
|
65
|
+
onTouchStart,
|
|
60
66
|
}) => (
|
|
61
67
|
<div
|
|
62
68
|
className={classNames(styles.tile, className, {
|
|
@@ -90,6 +96,8 @@ const TilePure: FunctionComponent<Props> = ({
|
|
|
90
96
|
onChange={onChange}
|
|
91
97
|
onFocus={onFocus}
|
|
92
98
|
onKeyDown={onKeyDown}
|
|
99
|
+
onMouseDown={onMouseDown}
|
|
100
|
+
onTouchStart={onTouchStart}
|
|
93
101
|
/>
|
|
94
102
|
|
|
95
103
|
{canShowPoints && (
|
|
@@ -56,6 +56,7 @@ const useAppLayout = () => {
|
|
|
56
56
|
const resultsHeight = isLessThanL
|
|
57
57
|
? viewportHeight - dictionaryHeight - BUTTON_HEIGHT - MODAL_HEADER_HEIGHT - 5 * componentsSpacing
|
|
58
58
|
: boardSize - componentsSpacing - dictionaryHeight;
|
|
59
|
+
const rackWidth = tileSize * config.maximumCharactersCount;
|
|
59
60
|
|
|
60
61
|
return {
|
|
61
62
|
actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
|
|
@@ -67,10 +68,11 @@ const useAppLayout = () => {
|
|
|
67
68
|
logoHeight,
|
|
68
69
|
logoWidth: logoHeight * LOGO_ASPECT_RATIO,
|
|
69
70
|
maxControlsWidth,
|
|
71
|
+
rackHeight: tileSize,
|
|
72
|
+
rackWidth,
|
|
70
73
|
resultsHeight,
|
|
71
74
|
resultsWidth: isLessThanL ? modalWidth - 2 * componentsSpacing : SOLVER_COLUMN_WIDTH,
|
|
72
75
|
showCompactControls: !showColumn,
|
|
73
|
-
showFloatingSolveButton: isTouchDevice,
|
|
74
76
|
showKeyMap: !isTouchDevice,
|
|
75
77
|
showResultsInModal,
|
|
76
78
|
showShortNav: isLessThanS,
|