@scrabble-solver/scrabble-solver 2.10.6 → 2.10.8

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 (137) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +7 -13
  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/176.js +4597 -568
  16. package/.next/server/chunks/290.js +3 -1
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/pages/404.html +2 -2
  19. package/.next/server/pages/404.js.nft.json +1 -1
  20. package/.next/server/pages/500.html +2 -2
  21. package/.next/server/pages/_app.js +296 -13
  22. package/.next/server/pages/_app.js.nft.json +1 -1
  23. package/.next/server/pages/_document.js.nft.json +1 -1
  24. package/.next/server/pages/api/dictionary/[locale]/[word].js +18 -16
  25. package/.next/server/pages/api/solve.js +17 -0
  26. package/.next/server/pages/index.html +1 -9
  27. package/.next/server/pages/index.js +26 -12
  28. package/.next/server/pages/index.js.nft.json +1 -1
  29. package/.next/server/pages/index.json +1 -1
  30. package/.next/server/pages-manifest.json +2 -2
  31. package/.next/static/Cs23uxWG6AxS72F2yrjHu/_buildManifest.js +1 -0
  32. package/.next/static/chunks/pages/{404-ff35a4cf7f1ec85a.js → 404-8cab6d62fe4ead73.js} +1 -1
  33. package/.next/static/chunks/pages/_app-dcbbb823dc93a031.js +28 -0
  34. package/.next/static/chunks/pages/index-df1ff01aa82d2d4d.js +1 -0
  35. package/.next/static/css/bf2e969b88c4e3dd.css +2 -0
  36. package/.next/static/css/d1cc6b79b211b7b8.css +1 -0
  37. package/.next/trace +55 -55
  38. package/package.json +10 -9
  39. package/src/components/Badge/Badge.module.scss +1 -1
  40. package/src/components/Board/components/Cell/Cell.module.scss +32 -64
  41. package/src/components/Board/components/Cell/Cell.tsx +6 -0
  42. package/src/components/Board/components/Cell/CellPure.tsx +24 -22
  43. package/src/components/Button/Button.module.scss +2 -2
  44. package/src/components/Button/Button.tsx +1 -0
  45. package/src/components/Button/Link.tsx +1 -0
  46. package/src/components/Checkbox/Checkbox.tsx +1 -4
  47. package/src/components/Dictionary/Dictionary.tsx +28 -30
  48. package/src/components/DictionaryInput/DictionaryInput.tsx +3 -0
  49. package/src/components/{SquareButton/SquareButton.module.scss → IconButton/IconButton.module.scss} +1 -1
  50. package/src/components/{SquareButton/SquareButton.tsx → IconButton/IconButton.tsx} +7 -6
  51. package/src/components/{SquareButton → IconButton}/Link.tsx +3 -2
  52. package/src/components/IconButton/index.ts +1 -0
  53. package/src/components/Key/Key.module.scss +1 -1
  54. package/src/components/Logo/Logo.svg +44 -15
  55. package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +1 -1
  56. package/src/components/Modal/Modal.module.scss +4 -2
  57. package/src/components/Modal/Modal.tsx +3 -2
  58. package/src/components/NavButtons/NavButtons.tsx +37 -9
  59. package/src/components/Rack/Rack.module.scss +4 -0
  60. package/src/components/Rack/RackTile.tsx +5 -0
  61. package/src/components/Radio/Radio.tsx +1 -4
  62. package/src/components/Results/HeaderButton.tsx +1 -0
  63. package/src/components/Results/Result.tsx +1 -0
  64. package/src/components/Results/Results.module.scss +2 -3
  65. package/src/components/Results/SolveButton.tsx +1 -0
  66. package/src/components/SeoMessage/SeoMessage.tsx +19 -0
  67. package/src/components/SeoMessage/index.ts +1 -0
  68. package/src/components/Solver/Solver.module.scss +4 -0
  69. package/src/components/Solver/Solver.tsx +26 -8
  70. package/src/components/Solver/components/EmptyState/EmptyState.module.scss +1 -3
  71. package/src/components/Solver/components/InsertButton/InsertButton.module.scss +15 -0
  72. package/src/components/Solver/components/{ApplyButton/ApplyButton.tsx → InsertButton/InsertButton.tsx} +10 -6
  73. package/src/components/Solver/components/InsertButton/index.ts +1 -0
  74. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +37 -7
  75. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +41 -17
  76. package/src/components/Solver/components/SolveButton/SolveButton.tsx +11 -1
  77. package/src/components/Solver/components/index.ts +1 -2
  78. package/src/components/Tile/Tile.module.scss +49 -11
  79. package/src/components/Tile/Tile.tsx +26 -8
  80. package/src/components/Tile/TilePure.tsx +29 -19
  81. package/src/components/Tooltip/Tooltip.module.scss +7 -7
  82. package/src/components/Tooltip/useTooltip.tsx +14 -2
  83. package/src/components/index.ts +2 -1
  84. package/src/i18n/de.json +6 -0
  85. package/src/i18n/en.json +6 -0
  86. package/src/i18n/es.json +6 -0
  87. package/src/i18n/fa.json +6 -0
  88. package/src/i18n/fr.json +6 -0
  89. package/src/i18n/pl.json +6 -0
  90. package/src/icons/ArrowDown.svg +1 -1
  91. package/src/icons/ArrowLeft.svg +1 -1
  92. package/src/icons/ArrowRight.svg +1 -1
  93. package/src/icons/ArrowUp.svg +1 -1
  94. package/src/icons/ChevronDown.svg +1 -1
  95. package/src/icons/ChevronLeft.svg +1 -1
  96. package/src/icons/ChevronRight.svg +1 -1
  97. package/src/icons/Flag.svg +2 -2
  98. package/src/icons/FlagFill.svg +4 -0
  99. package/src/icons/List.svg +1 -1
  100. package/src/icons/Square.svg +4 -0
  101. package/src/icons/SquareFill.svg +4 -0
  102. package/src/icons/index.ts +3 -0
  103. package/src/modals/MenuModal/MenuModal.tsx +9 -3
  104. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -1
  105. package/src/modals/RemainingTilesModal/components/Character/Character.tsx +1 -0
  106. package/src/modals/ResultsModal/ResultsModal.module.scss +1 -1
  107. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +0 -1
  108. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +0 -1
  109. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +14 -24
  110. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +0 -1
  111. package/src/pages/_app.tsx +9 -5
  112. package/src/pages/api/dictionary/[locale]/[word].ts +3 -1
  113. package/src/pages/index.module.scss +1 -2
  114. package/src/pages/index.tsx +6 -0
  115. package/src/parameters/index.ts +10 -0
  116. package/src/state/sagas.ts +5 -2
  117. package/src/state/slices/boardSlice.ts +5 -5
  118. package/src/state/useTranslate.ts +5 -2
  119. package/src/styles/mixins.scss +4 -2
  120. package/src/styles/variables.scss +39 -32
  121. package/src/types/index.ts +7 -1
  122. package/.next/server/chunks/579.js +0 -3917
  123. package/.next/static/chunks/490-d29992f1c264d70e.js +0 -5
  124. package/.next/static/chunks/791-93aa8b8c22e488ac.js +0 -1
  125. package/.next/static/chunks/pages/_app-fa0661b072fc6af9.js +0 -24
  126. package/.next/static/chunks/pages/index-ded620fd5df96be0.js +0 -1
  127. package/.next/static/css/4482c4a0064d3807.css +0 -1
  128. package/.next/static/css/78e42ad01f580f64.css +0 -1
  129. package/.next/static/css/a943dd97164732d4.css +0 -1
  130. package/.next/static/iL0av55MV28b0MXfhKKt2/_buildManifest.js +0 -1
  131. package/src/components/Board/components/Cell/Button.tsx +0 -31
  132. package/src/components/Solver/components/ApplyButton/ApplyButton.module.scss +0 -5
  133. package/src/components/Solver/components/ApplyButton/index.ts +0 -1
  134. package/src/components/Solver/components/MobileControls/MobileControls.tsx +0 -62
  135. package/src/components/Solver/components/MobileControls/index.ts +0 -1
  136. package/src/components/SquareButton/index.ts +0 -1
  137. /package/.next/static/{iL0av55MV28b0MXfhKKt2 → Cs23uxWG6AxS72F2yrjHu}/_ssgManifest.js +0 -0
