@scrabble-solver/scrabble-solver 2.11.8 → 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 (115) 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 +1378 -682
  17. package/.next/server/chunks/44.js +3 -0
  18. package/.next/server/chunks/50.js +20 -78
  19. package/.next/server/chunks/911.js +14 -14
  20. package/.next/server/middleware-build-manifest.js +1 -1
  21. package/.next/server/pages/404.html +1 -1
  22. package/.next/server/pages/404.js.nft.json +1 -1
  23. package/.next/server/pages/500.html +1 -1
  24. package/.next/server/pages/_app.js +8 -0
  25. package/.next/server/pages/_app.js.nft.json +1 -1
  26. package/.next/server/pages/api/solve.js +43 -11
  27. package/.next/server/pages/index.html +1 -1
  28. package/.next/server/pages/index.js +144 -11
  29. package/.next/server/pages/index.js.nft.json +1 -1
  30. package/.next/server/pages/index.json +1 -1
  31. package/.next/server/pages-manifest.json +2 -2
  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/{c6e0e01f44fc0425.css → 60e8258da7362a1a.css} +1 -1
  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/useBoardStyle.ts +27 -0
  56. package/src/components/Board/hooks/useFloatingActions.ts +22 -0
  57. package/src/components/Board/hooks/useFloatingFocus.ts +10 -0
  58. package/src/components/Board/hooks/useFloatingInputPrompt.ts +19 -0
  59. package/src/components/Board/hooks/useGrid.ts +2 -1
  60. package/src/components/NavButtons/NavButtons.tsx +2 -2
  61. package/src/components/Rack/Rack.module.scss +6 -6
  62. package/src/components/Rack/Rack.tsx +98 -23
  63. package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +22 -0
  64. package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +89 -0
  65. package/src/components/Rack/components/InputPrompt/index.ts +1 -0
  66. package/src/components/Rack/components/RackTile/RackTile.module.scss +11 -0
  67. package/src/components/Rack/{RackTile.tsx → components/RackTile/RackTile.tsx} +47 -7
  68. package/src/components/Rack/components/RackTile/index.ts +1 -0
  69. package/src/components/Rack/components/index.ts +2 -0
  70. package/src/components/Radio/Radio.module.scss +0 -8
  71. package/src/components/Solver/Solver.module.scss +0 -20
  72. package/src/components/Solver/Solver.tsx +2 -4
  73. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -10
  74. package/src/components/Solver/components/index.ts +0 -1
  75. package/src/components/Tile/Tile.module.scss +1 -0
  76. package/src/components/Tile/Tile.tsx +8 -6
  77. package/src/components/Tile/TilePure.tsx +8 -0
  78. package/src/hooks/useAppLayout.ts +3 -1
  79. package/src/hooks/useLocalStorage.ts +8 -0
  80. package/src/i18n/de.json +6 -1
  81. package/src/i18n/en.json +6 -1
  82. package/src/i18n/es.json +6 -1
  83. package/src/i18n/fa.json +6 -1
  84. package/src/i18n/fr.json +6 -1
  85. package/src/i18n/pl.json +6 -1
  86. package/src/icons/Keyboard.svg +4 -3
  87. package/src/icons/KeyboardFill.svg +4 -0
  88. package/src/icons/index.ts +1 -0
  89. package/src/lib/extractCharacters.test.ts +26 -0
  90. package/src/lib/extractCharacters.ts +11 -9
  91. package/src/lib/extractCharactersByCase.test.ts +31 -0
  92. package/src/lib/extractCharactersByCase.ts +31 -0
  93. package/src/lib/index.ts +3 -1
  94. package/src/lib/isUpperCase.ts +7 -0
  95. package/src/modals/SettingsModal/SettingsModal.tsx +5 -1
  96. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss +12 -0
  97. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +55 -0
  98. package/src/modals/SettingsModal/components/InputModeSetting/index.ts +1 -0
  99. package/src/modals/SettingsModal/components/InputModeSetting/lib.ts +13 -0
  100. package/src/modals/SettingsModal/components/InputModeSetting/types.ts +7 -0
  101. package/src/modals/SettingsModal/components/index.ts +1 -0
  102. package/src/state/localStorage.ts +10 -1
  103. package/src/state/selectors.ts +2 -0
  104. package/src/state/slices/settingsInitialState.ts +4 -1
  105. package/src/state/slices/settingsSlice.ts +6 -1
  106. package/src/styles/mixins.scss +1 -0
  107. package/src/types/index.ts +7 -0
  108. package/.next/static/5ttGCAW8jcIKxpR8om9fK/_buildManifest.js +0 -1
  109. package/.next/static/chunks/pages/_app-76a8840b6244d5a2.js +0 -28
  110. package/.next/static/chunks/pages/index-6894f40e6cac9243.js +0 -1
  111. package/.next/static/css/af871fef886ef5b7.css +0 -2
  112. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +0 -7
  113. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.tsx +0 -53
  114. package/src/components/Solver/components/FloatingSolveButton/index.ts +0 -1
  115. /package/.next/static/{5ttGCAW8jcIKxpR8om9fK → 9oRWxnZ1xFLSs55FJtiYi}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.11.8",
