@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.
Files changed (122) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +7 -7
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/131.js +153 -115
  16. package/.next/server/chunks/277.js +1430 -691
  17. package/.next/server/chunks/44.js +3 -0
  18. package/.next/server/chunks/50.js +20 -78
  19. package/.next/server/chunks/865.js +153 -115
  20. package/.next/server/chunks/911.js +14 -14
  21. package/.next/server/middleware-build-manifest.js +1 -1
  22. package/.next/server/pages/404.html +1 -1
  23. package/.next/server/pages/404.js.nft.json +1 -1
  24. package/.next/server/pages/500.html +1 -1
  25. package/.next/server/pages/_app.js +8 -0
  26. package/.next/server/pages/_app.js.nft.json +1 -1
  27. package/.next/server/pages/api/solve.js +44 -11
  28. package/.next/server/pages/index.html +1 -1
  29. package/.next/server/pages/index.js +169 -15
  30. package/.next/server/pages/index.js.nft.json +1 -1
  31. package/.next/server/pages/index.json +1 -1
  32. package/.next/static/9oRWxnZ1xFLSs55FJtiYi/_buildManifest.js +1 -0
  33. package/.next/static/chunks/pages/{404-ca203fa27afc37d8.js → 404-b4b5ce15153d4825.js} +1 -1
  34. package/.next/static/chunks/pages/_app-b0231bed954dd413.js +28 -0
  35. package/.next/static/chunks/pages/index-4e8566409753e1c3.js +1 -0
  36. package/.next/static/css/60e8258da7362a1a.css +1 -0
  37. package/.next/static/css/fcc46fec97b11afc.css +2 -0
  38. package/.next/trace +52 -50
  39. package/package.json +14 -13
  40. package/src/components/Board/Board.module.scss +18 -4
  41. package/src/components/Board/Board.tsx +145 -76
  42. package/src/components/Board/BoardPure.tsx +32 -40
  43. package/src/components/Board/components/Actions/Actions.module.scss +6 -17
  44. package/src/components/Board/components/Actions/Actions.tsx +36 -18
  45. package/src/components/Board/components/Cell/Cell.module.scss +12 -13
  46. package/src/components/Board/components/Cell/Cell.tsx +53 -3
  47. package/src/components/Board/components/InputPrompt/InputPrompt.module.scss +47 -0
  48. package/src/components/Board/components/InputPrompt/InputPrompt.tsx +81 -0
  49. package/src/components/Board/components/InputPrompt/index.ts +1 -0
  50. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss +21 -0
  51. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +34 -0
  52. package/src/components/Board/components/ToggleDirectionButton/index.ts +1 -0
  53. package/src/components/Board/components/index.ts +2 -0
  54. package/src/components/Board/hooks/index.ts +4 -0
  55. package/src/components/Board/hooks/useBackgroundImage.tsx +13 -9
  56. package/src/components/Board/hooks/useBoardStyle.ts +27 -0
  57. package/src/components/Board/hooks/useFloatingActions.ts +22 -0
  58. package/src/components/Board/hooks/useFloatingFocus.ts +10 -0
  59. package/src/components/Board/hooks/useFloatingInputPrompt.ts +19 -0
  60. package/src/components/Board/hooks/useGrid.ts +19 -2
  61. package/src/components/Key/Key.module.scss +7 -11
  62. package/src/components/NavButtons/NavButtons.tsx +2 -2
  63. package/src/components/Rack/Rack.module.scss +6 -6
  64. package/src/components/Rack/Rack.tsx +102 -24
  65. package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +22 -0
  66. package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +89 -0
  67. package/src/components/Rack/components/InputPrompt/index.ts +1 -0
  68. package/src/components/Rack/components/RackTile/RackTile.module.scss +11 -0
  69. package/src/components/Rack/{RackTile.tsx → components/RackTile/RackTile.tsx} +59 -9
  70. package/src/components/Rack/components/RackTile/index.ts +1 -0
  71. package/src/components/Rack/components/index.ts +2 -0
  72. package/src/components/Radio/Radio.module.scss +0 -8
  73. package/src/components/Solver/Solver.module.scss +0 -20
  74. package/src/components/Solver/Solver.tsx +2 -4
  75. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -10
  76. package/src/components/Solver/components/index.ts +0 -1
  77. package/src/components/Tile/Tile.module.scss +5 -0
  78. package/src/components/Tile/Tile.tsx +8 -6
  79. package/src/components/Tile/TilePure.tsx +8 -0
  80. package/src/hooks/useAppLayout.ts +3 -1
  81. package/src/hooks/useLocalStorage.ts +8 -0
  82. package/src/i18n/de.json +6 -1
  83. package/src/i18n/en.json +6 -1
  84. package/src/i18n/es.json +6 -1
  85. package/src/i18n/fa.json +6 -1
  86. package/src/i18n/fr.json +6 -1
  87. package/src/i18n/pl.json +6 -1
  88. package/src/icons/Keyboard.svg +4 -3
  89. package/src/icons/KeyboardFill.svg +4 -0
  90. package/src/icons/index.ts +1 -0
  91. package/src/lib/extractCharacters.test.ts +26 -0
  92. package/src/lib/extractCharacters.ts +11 -9
  93. package/src/lib/extractCharactersByCase.test.ts +31 -0
  94. package/src/lib/extractCharactersByCase.ts +31 -0
  95. package/src/lib/index.ts +4 -1
  96. package/src/lib/isCtrl.ts +7 -0
  97. package/src/lib/isUpperCase.ts +7 -0
  98. package/src/modals/KeyMapModal/KeyMapModal.tsx +20 -4
  99. package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss +10 -4
  100. package/src/modals/SettingsModal/SettingsModal.tsx +5 -1
  101. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss +12 -0
  102. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +55 -0
  103. package/src/modals/SettingsModal/components/InputModeSetting/index.ts +1 -0
  104. package/src/modals/SettingsModal/components/InputModeSetting/lib.ts +13 -0
  105. package/src/modals/SettingsModal/components/InputModeSetting/types.ts +7 -0
  106. package/src/modals/SettingsModal/components/index.ts +1 -0
  107. package/src/state/localStorage.ts +10 -1
  108. package/src/state/selectors.ts +2 -0
  109. package/src/state/slices/settingsInitialState.ts +4 -1
  110. package/src/state/slices/settingsSlice.ts +6 -1
  111. package/src/styles/mixins.scss +1 -0
  112. package/src/styles/variables.scss +2 -0
  113. package/src/types/index.ts +7 -0
  114. package/.next/static/chunks/pages/_app-e89a3c225b87516a.js +0 -28
  115. package/.next/static/chunks/pages/index-58744f49bf6b891f.js +0 -1
  116. package/.next/static/css/34adfcf12a7d9bb6.css +0 -1
  117. package/.next/static/css/edaeaa48321b4cf2.css +0 -2
  118. package/.next/static/uhB6d-q63uRC6RubwepLq/_buildManifest.js +0 -1
  119. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +0 -7
  120. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.tsx +0 -53
  121. package/src/components/Solver/components/FloatingSolveButton/index.ts +0 -1
  122. /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 { ChangeEvent, ClipboardEvent, createRef, FunctionComponent, useCallback, useMemo, useRef } from 'react';
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 { rackSlice, selectConfig, selectLocale, selectRack, selectResultCandidateTiles, useTypedSelector } from 'state';
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.currentTarget.value === event.key) {
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
- <div className={classNames(styles.rack, className)} style={{ fontSize: tileFontSize }} onPaste={handlePaste}>
116
- {tiles.map(({ character, tile }, index) => (
117
- <RackTile
118
- activeIndexRef={activeIndexRef}
119
- character={character}
120
- className={classNames({
121
- [styles.sharpLeft]: index !== 0,
122
- [styles.sharpRight]: index !== tiles.length - 1,
123
- })}
124
- index={index}
125
- inputRef={tilesRefs[index]}
126
- key={index}
127
- size={tileSize}
128
- tile={tile}
129
- onChange={handleChange}
130
- onKeyDown={handleKeyDown}
131
- />
132
- ))}
133
- </div>
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';
@@ -0,0 +1,11 @@
1
+ @import 'styles/mixins';
2
+
3
+ .rackTile {
4
+ @include focus-effect;
5
+
6
+ --background-color: var(--color--background);
7
+
8
+ &:focus-within {
9
+ z-index: 2;
10
+ }
11
+ }
@@ -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 '../Tile';
31
+ import Tile from '../../../Tile';
28
32
 
29
- import styles from './Rack.module.scss';
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
- activeIndexRef.current = index;
63
- }, [index]);
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.tile, className)}
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';
@@ -0,0 +1,2 @@
1
+ export { default as InputPrompt } from './InputPrompt';
2
+ export { default as RackTile } 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 { FloatingSolveButton, ResultCandidatePicker } from './components';
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, showFloatingSolveButton, tileSize } = useAppLayout();
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
- {showFloatingSolveButton && <ChevronDown className={styles.icon} />}
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
 
@@ -1,3 +1,2 @@
1
- export { default as FloatingSolveButton } from './FloatingSolveButton';
2
1
  export { default as InsertButton } from './InsertButton';
3
2
  export { default as ResultCandidatePicker } from './ResultCandidatePicker';
@@ -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,