@@ -2,24 +2,29 @@
2
2
 
3
3
  .resultCandidatePicker {
4
4
  display: flex;
5
- align-items: center;
6
5
  gap: var(--spacing--l);
6
+
7
+ @include media('<xs') {
8
+ gap: var(--spacing--m);
9
+ }
7
10
  }
8
11
 
9
12
  .buttons {
10
13
  flex: 0 0 auto;
11
14
  display: flex;
12
15
  border-radius: var(--border--radius);
13
- box-shadow: var(--box-shadow);
14
16
 
15
- @include media('<xs') {
16
- display: none;
17
+ &.bothEnabled {
18
+ box-shadow: var(--box-shadow);
19
+
20
+ .button {
21
+ box-shadow: none;
22
+ }
17
23
  }
18
24
  }
19
25
 
20
26
  .button {
21
27
  padding: var(--spacing--m);
22
- box-shadow: none;
23
28
 
24
29
  &:focus-within {
25
30
  z-index: 1;
@@ -52,7 +57,7 @@
52
57
  width: 100%;
53
58
  border: var(--border);
54
59
  border-radius: var(--border--radius);
55
- background-color: white;
60
+ background-color: var(--color--white);
56
61
  box-shadow: var(--box-shadow);
57
62
  cursor: pointer;
58
63
 
@@ -79,10 +84,20 @@
79
84
 
80
85
  [dir='ltr'] & {
81
86
  border-right: var(--border);
87
+
88
+ @include media('<xs') {
89
+ border-right: none;
90
+ padding-right: var(--spacing--xs);
91
+ }
82
92
  }
83
93
 
84
94
  [dir='rtl'] & {
85
95
  border-left: var(--border);
96
+
97
+ @include media('<xs') {
98
+ border-left: none;
99
+ padding-left: var(--spacing--xs);
100
+ }
86
101
  }
87
102
  }
88
103
 
@@ -92,6 +107,21 @@
92
107
  flex: 1;
93
108
  text-transform: uppercase;
94
109
  text-align: center;
110
+ white-space: pre-wrap;
111
+
112
+ [dir='ltr'] & {
113
+ @include media('<xs') {
114
+ padding: var(--spacing--m);
115
+ text-align: left;
116
+ }
117
+ }
118
+
119
+ [dir='rtl'] & {
120
+ @include media('<xs') {
121
+ padding: var(--spacing--m);
122
+ text-align: right;
123
+ }
124
+ }
95
125
  }
96
126
 
97
127
  .iconContainer {
@@ -115,6 +145,6 @@
115
145
  color: var(--color--inactive);
116
146
  }
117
147
 
118
- .apply {
148
+ .insert {
119
149
  flex: 0 0 auto;
120
150
  }
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { FunctionComponent, HTMLProps } from 'react';
2
+ import { FunctionComponent, HTMLProps, MouseEventHandler } from 'react';
3
3
  import { useDispatch } from 'react-redux';
4
4
 
5
5
  import { ChevronDown, ChevronLeft, ChevronRight } from 'icons';
@@ -9,29 +9,32 @@ import {
9
9
  selectLocale,
10
10
  selectResultCandidate,
11
11
  selectSortedResults,
12
+ useTranslate,
12
13
  useTypedSelector,
13
14
  } from 'state';
14
15
 
15
16
  import Button from '../../../Button';
16
- import ApplyButton from '../ApplyButton';
17
+ import InsertButton from '../InsertButton';
17
18
 
18
19
  import styles from './ResultCandidatePicker.module.scss';
19
20
 
20
- const ResultCandidatePicker: FunctionComponent<HTMLProps<HTMLDivElement>> = ({ className, ...props }) => {
21
+ interface Props extends HTMLProps<HTMLDivElement> {
22
+ onResultClick: MouseEventHandler<HTMLButtonElement>;
23
+ }
24
+
25
+ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultClick, ...props }) => {
21
26
  const dispatch = useDispatch();
27
+ const translate = useTranslate();
22
28
  const locale = useTypedSelector(selectLocale);
23
29
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
24
30
  const sortedResults = useTypedSelector(selectSortedResults);
25
31
  const results = sortedResults || [];
26
32
  const resultCandidate = useTypedSelector(selectResultCandidate);
27
-
28
- if (!resultCandidate) {
29
- return null;
30
- }
31
-
32
- const index = results.findIndex((result) => result.id === resultCandidate.id);
33
- const isPreviousDisabled = index <= 0;
34
- const isNextDisabled = index >= results.length - 1;
33
+ const index = resultCandidate ? results.findIndex((result) => result.id === resultCandidate.id) : -1;
34
+ const disabled = isOutdated || !resultCandidate;
35
+ const isPreviousDisabled = index <= 0 || disabled;
36
+ const isNextDisabled = index >= results.length - 1 || disabled;
37
+ const bothEnabled = !isPreviousDisabled && !isNextDisabled;
35
38
 
36
39
  const handleNextClick = () => {
37
40
  if (!isNextDisabled) {
@@ -49,25 +52,46 @@ const ResultCandidatePicker: FunctionComponent<HTMLProps<HTMLDivElement>> = ({ c
49
52
 
50
53
  return (
51
54
  <div className={classNames(styles.resultCandidatePicker, className)} {...props}>
52
- <div className={styles.buttons}>
55
+ <div className={classNames(styles.buttons, { [styles.bothEnabled]: bothEnabled })}>
53
56
  <Button
57
+ aria-label={translate('common.previous')}
54
58
  className={styles.button}
55
59
  disabled={isPreviousDisabled}
56
60
  Icon={ChevronLeft}
57
61
  onClick={handlePreviousClick}
58
62
  />
59
- <Button className={styles.button} disabled={isNextDisabled} Icon={ChevronRight} onClick={handleNextClick} />
63
+
64
+ <Button
65
+ aria-label={translate('common.next')}
66
+ className={styles.button}
67
+ disabled={isNextDisabled}
68
+ Icon={ChevronRight}
69
+ onClick={handleNextClick}
70
+ />
60
71
  </div>
61
72
 
62
- <button className={styles.resultCandidate} disabled={isOutdated} type="button">
63
- <div className={styles.points}>{resultCandidate.points.toLocaleString(locale)}</div>
64
- <div className={styles.word}>{resultCandidate.word}</div>
73
+ <button
74
+ aria-label={translate('results')}
75
+ className={styles.resultCandidate}
76
+ disabled={disabled}
77
+ type="button"
78
+ onClick={onResultClick}
79
+ >
80
+ {resultCandidate && (
81
+ <>
82
+ <div className={styles.points}>{resultCandidate.points.toLocaleString(locale)}</div>
83
+ <div className={styles.word}>{resultCandidate.word}</div>
84
+ </>
85
+ )}
86
+
87
+ {!resultCandidate && <div className={styles.word}> </div>}
88
+
65
89
  <div className={styles.iconContainer}>
66
90
  <ChevronDown className={styles.icon} />
67
91
  </div>
68
92
  </button>
69
93
 
70
- <ApplyButton className={styles.apply} />
94
+ <InsertButton className={styles.insert} />
71
95
  </div>
72
96
  );
73
97
  };
@@ -4,7 +4,14 @@ import { useDispatch } from 'react-redux';
4
4
 
5
5
  import { Search } from 'icons';
6
6
  import { noop } from 'lib';
7
- import { selectAreResultsOutdated, selectIsLoading, selectRack, solveSlice, useTypedSelector } from 'state';
7
+ import {
8
+ selectAreResultsOutdated,
9
+ selectIsLoading,
10
+ selectRack,
11
+ solveSlice,
12
+ useTranslate,
13
+ useTypedSelector,
14
+ } from 'state';
8
15
 
9
16
  import Button from '../../../Button';
10
17
 
@@ -17,6 +24,7 @@ interface Props {
17
24
 
18
25
  const SolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) => {
19
26
  const dispatch = useDispatch();
27
+ const translate = useTranslate();
20
28
  const isLoading = useTypedSelector(selectIsLoading);
21
29
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
22
30
  const rack = useTypedSelector(selectRack);
@@ -29,10 +37,12 @@ const SolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) =>
29
37
 
30
38
  return (
31
39
  <Button
40
+ aria-label={translate('results.solve')}
32
41
  className={classNames(styles.solveButton, className)}
33
42
  disabled={isLoading || !isOutdated || !hasTiles}
34
43
  Icon={Search}
35
44
  iconClassName={styles.icon}
45
+ tooltip={translate('results.solve')}
36
46
  type="submit"
37
47
  variant="primary"
38
48
  onClick={handleClick}
@@ -1,5 +1,4 @@
1
- export { default as ApplyButton } from './ApplyButton';
2
1
  export { default as EmptyState } from './EmptyState';
3
- export { default as MobileControls } from './MobileControls';
2
+ export { default as InsertButton } from './InsertButton';
4
3
  export { default as ResultCandidatePicker } from './ResultCandidatePicker';
5
4
  export { default as SolveButton } from './SolveButton';
@@ -24,18 +24,20 @@
24
24
  }
25
25
 
26
26
  &.blank {
27
- --background-color: white;
27
+ --background-color: var(--color--white);
28
28
 
29
+ .placeholder,
29
30
  .character {
30
- color: black;
31
+ color: var(--color--foreground);
31
32
  }
32
33
  }
33
34
 
34
35
  &.highlighted {
35
36
  --background-color: var(--color--primary);
36
37
 
37
- color: white;
38
+ color: var(--color--primary--opposite);
38
39
 
40
+ .placeholder,
39
41
  .character {
40
42
  color: inherit;
41
43
  }
@@ -45,12 +47,6 @@
45
47
  }
46
48
  }
47
49
 
48
- &.invalid {
49
- --background-color: var(--color--red--light);
50
-
51
- color: var(--color--error);
52
- }
53
-
54
50
  &:not(.disabled) {
55
51
  .input::selection {
56
52
  --background--color: transparent;
@@ -58,8 +54,19 @@
58
54
  }
59
55
  }
60
56
 
57
+ .inputContainer {
58
+ position: absolute;
59
+ top: 0;
60
+ right: 0;
61
+ bottom: 0;
62
+ left: 0;
63
+ display: flex;
64
+ overflow: hidden;
65
+ }
66
+
61
67
  .input,
62
- .character {
68
+ .character,
69
+ .placeholder {
63
70
  padding: 0;
64
71
  font-weight: bold;
65
72
  text-transform: uppercase;
@@ -75,9 +82,26 @@
75
82
  color: transparent;
76
83
  border: none;
77
84
  font-size: 16px; // prevent iOS from automatically zooming in on focus
85
+
86
+ // Hack for this Lighthouse warning:
87
+ // > Interactive elements like buttons and links should be large enough (48x48px), and have
88
+ // > enough space around them, to be easy enough to tap without overlapping onto other elements.
89
+ //
90
+ // .inputContainer exists only for the purpose of this hack
91
+ $min-tile-width: 20px;
92
+ $min-lighthouse-size: 48px;
93
+
94
+ position: absolute;
95
+ top: calc((100% - $min-lighthouse-size) / 2);
96
+ right: calc((100% - $min-lighthouse-size) / 2);
97
+ bottom: calc((100% - $min-lighthouse-size) / 2);
98
+ left: calc((100% - $min-lighthouse-size) / 2);
99
+ width: $min-lighthouse-size;
100
+ height: $min-lighthouse-size;
78
101
  }
79
102
 
80
- .character {
103
+ .character,
104
+ .placeholder {
81
105
  position: absolute;
82
106
  top: 0;
83
107
  right: 0;
@@ -97,6 +121,18 @@
97
121
 
98
122
  .raised & {
99
123
  box-shadow: inset -2px -2px 2px -1px rgba(34, 34, 34, 0.8);
124
+
125
+ @include media('<xs') {
126
+ box-shadow: inset -2px -2px 1px -1px rgba(34, 34, 34, 0.8);
127
+ }
128
+ }
129
+ }
130
+
131
+ .character {
132
+ opacity: 1;
133
+
134
+ .empty & {
135
+ opacity: 0;
100
136
  }
101
137
  }
102
138
 
@@ -130,6 +166,8 @@
130
166
  position: absolute;
131
167
  width: $size;
132
168
  height: $size;
169
+ background-color: var(--color--error--opposite);
170
+ color: var(--color--error);
133
171
 
134
172
  [dir='ltr'] & {
135
173
  top: 0;
@@ -1,21 +1,24 @@
1
1
  import { EMPTY_CELL } from '@scrabble-solver/constants';
2
+ import mergeRefs from 'merge-refs';
2
3
  import {
3
4
  ChangeEventHandler,
4
- createRef,
5
5
  FocusEventHandler,
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
8
  RefObject,
9
9
  useEffect,
10
10
  useMemo,
11
+ useRef,
11
12
  } from 'react';
12
13
 
13
14
  import { getTileSizes, noop } from 'lib';
15
+ import { EASE_OUT_CUBIC, TILE_APPEAR_DURATION, TILE_APPEAR_KEYFRAMES } from 'parameters';
14
16
  import { selectLocale, useTypedSelector } from 'state';
15
17
 
16
18
  import TilePure from './TilePure';
17
19
 
18
20
  interface Props {
21
+ 'aria-label': string;
19
22
  autoFocus?: boolean;
20
23
  character?: string;
21
24
  className?: string;
@@ -35,12 +38,13 @@ interface Props {
35
38
  }
36
39
 
37
40
  const Tile: FunctionComponent<Props> = ({
41
+ 'aria-label': ariaLabel,
38
42
  autoFocus,
39
43
  className,
40
44
  character = '',
41
45
  disabled,
42
46
  highlighted,
43
- inputRef: ref,
47
+ inputRef,
44
48
  isBlank,
45
49
  isValid,
46
50
  placeholder,
@@ -57,24 +61,38 @@ const Tile: FunctionComponent<Props> = ({
57
61
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
58
62
  const characterStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
59
63
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
60
- const inputRef = useMemo<RefObject<HTMLInputElement>>(() => ref || createRef(), [ref]);
64
+ const ref = useRef<HTMLInputElement>(null);
65
+ const mergedRef = inputRef ? mergeRefs(ref, inputRef) : ref;
61
66
  const isEmpty = !character || character === EMPTY_CELL;
62
67
  const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined';
63
68
  const pointsFormatted = typeof points === 'number' ? points.toLocaleString(locale) : '';
64
69
 
65
70
  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
66
- inputRef.current?.select();
71
+ ref.current?.select();
67
72
  onKeyDown(event);
68
73
  };
69
74
 
70
75
  useEffect(() => {
71
- if (autoFocus && inputRef.current) {
72
- inputRef.current.focus();
76
+ if (autoFocus && ref.current) {
77
+ ref.current.focus();
73
78
  }
74
- }, [autoFocus, inputRef]);
79
+ }, [autoFocus, ref]);
80
+
81
+ useEffect(() => {
82
+ if (!ref.current?.parentElement || !character) {
83
+ return;
84
+ }
85
+
86
+ ref.current.parentElement.animate(TILE_APPEAR_KEYFRAMES, {
87
+ duration: TILE_APPEAR_DURATION,
88
+ easing: EASE_OUT_CUBIC,
89
+ fill: 'forwards',
90
+ });
91
+ }, [character]);
75
92
 
76
93
  return (
77
94
  <TilePure
95
+ aria-label={ariaLabel}
78
96
  autoFocus={autoFocus}
79
97
  canShowPoints={canShowPoints}
80
98
  character={character}
@@ -82,7 +100,7 @@ const Tile: FunctionComponent<Props> = ({
82
100
  className={className}
83
101
  disabled={disabled}
84
102
  highlighted={highlighted}
85
- inputRef={inputRef}
103
+ inputRef={mergedRef}
86
104
  isBlank={isBlank}
87
105
  isValid={isValid}
88
106
  placeholder={placeholder}
@@ -6,7 +6,7 @@ import {
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
8
  memo,
9
- RefObject,
9
+ Ref,
10
10
  } from 'react';
11
11
 
12
12
  import { ExclamationSquareFill } from 'icons';
@@ -14,6 +14,7 @@ import { ExclamationSquareFill } from 'icons';
14
14
  import styles from './Tile.module.scss';
15
15
 
16
16
  interface Props {
17
+ 'aria-label': string;
17
18
  autoFocus?: boolean;
18
19
  canShowPoints?: boolean;
19
20
  character?: string;
@@ -21,7 +22,7 @@ interface Props {
21
22
  className?: string;
22
23
  disabled?: boolean;
23
24
  highlighted?: boolean;
24
- inputRef: RefObject<HTMLInputElement>;
25
+ inputRef: Ref<HTMLInputElement>;
25
26
  isBlank?: boolean;
26
27
  isValid?: boolean;
27
28
  placeholder?: string;
@@ -37,6 +38,7 @@ interface Props {
37
38
  }
38
39
 
39
40
  const TilePure: FunctionComponent<Props> = ({
41
+ 'aria-label': ariaLabel,
40
42
  autoFocus,
41
43
  canShowPoints,
42
44
  character,
@@ -64,7 +66,6 @@ const TilePure: FunctionComponent<Props> = ({
64
66
  [styles.disabled]: disabled,
65
67
  [styles.empty]: !character,
66
68
  [styles.highlighted]: highlighted,
67
- [styles.invalid]: !isValid,
68
69
  [styles.raised]: raised,
69
70
  [styles.points1]: points === 1,
70
71
  [styles.points2]: points === 2,
@@ -74,24 +75,33 @@ const TilePure: FunctionComponent<Props> = ({
74
75
  })}
75
76
  style={style}
76
77
  >
77
- <input
78
- autoCapitalize="none"
79
- autoComplete="off"
80
- autoCorrect="off"
81
- autoFocus={autoFocus}
82
- className={styles.input}
83
- disabled={disabled}
84
- ref={inputRef}
85
- spellCheck={false}
86
- tabIndex={tabIndex}
87
- value={character || ''}
88
- onChange={onChange}
89
- onFocus={onFocus}
90
- onKeyDown={onKeyDown}
91
- />
78
+ <div className={styles.inputContainer}>
79
+ <input
80
+ aria-label={ariaLabel}
81
+ autoCapitalize="none"
82
+ autoComplete="off"
83
+ autoCorrect="off"
84
+ autoFocus={autoFocus}
85
+ className={styles.input}
86
+ disabled={disabled}
87
+ ref={inputRef}
88
+ spellCheck={false}
89
+ tabIndex={tabIndex}
90
+ value={character || ''}
91
+ onChange={onChange}
92
+ onFocus={onFocus}
93
+ onKeyDown={onKeyDown}
94
+ />
95
+ </div>
96
+
97
+ {placeholder && (
98
+ <div className={styles.placeholder} style={characterStyle} tabIndex={-1}>
99
+ {placeholder}
100
+ </div>
101
+ )}
92
102
 
93
103
  <div className={styles.character} style={characterStyle} tabIndex={-1}>
94
- {character || placeholder}
104
+ {character}
95
105
  </div>
96
106
 
97
107
  {canShowPoints && (
@@ -10,9 +10,9 @@ $arrow-size: 4px;
10
10
  padding: var(--spacing--s) var(--spacing--m);
11
11
  box-shadow: var(--box-shadow);
12
12
  border-radius: var(--border--radius);
13
- background-color: var(--tooltip--background);
14
- color: var(--tooltip--foreground);
15
- z-index: 200;
13
+ background-color: var(--color--tooltip--background);
14
+ color: var(--color--tooltip--foreground);
15
+ z-index: var(--z-index--tooltip);
16
16
 
17
17
  &.top {
18
18
  .arrow {
@@ -21,7 +21,7 @@ $arrow-size: 4px;
21
21
  &::after {
22
22
  left: 0;
23
23
  bottom: 0;
24
- border-top-color: var(--tooltip--background);
24
+ border-top-color: var(--color--tooltip--background);
25
25
  border-bottom: none;
26
26
  }
27
27
  }
@@ -34,7 +34,7 @@ $arrow-size: 4px;
34
34
  &::after {
35
35
  left: 0;
36
36
  top: 0;
37
- border-right-color: var(--tooltip--background);
37
+ border-right-color: var(--color--tooltip--background);
38
38
  border-left: none;
39
39
  }
40
40
  }
@@ -47,7 +47,7 @@ $arrow-size: 4px;
47
47
  &::after {
48
48
  top: 0;
49
49
  left: 0;
50
- border-bottom-color: var(--tooltip--background);
50
+ border-bottom-color: var(--color--tooltip--background);
51
51
  border-top: none;
52
52
  }
53
53
  }
@@ -60,7 +60,7 @@ $arrow-size: 4px;
60
60
  &::after {
61
61
  right: 0;
62
62
  top: 0;
63
- border-left-color: var(--tooltip--background);
63
+ border-left-color: var(--color--tooltip--background);
64
64
  border-right: none;
65
65
  }
66
66
  }
@@ -12,7 +12,7 @@ import {
12
12
  import { usePopper } from 'react-popper';
13
13
  import { useMountedState, useRafLoop } from 'react-use';
14
14
 
15
- import { usePortal, useUniqueId } from 'hooks';
15
+ import { useIsTouchDevice, usePortal, useUniqueId } from 'hooks';
16
16
  import { noop } from 'lib';
17
17
 
18
18
  import { MODIFIERS } from './constants';
@@ -36,11 +36,13 @@ interface TriggerProps {
36
36
  onMouseOver?: MouseEventHandler;
37
37
  }
38
38
 
39
+ // eslint-disable-next-line max-statements
39
40
  const useTooltip = (
40
41
  tooltip: ReactNode,
41
42
  { className, placement = 'top', onBlur = noop, onFocus = noop, onMouseOut = noop, onMouseOver = noop }: Props = {},
42
43
  ): TriggerProps => {
43
44
  const id = useUniqueId();
45
+ const isTouchDevice = useIsTouchDevice();
44
46
  const isEnabled = Boolean(tooltip) || tooltip === 0;
45
47
  const isMounted = useMountedState();
46
48
  const [isShown, setIsShown] = useState<boolean>(false);
@@ -90,7 +92,7 @@ const useTooltip = (
90
92
  [onMouseOver],
91
93
  );
92
94
 
93
- const triggerProps = useMemo(
95
+ const mouseTriggerProps = useMemo(
94
96
  () => ({
95
97
  ...ariaAttributes,
96
98
  ref: setReferenceElement,
@@ -102,6 +104,16 @@ const useTooltip = (
102
104
  [ariaAttributes, handleBlur, handleFocus, handleMouseOut, handleMouseOver],
103
105
  );
104
106
 
107
+ const touchTriggerProps = useMemo(
108
+ () => ({
109
+ ...ariaAttributes,
110
+ ref: setReferenceElement,
111
+ }),
112
+ [ariaAttributes],
113
+ );
114
+
115
+ const triggerProps = isTouchDevice ? touchTriggerProps : mouseTriggerProps;
116
+
105
117
  useRafLoop(() => {
106
118
  if (isMounted() && update) {
107
119
  update();
@@ -5,6 +5,7 @@ export { default as Checkbox } from './Checkbox';
5
5
  export { default as Dictionary } from './Dictionary';
6
6
  export { default as DictionaryInput } from './DictionaryInput';
7
7
  export { default as EmptyState } from './EmptyState';
8
+ export { default as IconButton } from './IconButton';
8
9
  export { default as Key } from './Key';
9
10
  export { default as Loading } from './Loading';
10
11
  export { default as Logo } from './Logo';
@@ -18,10 +19,10 @@ export { default as Rack } from './Rack';
18
19
  export { default as Radio } from './Radio';
19
20
  export { default as Results } from './Results';
20
21
  export { default as ResultsInput } from './ResultsInput';
22
+ export { default as SeoMessage } from './SeoMessage';
21
23
  export { default as Sizer } from './Sizer';
22
24
  export { default as Solver } from './Solver';
23
25
  export { default as SplashScreen } from './SplashScreen';
24
- export { default as SquareButton } from './SquareButton';
25
26
  export { default as SvgFontCss } from './SvgFontCss';
26
27
  export { default as SvgFontFix } from './SvgFontFix';
27
28
  export { default as Tile } from './Tile';