3
+ "version": "2.11.9",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -28,22 +28,23 @@
28
28
  "start": "env-cmd next start -p 3333"
29
29
  },
30
30
  "dependencies": {
31
- "@floating-ui/react": "^0.21.1",
31
+ "@floating-ui/react": "^0.22.2",
32
32
  "@kamilmielnik/trie": "^2.0.1",
33
33
  "@reduxjs/toolkit": "^1.9.3",
34
- "@scrabble-solver/configs": "^2.11.8",
35
- "@scrabble-solver/constants": "^2.11.8",
36
- "@scrabble-solver/dictionaries": "^2.11.8",
37
- "@scrabble-solver/logger": "^2.11.8",
38
- "@scrabble-solver/solver": "^2.11.8",
39
- "@scrabble-solver/types": "^2.11.8",
40
- "@scrabble-solver/word-definitions": "^2.11.8",
34
+ "@scrabble-solver/configs": "^2.11.9",
35
+ "@scrabble-solver/constants": "^2.11.9",
36
+ "@scrabble-solver/dictionaries": "^2.11.9",
37
+ "@scrabble-solver/logger": "^2.11.9",
38
+ "@scrabble-solver/solver": "^2.11.9",
39
+ "@scrabble-solver/types": "^2.11.9",
40
+ "@scrabble-solver/word-definitions": "^2.11.9",
41
41
  "classnames": "^2.3.2",
42
42
  "include-media": "^2.0.0",
43
43
  "include-media-query-builder": "^1.1.0",
44
44
  "next": "^13.2.4",
45
45
  "normalize.css": "^8.0.1",
46
46
  "react": "^18.2.0",
47
+ "react-cool-onclickoutside": "^1.7.0",
47
48
  "react-dom": "^18.2.0",
48
49
  "react-modal": "^3.16.1",
49
50
  "react-portal": "^4.2.2",
@@ -57,9 +58,9 @@
57
58
  "workbox-window": "^6.5.4"
58
59
  },
59
60
  "devDependencies": {
60
- "@svgr/webpack": "^6.5.1",
61
+ "@svgr/webpack": "^7.0.0",
61
62
  "@types/classnames": "^2.3.0",
62
- "@types/react": "^18.0.28",
63
+ "@types/react": "^18.0.31",
63
64
  "@types/react-dom": "^18.0.11",
64
65
  "@types/react-modal": "^3.13.1",
65
66
  "@types/react-portal": "^4.0.4",
@@ -68,8 +69,8 @@
68
69
  "@types/redux": "^3.6.31",
69
70
  "@types/redux-saga": "^0.10.5",
70
71
  "env-cmd": "^10.1.0",
71
- "sass": "^1.59.3",
72
+ "sass": "^1.60.0",
72
73
  "workbox-webpack-plugin": "^6.5.4"
73
74
  },
74
- "gitHead": "fa0c31d85a98ce5704ff8e57741204a50071bdc6"
75
+ "gitHead": "ff5376346ee2ff7d0db2cbcbe98a47de1c96bea2"
75
76
  }
@@ -1,4 +1,5 @@
1
1
  @import 'styles/animations';
2
+ @import 'styles/mixins';
2
3
 
3
4
  .board {
4
5
  display: grid;
@@ -8,14 +9,27 @@
8
9
  border-radius: var(--border--radius);
9
10
  }
