@scrabble-solver/scrabble-solver 2.11.8 → 2.12.0

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 (120) 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 +1426 -718
  17. package/.next/server/chunks/44.js +6 -3
  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 +16 -0
  25. package/.next/server/pages/_app.js.nft.json +1 -1
  26. package/.next/server/pages/_document.js.nft.json +1 -1
  27. package/.next/server/pages/api/solve.js +43 -11
  28. package/.next/server/pages/index.html +1 -1
  29. package/.next/server/pages/index.js +152 -11
  30. package/.next/server/pages/index.js.nft.json +1 -1
  31. package/.next/server/pages/index.json +1 -1
  32. package/.next/server/pages-manifest.json +2 -2
  33. package/.next/static/chunks/pages/{404-ca203fa27afc37d8.js → 404-b4b5ce15153d4825.js} +1 -1
  34. package/.next/static/chunks/pages/_app-bea4539a6b8042de.js +32 -0
  35. package/.next/static/chunks/pages/index-4e8566409753e1c3.js +1 -0
  36. package/.next/static/css/58053f9594647860.css +2 -0
  37. package/.next/static/css/{c6e0e01f44fc0425.css → 60e8258da7362a1a.css} +1 -1
  38. package/.next/static/fsjQvvJ13WNxBdMioL4sc/_buildManifest.js +1 -0
  39. package/.next/trace +52 -50
  40. package/package.json +16 -13
  41. package/src/components/Board/Board.module.scss +18 -4
  42. package/src/components/Board/Board.tsx +145 -76
  43. package/src/components/Board/BoardPure.tsx +32 -40
  44. package/src/components/Board/components/Actions/Actions.module.scss +6 -17
  45. package/src/components/Board/components/Actions/Actions.tsx +36 -18
  46. package/src/components/Board/components/Cell/Cell.module.scss +12 -13
  47. package/src/components/Board/components/Cell/Cell.tsx +53 -3
  48. package/src/components/Board/components/InputPrompt/InputPrompt.module.scss +48 -0
  49. package/src/components/Board/components/InputPrompt/InputPrompt.tsx +81 -0
  50. package/src/components/Board/components/InputPrompt/index.ts +1 -0
  51. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss +21 -0
  52. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +34 -0
  53. package/src/components/Board/components/ToggleDirectionButton/index.ts +1 -0
  54. package/src/components/Board/components/index.ts +2 -0
  55. package/src/components/Board/hooks/index.ts +4 -0
  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 +2 -1
  61. package/src/components/NavButtons/NavButtons.tsx +2 -2
  62. package/src/components/Rack/Rack.module.scss +6 -6
  63. package/src/components/Rack/Rack.tsx +98 -23
  64. package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +22 -0
  65. package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +89 -0
  66. package/src/components/Rack/components/InputPrompt/index.ts +1 -0
  67. package/src/components/Rack/components/RackTile/RackTile.module.scss +11 -0
  68. package/src/components/Rack/{RackTile.tsx → components/RackTile/RackTile.tsx} +47 -7
  69. package/src/components/Rack/components/RackTile/index.ts +1 -0
  70. package/src/components/Rack/components/index.ts +2 -0
  71. package/src/components/Radio/Radio.module.scss +0 -8
  72. package/src/components/Results/Cell.tsx +4 -3
  73. package/src/components/Results/Result.tsx +6 -2
  74. package/src/components/Results/Results.module.scss +6 -0
  75. package/src/components/Solver/Solver.module.scss +0 -20
  76. package/src/components/Solver/Solver.tsx +2 -4
  77. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -10
  78. package/src/components/Solver/components/index.ts +0 -1
  79. package/src/components/Tile/Tile.module.scss +1 -0
  80. package/src/components/Tile/Tile.tsx +8 -6
  81. package/src/components/Tile/TilePure.tsx +8 -0
  82. package/src/hooks/useAppLayout.ts +3 -1
  83. package/src/hooks/useLocalStorage.ts +8 -0
  84. package/src/i18n/de.json +6 -1
  85. package/src/i18n/en.json +6 -1
  86. package/src/i18n/es.json +6 -1
  87. package/src/i18n/fa.json +6 -1
  88. package/src/i18n/fr.json +6 -1
  89. package/src/i18n/pl.json +6 -1
  90. package/src/icons/Keyboard.svg +4 -3
  91. package/src/icons/KeyboardFill.svg +4 -0
  92. package/src/icons/index.ts +1 -0
  93. package/src/lib/extractCharacters.test.ts +26 -0
  94. package/src/lib/extractCharacters.ts +11 -9
  95. package/src/lib/extractCharactersByCase.test.ts +31 -0
  96. package/src/lib/extractCharactersByCase.ts +31 -0
  97. package/src/lib/index.ts +3 -1
  98. package/src/lib/isUpperCase.ts +7 -0
  99. package/src/modals/SettingsModal/SettingsModal.tsx +5 -1
  100. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss +12 -0
  101. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +55 -0
  102. package/src/modals/SettingsModal/components/InputModeSetting/index.ts +1 -0
  103. package/src/modals/SettingsModal/components/InputModeSetting/lib.ts +13 -0
  104. package/src/modals/SettingsModal/components/InputModeSetting/types.ts +7 -0
  105. package/src/modals/SettingsModal/components/index.ts +1 -0
  106. package/src/state/localStorage.ts +10 -1
  107. package/src/state/selectors.ts +2 -0
  108. package/src/state/slices/settingsInitialState.ts +4 -1
  109. package/src/state/slices/settingsSlice.ts +6 -1
  110. package/src/styles/mixins.scss +1 -0
  111. package/src/styles/variables.scss +1 -0
  112. package/src/types/index.ts +7 -0
  113. package/.next/static/5ttGCAW8jcIKxpR8om9fK/_buildManifest.js +0 -1
  114. package/.next/static/chunks/pages/_app-76a8840b6244d5a2.js +0 -28
  115. package/.next/static/chunks/pages/index-6894f40e6cac9243.js +0 -1
  116. package/.next/static/css/af871fef886ef5b7.css +0 -2
  117. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +0 -7
  118. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.tsx +0 -53
  119. package/src/components/Solver/components/FloatingSolveButton/index.ts +0 -1
  120. /package/.next/static/{5ttGCAW8jcIKxpR8om9fK → fsjQvvJ13WNxBdMioL4sc}/_ssgManifest.js +0 -0
@@ -1,9 +1,24 @@
1
1
  import { EMPTY_CELL } from '@scrabble-solver/constants';
2
2
  import { Cell as CellModel } from '@scrabble-solver/types';
3
3
  import classNames from 'classnames';
4
- import { ChangeEventHandler, FunctionComponent, RefObject, useCallback } from 'react';
4
+ import {
5
+ ChangeEventHandler,
6
+ FocusEventHandler,
7
+ FunctionComponent,
8
+ MouseEventHandler,
9
+ RefObject,
10
+ TouchEventHandler,
11
+ useCallback,
12
+ } from 'react';
5
13
 
6
- import { selectCellIsValid, selectLocale, selectTilePoints, useTranslate, useTypedSelector } from 'state';
14
+ import {
15
+ selectCellIsValid,
16
+ selectInputMode,
17
+ selectLocale,
18
+ selectTilePoints,
19
+ useTranslate,
20
+ useTypedSelector,
21
+ } from 'state';
7
22
 
8
23
  import Tile from '../../../Tile';
9
24
 
@@ -37,11 +52,44 @@ const Cell: FunctionComponent<Props> = ({
37
52
  const { tile, x, y } = cell;
38
53
  const translate = useTranslate();
39
54
  const locale = useTypedSelector(selectLocale);
55
+ const inputMode = useTypedSelector(selectInputMode);
40
56
  const points = useTypedSelector((state) => selectTilePoints(state, cell.tile));
41
57
  const isValid = useTypedSelector((state) => selectCellIsValid(state, cell));
42
58
  const isEmpty = tile.character === EMPTY_CELL;
43
59
 
44
- const handleFocus = useCallback(() => onFocus(x, y), [x, y, onFocus]);
60
+ const handleFocus: FocusEventHandler<HTMLInputElement> = useCallback(
61
+ (event) => {
62
+ if (inputMode === 'touchscreen') {
63
+ event.preventDefault();
64
+ event.target.blur();
65
+ }
66
+
67
+ onFocus(x, y);
68
+ },
69
+ [inputMode, onFocus, x, y],
70
+ );
71
+
72
+ const handleMouseDown: MouseEventHandler<HTMLInputElement> = useCallback(
73
+ (event) => {
74
+ if (inputMode === 'touchscreen') {
75
+ event.preventDefault();
76
+ }
77
+
78
+ onFocus(x, y);
79
+ },
80
+ [inputMode, onFocus, x, y],
81
+ );
82
+
83
+ const handleTouchStart: TouchEventHandler<HTMLInputElement> = useCallback(
84
+ (event) => {
85
+ if (inputMode === 'touchscreen') {
86
+ event.preventDefault();
87
+ }
88
+
89
+ onFocus(x, y);
90
+ },
91
+ [inputMode, onFocus, x, y],
92
+ );
45
93
 
46
94
  return (
47
95
  <Tile
@@ -66,6 +114,8 @@ const Cell: FunctionComponent<Props> = ({
66
114
  tabIndex={cell.x === 0 && cell.y === 0 ? undefined : -1}
67
115
  onChange={onChange}
68
116
  onFocus={handleFocus}
117
+ onMouseDown={handleMouseDown}
118
+ onTouchStart={handleTouchStart}
69
119
  />
70
120
  );
71
121
  };
@@ -0,0 +1,48 @@
1
+ @import 'styles/mixins';
2
+
3
+ .inputPrompt {
4
+ display: flex;
5
+ box-shadow: var(--box-shadow);
6
+ border-radius: var(--border--radius);
7
+ transition: var(--transition);
8
+ transition-property: opacity;
9
+ }
10
+
11
+ .toggleDirection {
12
+ [dir='ltr'] & {
13
+ border-top-right-radius: 0;
14
+ border-bottom-right-radius: 0;
15
+ }
16
+
17
+ [dir='rtl'] & {
18
+ border-top-left-radius: 0;
19
+ border-bottom-left-radius: 0;
20
+ }
21
+ }
22
+
23
+ .input {
24
+ @include text-input;
25
+
26
+ height: 100%;
27
+ max-width: 200px;
28
+ border: none;
29
+ border-top: var(--border);
30
+ border-bottom: var(--border);
31
+ border-radius: 0;
32
+ }
33
+
34
+ .insert {
35
+ [dir='ltr'] & {
36
+ border-top-left-radius: 0;
37
+ border-bottom-left-radius: 0;
38
+ }
39
+
40
+ [dir='rtl'] & {
41
+ border-top-right-radius: 0;
42
+ border-bottom-right-radius: 0;
43
+ }
44
+ }
45
+
46
+ .insertIcon {
47
+ transform: scale(1.5);
48
+ }
@@ -0,0 +1,81 @@
1
+ import classNames from 'classnames';
2
+ import { FormEventHandler, forwardRef, HTMLProps, MouseEventHandler, useEffect, useState } from 'react';
3
+
4
+ import { Check } from 'icons';
5
+ import { useTranslate } from 'state';
6
+ import { Direction } from 'types';
7
+
8
+ import Button from '../../../Button';
9
+ import ToggleDirectionButton from '../ToggleDirectionButton';
10
+
11
+ import styles from './InputPrompt.module.scss';
12
+
13
+ interface Props extends Omit<HTMLProps<HTMLFormElement>, 'onSubmit'> {
14
+ className?: string;
15
+ direction: Direction;
16
+ initialValue: string;
17
+ onDirectionToggle: MouseEventHandler<HTMLButtonElement>;
18
+ onSubmit: (input: string) => void;
19
+ }
20
+
21
+ const InputPrompt = forwardRef<HTMLFormElement, Props>(
22
+ ({ className, direction, initialValue, onDirectionToggle, onSubmit, ...props }, ref) => {
23
+ const translate = useTranslate();
24
+ const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
25
+ const [input, setInput] = useState(initialValue.trim());
26
+
27
+ // On iOS it helps with losing focus too early which makes Actions disappear
28
+ const handleMouseDown: MouseEventHandler = (event) => event.preventDefault();
29
+
30
+ const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
31
+ event.preventDefault();
32
+ event.stopPropagation();
33
+ onSubmit(input);
34
+ };
35
+
36
+ useEffect(() => {
37
+ if (inputRef) {
38
+ inputRef.focus();
39
+ inputRef.select();
40
+ }
41
+ }, [inputRef]);
42
+
43
+ return (
44
+ <form className={classNames(styles.inputPrompt, className)} ref={ref} onSubmit={handleSubmit} {...props}>
45
+ <ToggleDirectionButton
46
+ className={styles.toggleDirection}
47
+ direction={direction}
48
+ onClick={onDirectionToggle}
49
+ onMouseDown={handleMouseDown}
50
+ />
51
+
52
+ <div>
53
+ <input
54
+ autoCapitalize="none"
55
+ autoComplete="off"
56
+ autoCorrect="off"
57
+ className={styles.input}
58
+ placeholder={translate('rack.placeholder')}
59
+ spellCheck={false}
60
+ ref={setInputRef}
61
+ value={input}
62
+ onChange={(event) => setInput(event.target.value)}
63
+ />
64
+ </div>
65
+
66
+ <Button
67
+ aria-label={translate('results.insert')}
68
+ className={styles.insert}
69
+ Icon={Check}
70
+ iconClassName={styles.insertIcon}
71
+ tooltip={translate('results.insert')}
72
+ type="submit"
73
+ variant="primary"
74
+ onMouseDown={handleMouseDown}
75
+ />
76
+ </form>
77
+ );
78
+ },
79
+ );
80
+
81
+ export default InputPrompt;
@@ -0,0 +1 @@
1
+ export { default } from './InputPrompt';
@@ -0,0 +1,21 @@
1
+ .button {
2
+ padding: var(--spacing--m);
3
+ box-shadow: none !important;
4
+
5
+ &:active,
6
+ &:hover {
7
+ color: var(--color--foreground);
8
+ }
9
+ }
10
+
11
+ .icon {
12
+ transition: var(--transition);
13
+
14
+ &.right {
15
+ transform: rotate(-90deg);
16
+
17
+ [dir='rtl'] & {
18
+ transform: rotate(90deg);
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,34 @@
1
+ import classNames from 'classnames';
2
+ import { ButtonHTMLAttributes, FunctionComponent } from 'react';
3
+
4
+ import { ArrowDown } from 'icons';
5
+ import { useTranslate } from 'state';
6
+ import { Direction } from 'types';
7
+
8
+ import Button from '../../../Button';
9
+
10
+ import styles from './ToggleDirectionButton.module.scss';
11
+
12
+ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
13
+ className?: string;
14
+ direction: Direction;
15
+ }
16
+
17
+ const ToggleDirectionButton: FunctionComponent<Props> = ({ className, direction, ...props }) => {
18
+ const translate = useTranslate();
19
+
20
+ return (
21
+ <Button
22
+ aria-label={translate('cell.toggle-direction')}
23
+ className={classNames(styles.button, className)}
24
+ Icon={ArrowDown}
25
+ iconClassName={classNames(styles.icon, {
26
+ [styles.right]: direction === 'horizontal',
27
+ })}
28
+ tooltip={translate('cell.toggle-direction')}
29
+ {...props}
30
+ />
31
+ );
32
+ };
33
+
34
+ export default ToggleDirectionButton;
@@ -0,0 +1 @@
1
+ export { default } from './ToggleDirectionButton';
@@ -1,2 +1,4 @@
1
1
  export { default as Actions } from './Actions';
2
2
  export { default as Cell } from './Cell';
3
+ export { default as InputPrompt } from './InputPrompt';
4
+ export { default as ToggleDirectionButton } from './ToggleDirectionButton';
@@ -1,2 +1,6 @@
1
1
  export { default as useBackgroundImage } from './useBackgroundImage';
2
+ export { default as useBoardStyle } from './useBoardStyle';
3
+ export { default as useFloatingActions } from './useFloatingActions';
4
+ export { default as useFloatingFocus } from './useFloatingFocus';
5
+ export { default as useFloatingInputPrompt } from './useFloatingInputPrompt';
2
6
  export { default as useGrid } from './useGrid';
@@ -0,0 +1,27 @@
1
+ import { CSSProperties, useMemo } from 'react';
2
+
3
+ import { useAppLayout } from 'hooks';
4
+ import { getTileSizes } from 'lib';
5
+ import { selectConfig, useTypedSelector } from 'state';
6
+
7
+ import useBackgroundImage from './useBackgroundImage';
8
+
9
+ const useBoardStyle = () => {
10
+ const config = useTypedSelector(selectConfig);
11
+ const { cellSize } = useAppLayout();
12
+ const { tileFontSize } = getTileSizes(cellSize);
13
+ const backgroundImage = useBackgroundImage();
14
+ const boardStyle = useMemo<CSSProperties>(
15
+ () => ({
16
+ backgroundImage,
17
+ fontSize: tileFontSize,
18
+ gridTemplateColumns: `repeat(${config.boardWidth}, 1fr)`,
19
+ gridTemplateRows: `repeat(${config.boardHeight}, 1fr)`,
20
+ }),
21
+ [backgroundImage, config.boardHeight, config.boardWidth, tileFontSize],
22
+ );
23
+
24
+ return boardStyle;
25
+ };
26
+
27
+ export default useBoardStyle;
@@ -0,0 +1,22 @@
1
+ import { autoUpdate, offset, shift, useFloating } from '@floating-ui/react';
2
+
3
+ import { useAppLayout } from 'hooks';
4
+ import { BOARD_CELL_ACTIONS_OFFSET } from 'parameters';
5
+
6
+ const useFloatingActions = () => {
7
+ const { actionsWidth } = useAppLayout();
8
+
9
+ return useFloating({
10
+ middleware: [
11
+ offset({
12
+ mainAxis: -BOARD_CELL_ACTIONS_OFFSET,
13
+ alignmentAxis: BOARD_CELL_ACTIONS_OFFSET - actionsWidth,
14
+ }),
15
+ shift(),
16
+ ],
17
+ placement: 'top-end',
18
+ whileElementsMounted: autoUpdate,
19
+ });
20
+ };
21
+
22
+ export default useFloatingActions;
@@ -0,0 +1,10 @@
1
+ import { autoUpdate, useFloating } from '@floating-ui/react';
2
+
3
+ const useFloatingFocus = () => {
4
+ return useFloating({
5
+ placement: 'top-start',
6
+ whileElementsMounted: autoUpdate,
7
+ });
8
+ };
9
+
10
+ export default useFloatingFocus;
@@ -0,0 +1,19 @@
1
+ import { autoUpdate, offset, shift, useFloating } from '@floating-ui/react';
2
+
3
+ import { BOARD_CELL_ACTIONS_OFFSET } from 'parameters';
4
+
5
+ const useFloatingInputPrompt = () => {
6
+ return useFloating({
7
+ middleware: [
8
+ offset({
9
+ mainAxis: -BOARD_CELL_ACTIONS_OFFSET,
10
+ alignmentAxis: BOARD_CELL_ACTIONS_OFFSET,
11
+ }),
12
+ shift(),
13
+ ],
14
+ placement: 'top',
15
+ whileElementsMounted: autoUpdate,
16
+ });
17
+ };
18
+
19
+ export default useFloatingInputPrompt;
@@ -32,6 +32,7 @@ interface State {
32
32
  }
33
33
 
34
34
  interface Actions {
35
+ insertValue: (position: Point, value: string) => void;
35
36
  onChange: ChangeEventHandler<HTMLInputElement>;
36
37
  onDirectionToggle: () => void;
37
38
  onFocus: (x: number, y: number) => void;
@@ -358,7 +359,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
358
359
 
359
360
  return [
360
361
  { activeIndex, direction, inputRefs },
361
- { onChange, onDirectionToggle, onFocus, onKeyDown, onPaste },
362
+ { insertValue, onChange, onDirectionToggle, onFocus, onKeyDown, onPaste },
362
363
  ];
363
364
  };
364
365
 
@@ -2,7 +2,7 @@ import classNames from 'classnames';
2
2
  import { FunctionComponent, memo } from 'react';
3
3
 
4
4
  import { useAppLayout } from 'hooks';
5
- import { CardChecklist, Cog, Eraser, Github, Keyboard, List, Sack } from 'icons';
5
+ import { CardChecklist, Cog, Eraser, Github, KeyboardFill, List, Sack } from 'icons';
6
6
  import { GITHUB_PROJECT_URL } from 'parameters';
7
7
  import { selectHasInvalidWords, selectHasOverusedTiles, useTranslate, useTypedSelector } from 'state';
8
8
 
@@ -117,7 +117,7 @@ const NavButtons: FunctionComponent<Props> = ({
117
117
  <IconButton
118
118
  aria-label={translate('keyMap')}
119
119
  className={styles.button}
120
- Icon={Keyboard}
120
+ Icon={KeyboardFill}
121
121
  tooltip={translate('keyMap')}
122
122
  onClick={onShowKeyMap}
123
123
  />
@@ -60,20 +60,20 @@
60
60
  }
61
61
 
62
62
  .rack {
63
+ position: relative;
63
64
  display: flex;
64
65
  box-shadow: var(--box-shadow);
65
66
  border-radius: var(--border--radius);
67
+ transition: var(--transition);
68
+ opacity: 1;
66
69
  }
67
70
 
68
71
  .tile {
69
- @include focus-effect;
70
72
  @include lighthouse-input-size-hack;
73
+ }
71
74
 
72
- --background-color: var(--color--background);
73
-
74
- &:focus-within {
75
- z-index: 2;
76
- }
75
+ .hidden {
76
+ opacity: 0;
77
77
  }
78
78
 
79
79
  .sharpLeft {
@@ -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,
@@ -12,10 +26,18 @@ import {
12
26
  isCtrl,
13
27
  zipCharactersAndTiles,
14
28
  } from 'lib';
15
- import { rackSlice, selectConfig, selectLocale, selectRack, selectResultCandidateTiles, useTypedSelector } from 'state';
16
-
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';
17
40
  import styles from './Rack.module.scss';
18
- import RackTile from './RackTile';
19
41
 
20
42
  interface Props {
21
43
  className?: string;
@@ -27,13 +49,24 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
27
49
  const config = useTypedSelector(selectConfig);
28
50
  const locale = useTypedSelector(selectLocale);
29
51
  const rack = useTypedSelector(selectRack);
52
+ const inputMode = useTypedSelector(selectInputMode);
53
+ const { rackHeight } = useAppLayout();
30
54
  const resultCandidateTiles = useTypedSelector(selectResultCandidateTiles);
31
55
  const tiles = useMemo(() => zipCharactersAndTiles(rack, resultCandidateTiles), [rack, resultCandidateTiles]);
32
56
  const tilesCount = tiles.length;
33
57
  const tilesRefs = useMemo(() => createArray(tilesCount).map(() => createRef<HTMLInputElement>()), [tilesCount]);
34
58
  const activeIndexRef = useRef<number>();
59
+ const [hasFocus, setHasFocus] = useState(false);
60
+ const [input, setInput] = useState('');
35
61
  const { direction } = LOCALE_FEATURES[locale];
36
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
+ });
37
70
 
38
71
  const changeActiveIndex = useCallback(
39
72
  (offset: number) => {
@@ -75,6 +108,16 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
75
108
  [changeActiveIndex, config, dispatch],
76
109
  );
77
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
+
78
121
  const handleKeyDown = useMemo(() => {
79
122
  return createKeyboardNavigation({
80
123
  onArrowLeft: (event) => {
@@ -114,26 +157,58 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
114
157
  });
115
158
  }, [changeActiveIndex, config, direction]);
116
159
 
160
+ const floatingInputPrompt = useFloating({
161
+ placement: 'bottom-start',
162
+ whileElementsMounted: autoUpdate,
163
+ });
164
+
117
165
  return (
118
- <div className={classNames(styles.rack, className)} style={{ fontSize: tileFontSize }} onPaste={handlePaste}>
119
- {tiles.map(({ character, tile }, index) => (
120
- <RackTile
121
- activeIndexRef={activeIndexRef}
122
- character={character}
123
- className={classNames({
124
- [styles.sharpLeft]: index !== 0,
125
- [styles.sharpRight]: index !== tiles.length - 1,
126
- })}
127
- index={index}
128
- inputRef={tilesRefs[index]}
129
- key={index}
130
- size={tileSize}
131
- tile={tile}
132
- onChange={handleChange}
133
- onKeyDown={handleKeyDown}
134
- />
135
- ))}
136
- </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
+ </>
137
212
  );
138
213
  };
139
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
+ }