10
11
 
11
- .actions {
12
+ .floating {
12
13
  position: absolute;
13
14
  z-index: var(--z-index--actions);
14
15
  width: max-content;
15
16
  height: max-content;
16
- animation: var(--transition--duration) var(--transition--easing) hide;
17
+ animation: var(--transition--duration) var(--transition--easing) show;
18
+ }
19
+
20
+ .focus {
21
+ @include focus-effect;
22
+
23
+ cursor: text;
17
24
 
18
- &.shown {
19
- animation: var(--transition--duration) var(--transition--easing) show;
25
+ &::after {
26
+ content: '';
27
+ box-shadow: 0 0 0 var(--focus-effect--size) var(--color--focus);
20
28
  }
21
29
  }
30
+
31
+ .hidden {
32
+ pointer-events: none;
33
+ user-select: none;
34
+ animation: var(--transition--duration) var(--transition--easing) hide;
35
+ }
@@ -1,17 +1,28 @@
1
- import { autoUpdate, FloatingPortal, offset, shift, useFloating } from '@floating-ui/react';
1
+ /* eslint-disable max-lines, max-statements */
2
+
3
+ import { FloatingPortal, ReferenceType } from '@floating-ui/react';
4
+ import { EMPTY_CELL } from '@scrabble-solver/constants';
2
5
  import classNames from 'classnames';
3
- import { CSSProperties, FocusEventHandler, FunctionComponent, useCallback, useMemo, useState } from 'react';
6
+ import { CSSProperties, FocusEventHandler, FunctionComponent, useCallback, useState } from 'react';
7
+ import useOnclickOutside from 'react-cool-onclickoutside';
4
8
  import { useDispatch } from 'react-redux';
5
9
 
6
10
  import { useAppLayout } from 'hooks';
7
- import { getTileSizes } from 'lib';
8
- import { BOARD_CELL_ACTIONS_OFFSET, TRANSITION } from 'parameters';
9
- import { boardSlice, cellFilterSlice, selectConfig, selectRowsWithCandidate, useTypedSelector } from 'state';
11
+ import { TRANSITION } from 'parameters';
12
+ import {
13
+ boardSlice,
14
+ cellFilterSlice,
15
+ selectInputMode,
16
+ selectLocale,
17
+ selectRowsWithCandidate,
18
+ solveSlice,
19
+ useTypedSelector,
20
+ } from 'state';
10
21
 
11
22
  import styles from './Board.module.scss';
12
23
  import BoardPure from './BoardPure';
13
- import { Actions } from './components';
14
- import { useBackgroundImage, useGrid } from './hooks';
24
+ import { Actions, InputPrompt } from './components';
25
+ import { useBoardStyle, useFloatingActions, useFloatingFocus, useFloatingInputPrompt, useGrid } from './hooks';
15
26
 
16
27
  interface Props {
17
28
  className?: string;
@@ -19,91 +30,117 @@ interface Props {
19
30
 
20
31
  const Board: FunctionComponent<Props> = ({ className }) => {
21
32
  const dispatch = useDispatch();
33
+ const locale = useTypedSelector(selectLocale);
22
34
  const rows = useTypedSelector(selectRowsWithCandidate);
23
- const config = useTypedSelector(selectConfig);
24
- const { actionsWidth, cellSize } = useAppLayout();
25
- const { tileFontSize } = getTileSizes(cellSize);
26
-
27
- const [{ activeIndex, direction, inputRefs }, { onChange, onDirectionToggle, onFocus, onKeyDown, onPaste }] =
28
- useGrid(rows);
29
- const backgroundImage = useBackgroundImage();
30
- const boardStyle = useMemo<CSSProperties>(
31
- () => ({
32
- backgroundImage,
33
- fontSize: tileFontSize,
34
- gridTemplateColumns: `repeat(${config.boardWidth}, 1fr)`,
35
- gridTemplateRows: `repeat(${config.boardHeight}, 1fr)`,
36
- }),
37
- [backgroundImage, config.boardHeight, config.boardWidth, tileFontSize],
38
- );
39
- const [showActions, setShowActions] = useState(false);
35
+ const inputMode = useTypedSelector(selectInputMode);
36
+ const { cellSize } = useAppLayout();
37
+ const [
38
+ { activeIndex, direction, inputRefs },
39
+ { insertValue, onChange, onDirectionToggle, onFocus, onKeyDown, onPaste },
40
+ ] = useGrid(rows);
41
+ const boardStyle = useBoardStyle();
42
+ const [hasFocus, setHasFocus] = useState(false);
43
+ const [showInputPrompt, setShowInputPrompt] = useState(false);
40
44
  const [transition, setTransition] = useState<CSSProperties['transition']>(TRANSITION);
41
45
  const inputRef = inputRefs[activeIndex.y][activeIndex.x];
42
46
  const cell = rows[activeIndex.y][activeIndex.x];
43
-
44
- const { x, y, strategy, refs } = useFloating({
45
- middleware: [
46
- offset({
47
- mainAxis: -BOARD_CELL_ACTIONS_OFFSET,
48
- alignmentAxis: BOARD_CELL_ACTIONS_OFFSET - actionsWidth,
49
- }),
50
- shift(),
51
- ],
52
- placement: 'top-end',
53
- whileElementsMounted: autoUpdate,
54
- });
47
+ const floatingActions = useFloatingActions();
48
+ const floatingInputPrompt = useFloatingInputPrompt();
49
+ const floatingFocus = useFloatingFocus();
55
50
 
56
51
  const handleBlur: FocusEventHandler = useCallback(
57
52
  (event) => {
58
- const eventComesFromActions = refs.floating.current?.contains(event.relatedTarget);
59
- const eventComesFromBoard = event.currentTarget.contains(event.relatedTarget);
60
- const isLocalEvent = eventComesFromActions || eventComesFromBoard;
53
+ const comesFromActions = floatingActions.refs.floating.current?.contains(event.relatedTarget);
54
+ const comesFromBoard = event.currentTarget.contains(event.relatedTarget);
55
+ const comesFromFocus = floatingFocus.refs.floating.current?.contains(event.relatedTarget);
56
+ const comesFromInputPrompt = floatingInputPrompt.refs.floating.current?.contains(event.relatedTarget);
57
+ const isLocalEvent = comesFromActions || comesFromBoard || comesFromFocus || comesFromInputPrompt;
61
58
 
62
59
  if (!isLocalEvent) {
63
- setShowActions(false);
60
+ setHasFocus(false);
64
61
  }
65
62
  },
66
- [refs.floating],
63
+ [floatingActions.refs.floating, floatingFocus.refs.floating, floatingInputPrompt.refs.floating],
67
64
  );
68
65
 
69
- const handleDirectionToggle = useCallback(() => {
70
- inputRef.current?.focus();
71
- onDirectionToggle();
72
- }, [inputRef, onDirectionToggle]);
66
+ const updateFloatingReference = useCallback(
67
+ (newReference: ReferenceType | null) => {
68
+ floatingActions.refs.setReference(newReference);
69
+ floatingFocus.refs.setReference(newReference);
70
+ floatingInputPrompt.refs.setReference(newReference);
71
+ },
72
+ [floatingActions.refs, floatingFocus.refs, floatingInputPrompt.refs],
73
+ );
73
74
 
74
75
  const handleFocus: typeof onFocus = useCallback(
75
76
  (newX, newY) => {
76
- const isFirstFocus = !showActions;
77
- const originalTransition = refs.floating.current?.style.transition || '';
77
+ const isFirstFocus = !hasFocus;
78
+ const originalTransition = floatingActions.refs.floating.current?.style.transition || '';
78
79
  const newInputRef = inputRefs[newY][newX].current;
79
80
  const newTileElement = newInputRef?.parentElement || null;
80
81
 
81
- if (isFirstFocus) {
82
- setTransition('none');
83
- }
84
-
85
- refs.setReference(newTileElement);
82
+ updateFloatingReference(newTileElement);
86
83
  onFocus(newX, newY);
87
- setShowActions(true);
84
+ setHasFocus(true);
85
+ setShowInputPrompt(false);
88
86
 
89
87
  if (isFirstFocus) {
90
- setTimeout(() => {
88
+ setTransition('none');
89
+
90
+ globalThis.setTimeout(() => {
91
91
  setTransition(originalTransition);
92
92
  }, 0);
93
93
  }
94
94
  },
95
- [inputRefs, onFocus, refs.floating, refs.setReference, showActions],
95
+ [floatingActions.refs.floating, hasFocus, inputRefs, onFocus, updateFloatingReference],
96
+ );
97
+
98
+ const handleEnterWord = useCallback(() => {
99
+ setShowInputPrompt(true);
100
+ }, []);
101
+
102
+ const handleInsertWord = useCallback(
103
+ (word: string) => {
104
+ if (word.trim().length === 0) {
105
+ dispatch(boardSlice.actions.changeCellValue({ ...activeIndex, value: EMPTY_CELL }));
106
+ } else {
107
+ insertValue(activeIndex, word.toLocaleLowerCase(locale));
108
+ }
109
+
110
+ setShowInputPrompt(false);
111
+ dispatch(solveSlice.actions.submit());
112
+ setHasFocus(false);
113
+ },
114
+ [activeIndex, dispatch, locale],
96
115
  );
97
116
 
98
117
  const handleToggleBlank = useCallback(() => {
99
- inputRef.current?.focus();
118
+ if (inputMode === 'keyboard') {
119
+ inputRef.current?.focus();
120
+ }
121
+
100
122
  dispatch(boardSlice.actions.toggleCellIsBlank(cell));
101
- }, [cell, dispatch, inputRef]);
123
+ }, [cell, dispatch, inputMode, inputRef]);
124
+
125
+ const handleToggleDirection = useCallback(() => {
126
+ if (inputMode === 'keyboard') {
127
+ inputRef.current?.focus();
128
+ }
129
+
130
+ onDirectionToggle();
131
+ }, [inputMode, inputRef, onDirectionToggle]);
102
132
 
103
133
  const handleToggleFilterCell = useCallback(() => {
104
- inputRef.current?.focus();
134
+ if (inputMode === 'keyboard') {
135
+ inputRef.current?.focus();
136
+ }
137
+
105
138
  dispatch(cellFilterSlice.actions.toggle(cell));
106
- }, [cell, dispatch, inputRef]);
139
+ }, [cell, dispatch, inputMode, inputRef]);
140
+
141
+ const ref = useOnclickOutside(() => setHasFocus(false), {
142
+ ignoreClass: [styles.floating],
143
+ });
107
144
 
108
145
  return (
109
146
  <>
@@ -111,6 +148,7 @@ const Board: FunctionComponent<Props> = ({ className }) => {
111
148
  className={className}
112
149
  cellSize={cellSize}
113
150
  inputRefs={inputRefs}
151
+ ref={ref}
114
152
  rows={rows}
115
153
  style={boardStyle}
116
154
  onBlur={handleBlur}
@@ -121,28 +159,59 @@ const Board: FunctionComponent<Props> = ({ className }) => {
121
159
  />
122
160
 
123
161
  <FloatingPortal>
124
- <Actions
125
- cell={cell}
126
- className={classNames(styles.actions, {
127
- [styles.shown]: showActions,
162
+ <div
163
+ className={classNames(styles.floating, styles.focus, {
164
+ [styles.hidden]: !hasFocus,
128
165
  })}
129
- disabled={!showActions}
130
- direction={direction}
131
- ref={refs.setFloating}
166
+ ref={floatingFocus.refs.setFloating}
132
167
  style={{
133
- position: strategy,
134
- top: y ?? 0,
135
- left: x ?? 0,
168
+ position: floatingFocus.strategy,
169
+ top: floatingFocus.y ? floatingFocus.y + cellSize : 0,
170
+ left: floatingFocus.x ?? 0,
171
+ width: cellSize,
172
+ height: cellSize,
173
+ opacity: hasFocus ? 1 : 0,
174
+ visibility: floatingFocus.x === null || floatingFocus.y === null ? 'hidden' : 'visible',
136
175
  transition,
137
- opacity: showActions ? 1 : 0,
138
- pointerEvents: showActions ? 'auto' : 'none',
139
- userSelect: showActions ? 'auto' : 'none',
140
- visibility: x === null || y === null ? 'hidden' : 'visible',
141
176
  }}
142
- onDirectionToggle={handleDirectionToggle}
143
- onToggleBlank={handleToggleBlank}
144
- onToggleFilterCell={handleToggleFilterCell}
177
+ tabIndex={0}
145
178
  />
179
+
180
+ {hasFocus && !showInputPrompt && (
181
+ <Actions
182
+ cell={cell}
183
+ className={styles.floating}
184
+ direction={direction}
185
+ ref={floatingActions.refs.setFloating}
186
+ style={{
187
+ position: floatingActions.strategy,
188
+ top: floatingActions.y ?? 0,
189
+ left: floatingActions.x ?? 0,
190
+ transition,
191
+ }}
192
+ onDirectionToggle={handleToggleDirection}
193
+ onEnterWord={handleEnterWord}
194
+ onToggleBlank={handleToggleBlank}
195
+ onToggleFilterCell={handleToggleFilterCell}
196
+ />
197
+ )}
198
+
199
+ {hasFocus && showInputPrompt && (
200
+ <InputPrompt
201
+ className={styles.floating}
202
+ direction={direction}
203
+ initialValue={cell.tile.character}
204
+ ref={floatingInputPrompt.refs.setFloating}
205
+ style={{
206
+ position: floatingInputPrompt.strategy,
207
+ top: floatingInputPrompt.y ?? 0,
208
+ left: floatingInputPrompt.x ?? 0,
209
+ transition,
210
+ }}
211
+ onDirectionToggle={handleToggleDirection}
212
+ onSubmit={handleInsertWord}
213
+ />
214
+ )}
146
215
  </FloatingPortal>
147
216
  </>
148
217
  );
@@ -5,8 +5,8 @@ import {
5
5
  ClipboardEventHandler,
6
6
  CSSProperties,
7
7
  FocusEventHandler,
8
+ forwardRef,
8
9
  Fragment,
9
- FunctionComponent,
10
10
  KeyboardEventHandler,
11
11
  memo,
12
12
  RefObject,
@@ -28,45 +28,37 @@ interface Props {
28
28
  onPaste: ClipboardEventHandler<HTMLInputElement>;
29
29
  }
30
30
 
31
- const BoardPure: FunctionComponent<Props> = ({
32
- className,
33
- cellSize,
34
- inputRefs,
35
- rows,
36
- style,
37
- onBlur,
38
- onChange,
39
- onFocus,
40
- onKeyDown,
41
- onPaste,
42
- }) => (
43
- <div
44
- className={classNames(styles.board, className)}
45
- style={style}
46
- onBlur={onBlur}
47
- onKeyDown={onKeyDown}
48
- onPaste={onPaste}
49
- >
50
- {rows.map((cells, y) => (
51
- <Fragment key={y}>
52
- {cells.map((cell, x) => (
53
- <Cell
54
- className={styles.cell}
55
- cell={cell}
56
- cellBottom={y < rows.length - 1 ? rows[y + 1][x] : undefined}
57
- cellLeft={x > 0 ? rows[y][x - 1] : undefined}
58
- cellRight={x < rows.length - 1 ? rows[y][x + 1] : undefined}
59
- cellTop={y > 0 ? rows[y - 1][x] : undefined}
60
- inputRef={inputRefs[y][x]}
61
- key={x}
62
- size={cellSize}
63
- onChange={onChange}
64
- onFocus={onFocus}
65
- />
66
- ))}
67
- </Fragment>
68
- ))}
69
- </div>
31
+ const BoardPure = forwardRef<HTMLDivElement, Props>(
32
+ ({ className, cellSize, inputRefs, rows, style, onBlur, onChange, onFocus, onKeyDown, onPaste }, ref) => (
33
+ <div
34
+ className={classNames(styles.board, className)}
35
+ ref={ref}
36
+ style={style}
37
+ onBlur={onBlur}
38
+ onKeyDown={onKeyDown}
39
+ onPaste={onPaste}
40
+ >
41
+ {rows.map((cells, y) => (
42
+ <Fragment key={y}>
43
+ {cells.map((cell, x) => (
44
+ <Cell
45
+ className={styles.cell}
46
+ cell={cell}
47
+ cellBottom={y < rows.length - 1 ? rows[y + 1][x] : undefined}
48
+ cellLeft={x > 0 ? rows[y][x - 1] : undefined}
49
+ cellRight={x < rows.length - 1 ? rows[y][x + 1] : undefined}
50
+ cellTop={y > 0 ? rows[y - 1][x] : undefined}
51
+ inputRef={inputRefs[y][x]}
52
+ key={x}
53
+ size={cellSize}
54
+ onChange={onChange}
55
+ onFocus={onFocus}
56
+ />
57
+ ))}
58
+ </Fragment>
59
+ ))}
60
+ </div>
61
+ ),
70
62
  );
71
63
 
72
64
  export default memo(BoardPure);
@@ -5,12 +5,18 @@
5
5
  box-shadow: var(--box-shadow);
6
6
  border-radius: var(--border--radius);
7
7
  transition: var(--transition);
8
+ transition-property: opacity;
8
9
  }
9
10
 
10
11
  .action {
11
12
  padding: var(--spacing--m);
12
13
  box-shadow: none !important;
13
14
 
15
+ &:active,
16
+ &:hover {
17
+ color: var(--color--foreground);
18
+ }
19
+
14
20
  & + & {
15
21
  [dir='ltr'] & {
16
22
  border-left: none;
@@ -44,21 +50,4 @@
44
50
  border-bottom-right-radius: 0;
45
51
  }
46
52
  }
47
-
48
- &:active,
49
- &:hover {
50
- color: var(--color--foreground);
51
- }
52
- }
53
-
54
- .toggleDirection {
55
- transition: var(--transition);
56
-
57
- &.right {
58
- transform: rotate(-90deg);
59
-
60
- [dir='rtl'] & {
61
- transform: rotate(90deg);
62
- }
63
- }
64
53
  }
@@ -3,25 +3,38 @@ import { Cell } from '@scrabble-solver/types';
3
3
  import classNames from 'classnames';
4
4
  import { forwardRef, HTMLProps, MouseEventHandler } from 'react';
5
5
 
6
- import { ArrowDown, Flag, FlagFill, Square, SquareFill } from 'icons';
6
+ import { Flag, FlagFill, Keyboard, Square, SquareFill } from 'icons';
7
7
  import { findCell } from 'lib';
8
- import { selectCellIsFiltered, selectResultCandidateCells, useTranslate, useTypedSelector } from 'state';
8
+ import {
9
+ selectCellIsFiltered,
10
+ selectInputMode,
11
+ selectResultCandidateCells,
12
+ useTranslate,
13
+ useTypedSelector,
14
+ } from 'state';
15
+ import { Direction } from 'types';
9
16
 
10
17
  import Button from '../../../Button';
18
+ import ToggleDirectionButton from '../ToggleDirectionButton';
11
19
 
12
20
  import styles from './Actions.module.scss';
13
21
 
14
22
  interface Props extends HTMLProps<HTMLDivElement> {
15
23
  cell: Cell;
16
- direction: 'horizontal' | 'vertical';
24
+ direction: Direction;
17
25
  onDirectionToggle: MouseEventHandler<HTMLButtonElement>;
26
+ onEnterWord: MouseEventHandler<HTMLButtonElement>;
18
27
  onToggleBlank: MouseEventHandler<HTMLButtonElement>;
19
28
  onToggleFilterCell: MouseEventHandler<HTMLButtonElement>;
20
29
  }
21
30
 
22
31
  const Actions = forwardRef<HTMLDivElement, Props>(
23
- ({ cell, className, direction, disabled, onDirectionToggle, onToggleBlank, onToggleFilterCell, ...props }, ref) => {
32
+ (
33
+ { cell, className, direction, onDirectionToggle, onEnterWord, onToggleBlank, onToggleFilterCell, ...props },
34
+ ref,
35
+ ) => {
24
36
  const translate = useTranslate();
37
+ const inputMode = useTypedSelector(selectInputMode);
25
38
  const isFiltered = useTypedSelector((state) => selectCellIsFiltered(state, cell));
26
39
  const resultCandidateCells = useTypedSelector(selectResultCandidateCells);
27
40
  const isBlank = cell.tile.isBlank;
@@ -32,25 +45,31 @@ const Actions = forwardRef<HTMLDivElement, Props>(
32
45
 
33
46
  return (
34
47
  <div className={classNames(styles.actions, className)} ref={ref} {...props}>
35
- <Button
36
- aria-label={translate('cell.toggle-direction')}
37
- className={styles.action}
38
- Icon={ArrowDown}
39
- iconClassName={classNames(styles.toggleDirection, {
40
- [styles.right]: direction === 'horizontal',
41
- })}
42
- tabIndex={disabled ? -1 : undefined}
43
- tooltip={translate('cell.toggle-direction')}
44
- onClick={onDirectionToggle}
45
- onMouseDown={handleMouseDown}
46
- />
48
+ {inputMode === 'touchscreen' && (
49
+ <Button
50
+ aria-label={translate('cell.enter-word')}
51
+ className={styles.action}
52
+ Icon={Keyboard}
53
+ tooltip={translate('cell.enter-word')}
54
+ onClick={onEnterWord}
55
+ onMouseDown={handleMouseDown}
56
+ />
57
+ )}
58
+
59
+ {inputMode === 'keyboard' && (
60
+ <ToggleDirectionButton
61
+ className={styles.action}
62
+ direction={direction}
63
+ onClick={onDirectionToggle}
64
+ onMouseDown={handleMouseDown}
65
+ />
66
+ )}
47
67
 
48
68
  {isEmpty && (
49
69
  <Button
50
70
  aria-label={translate('cell.filter-cell')}
51
71
  className={classNames(styles.action)}
52
72
  Icon={isFiltered ? Flag : FlagFill}
53
- tabIndex={disabled ? -1 : undefined}
54
73
  tooltip={translate('cell.filter-cell')}
55
74
  onClick={onToggleFilterCell}
56
75
  onMouseDown={handleMouseDown}
@@ -62,7 +81,6 @@ const Actions = forwardRef<HTMLDivElement, Props>(
62
81
  aria-label={isBlank ? translate('cell.set-not-blank') : translate('cell.set-blank')}
63
82
  className={styles.action}
64
83
  Icon={isBlank ? SquareFill : Square}
65
- tabIndex={disabled ? -1 : undefined}
66
84
  tooltip={isBlank ? translate('cell.set-not-blank') : translate('cell.set-blank')}
67
85
  onClick={onToggleBlank}
68
86
  onMouseDown={handleMouseDown}
@@ -17,18 +17,18 @@
17
17
  }
18
18
 
19
19
  [dir='ltr'] & {
20
- &:nth-child(1),
21
- &:nth-child(2),
22
- &:nth-child(3) {
20
+ &:nth-child(15n + 1),
21
+ &:nth-child(15n + 2),
22
+ &:nth-child(15n + 3) {
23
23
  input {
24
24
  left: 0;
25
25
  clip-path: polygon(0 (100% / 3), (100% / 3) (100% / 3), (100% / 3) (200% / 3), 0 (200% / 3));
26
26
  }
27
27
  }
28
28
 
29
- &:nth-last-child(1),
30
- &:nth-last-child(2),
31
- &:nth-last-child(3) {
29
+ &:nth-last-child(15n + 1),
30
+ &:nth-last-child(15n + 2),
31
+ &:nth-last-child(15n + 3) {
32
32
  input {
33
33
  left: -200%;
34
34
  clip-path: polygon((200% / 3) (100% / 3), 100% (100% / 3), 100% (200% / 3), (200% / 3) (200% / 3));
@@ -37,9 +37,9 @@
37
37
  }
38
38
 
39
39
  [dir='rtl'] & {
40
- &:nth-child(1),
41
- &:nth-child(2),
42
- &:nth-child(3) {
40
+ &:nth-child(15n + 1),
41
+ &:nth-child(15n + 2),
42
+ &:nth-child(15n + 3) {
43
43
  input {
44
44
  left: -200%;
45
45
  right: 0;
@@ -47,9 +47,9 @@
47
47
  }
48
48
  }
49
49
 
50
- &:nth-last-child(1),
51
- &:nth-last-child(2),
52
- &:nth-last-child(3) {
50
+ &:nth-last-child(15n + 1),
51
+ &:nth-last-child(15n + 2),
52
+ &:nth-last-child(15n + 3) {
53
53
  input {
54
54
  left: 0;
55
55
  right: -200%;
@@ -60,7 +60,6 @@
60
60
  }
61
61
 
62
62
  .tile {
63
- @include focus-effect;
64
63
  @include lighthouse-input-size-hack;
65
64
 
66
65
  &.sharpTopLeft {