@scrabble-solver/scrabble-solver 2.13.12 → 2.14.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 (99) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +14 -14
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack.old +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/edge-server-production/index.pack.old +0 -0
  11. package/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  14. package/.next/prerender-manifest.js +1 -1
  15. package/.next/prerender-manifest.json +1 -1
  16. package/.next/routes-manifest.json +1 -1
  17. package/.next/server/chunks/807.js +1 -1
  18. package/.next/server/middleware-build-manifest.js +1 -1
  19. package/.next/server/pages/404.html +1 -1
  20. package/.next/server/pages/500.html +1 -1
  21. package/.next/server/pages/api/solve.js +1 -1
  22. package/.next/server/pages/index.html +1 -1
  23. package/.next/server/pages/index.js +1 -1
  24. package/.next/server/pages/index.json +1 -1
  25. package/.next/server/pages-manifest.json +1 -1
  26. package/.next/static/{N8hSsS6Ppzlj3ebHMSZvI → TxfjxrH5h31sPQxPhrJzG}/_buildManifest.js +1 -1
  27. package/.next/static/chunks/{main-b5b360c6afb66b05.js → main-8b0b4e610892a916.js} +1 -1
  28. package/.next/static/chunks/pages/{404-0c9f3e0f8b15f487.js → 404-b0c2ccded2455be0.js} +1 -1
  29. package/.next/static/chunks/pages/_app-2912876c7b6e698e.js +17 -0
  30. package/.next/static/chunks/pages/index-86d9ad372c48c8b7.js +1 -0
  31. package/.next/static/chunks/webpack-c4acd79e87956a0e.js +1 -0
  32. package/.next/static/css/14625ff7b4d265d0.css +2 -0
  33. package/.next/trace +45 -45
  34. package/package.json +14 -14
  35. package/src/components/Alert/Alert.tsx +1 -1
  36. package/src/components/Board/Board.module.scss +3 -9
  37. package/src/components/Board/Board.tsx +2 -2
  38. package/src/components/Board/BoardPure.tsx +4 -3
  39. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +2 -0
  40. package/src/components/Board/hooks/useBackgroundImage.tsx +2 -6
  41. package/src/components/Board/hooks/useBoardStyle.ts +10 -4
  42. package/src/components/Button/Button.tsx +1 -1
  43. package/src/components/Dictionary/Dictionary.module.scss +10 -7
  44. package/src/components/Dictionary/Dictionary.tsx +38 -36
  45. package/src/components/IconButton/IconButton.tsx +1 -1
  46. package/src/components/IconButton/Link.tsx +1 -1
  47. package/src/components/Keys/Arrows/Arrows.tsx +4 -4
  48. package/src/components/Loading/Loading.module.scss +1 -0
  49. package/src/components/Loading/Loading.tsx +1 -1
  50. package/src/components/Modal/components/Section/Section.tsx +3 -2
  51. package/src/components/NavButtons/NavButtons.tsx +1 -0
  52. package/src/components/NotFound/NotFound.tsx +1 -1
  53. package/src/components/Rack/Rack.tsx +1 -0
  54. package/src/components/Radio/Radio.tsx +1 -1
  55. package/src/components/Results/Cell.tsx +7 -3
  56. package/src/components/Results/Header.tsx +99 -0
  57. package/src/components/Results/HeaderButton.tsx +18 -13
  58. package/src/components/Results/Result.tsx +23 -16
  59. package/src/components/Results/Results.module.scss +27 -18
  60. package/src/components/Results/Results.tsx +3 -9
  61. package/src/components/Results/types.ts +0 -8
  62. package/src/components/Solver/Solver.tsx +1 -1
  63. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +1 -1
  64. package/src/components/Tile/TilePure.tsx +2 -1
  65. package/src/hooks/index.ts +1 -0
  66. package/src/hooks/useAppLayout.ts +12 -1
  67. package/src/hooks/useColumns.ts +47 -0
  68. package/src/icons/GeoAlt.svg +5 -0
  69. package/src/icons/OneTwoThree.svg +4 -0
  70. package/src/icons/SquareA.svg +6 -0
  71. package/src/icons/SquareB.svg +6 -0
  72. package/src/icons/Squares.svg +34 -0
  73. package/src/icons/Words.svg +22 -0
  74. package/src/icons/index.ts +6 -0
  75. package/src/lib/groupResults.ts +1 -1
  76. package/src/lib/index.ts +0 -1
  77. package/src/lib/sortResults.ts +10 -10
  78. package/src/modals/KeyMapModal/KeyMapModal.tsx +8 -9
  79. package/src/modals/MenuModal/MenuModal.tsx +1 -1
  80. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +1 -0
  81. package/src/modals/SettingsModal/SettingsModal.tsx +5 -5
  82. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +1 -1
  83. package/src/modals/WordsModal/WordsModal.tsx +4 -2
  84. package/src/parameters/index.ts +12 -0
  85. package/src/state/selectors.ts +26 -1
  86. package/src/state/slices/resultsInitialState.ts +2 -2
  87. package/src/state/slices/resultsSlice.ts +2 -2
  88. package/src/state/useTranslate.ts +5 -1
  89. package/src/styles/variables.scss +1 -0
  90. package/src/types/index.ts +11 -2
  91. package/.next/static/chunks/pages/_app-8246f5b39b6a5e59.js +0 -17
  92. package/.next/static/chunks/pages/index-65bfe83d121535ab.js +0 -1
  93. package/.next/static/chunks/webpack-6ef43a8d4a395f49.js +0 -1
  94. package/.next/static/css/2f727b21d1331ea5.css +0 -2
  95. package/src/components/Results/getCoordinatesColumn.ts +0 -14
  96. package/src/components/Results/getLocaleColumns.ts +0 -58
  97. package/src/components/Results/useColumns.ts +0 -44
  98. package/src/lib/dataUrlToBlob.ts +0 -20
  99. /package/.next/static/{N8hSsS6Ppzlj3ebHMSZvI → TxfjxrH5h31sPQxPhrJzG}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.13.12",
3
+ "version": "2.14.0",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -27,21 +27,21 @@
27
27
  "start": "env-cmd next start -p 3333"
28
28
  },
29
29
  "dependencies": {
30
- "@floating-ui/react": "^0.26.17",
30
+ "@floating-ui/react": "^0.26.20",
31
31
  "@kamilmielnik/trie": "^3.0.0",
32
- "@reduxjs/toolkit": "^2.2.5",
33
- "@scrabble-solver/configs": "^2.13.12",
34
- "@scrabble-solver/constants": "^2.13.12",
35
- "@scrabble-solver/dictionaries": "^2.13.12",
36
- "@scrabble-solver/logger": "^2.13.12",
37
- "@scrabble-solver/solver": "^2.13.12",
38
- "@scrabble-solver/types": "^2.13.12",
39
- "@scrabble-solver/word-definitions": "^2.13.12",
32
+ "@reduxjs/toolkit": "^2.2.7",
33
+ "@scrabble-solver/configs": "^2.14.0",
34
+ "@scrabble-solver/constants": "^2.14.0",
35
+ "@scrabble-solver/dictionaries": "^2.14.0",
36
+ "@scrabble-solver/logger": "^2.14.0",
37
+ "@scrabble-solver/solver": "^2.14.0",
38
+ "@scrabble-solver/types": "^2.14.0",
39
+ "@scrabble-solver/word-definitions": "^2.14.0",
40
40
  "classnames": "^2.5.1",
41
41
  "env-cmd": "^10.1.0",
42
42
  "include-media": "^2.0.0",
43
43
  "include-media-query-builder": "^1.1.0",
44
- "next": "^14.2.4",
44
+ "next": "^14.2.5",
45
45
  "normalize.css": "^8.0.1",
46
46
  "react": "^18.3.1",
47
47
  "react-cool-onclickoutside": "^1.7.0",
@@ -52,7 +52,7 @@
52
52
  "react-window": "^1.8.10",
53
53
  "redux-saga": "^1.3.0",
54
54
  "store2": "^2.14.3",
55
- "use-debounce": "^10.0.1",
55
+ "use-debounce": "^10.0.2",
56
56
  "workbox-expiration": "^7.1.0",
57
57
  "workbox-precaching": "^7.1.0",
58
58
  "workbox-routing": "^7.1.0",
@@ -71,7 +71,7 @@
71
71
  "@types/react-window": "^1.8.8",
72
72
  "@types/redux": "^3.6.31",
73
73
  "@types/redux-saga": "^0.10.5",
74
- "sass": "^1.77.5"
74
+ "sass": "^1.77.8"
75
75
  },
76
- "gitHead": "092d0fb2988a32cfd29384d6d075e106e0042b11"
76
+ "gitHead": "013ecd9af1dcd04b1426358221c8131cdafd7331"
77
77
  }
@@ -32,7 +32,7 @@ const Alert: FunctionComponent<Props> = ({ children, className, variant, ...prop
32
32
  {...props}
33
33
  >
34
34
  <div className={styles.iconContainer}>
35
- <Icon className={styles.icon} />
35
+ <Icon aria-hidden="true" className={styles.icon} role="img" />
36
36
  </div>
37
37
 
38
38
  <div className={styles.content}>{children}</div>
@@ -5,6 +5,7 @@
5
5
  display: grid;
6
6
  gap: var(--border--width);
7
7
  box-shadow: var(--box-shadow);
8
+ background-color: white;
8
9
  border: var(--border);
9
10
  border-radius: var(--border--radius);
10
11
  }
@@ -64,16 +65,9 @@
64
65
  color: var(--color--white);
65
66
  }
66
67
 
67
- .coordinateColumn {
68
+ .coordinate {
69
+ position: relative;
68
70
  display: flex;
69
71
  align-items: center;
70
72
  justify-content: center;
71
- font-size: var(--font--size--h2);
72
- }
73
-
74
- .coordinateRow {
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- font-size: var(--font--size--h2);
79
73
  }
@@ -176,8 +176,8 @@ const Board: FunctionComponent<Props> = ({ className }) => {
176
176
  ref={floatingFocus.refs.setFloating}
177
177
  style={{
178
178
  position: floatingFocus.strategy,
179
- top: (floatingFocus.y ? floatingFocus.y + cellSize : 0) - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
180
- left: (floatingFocus.x ?? 0) - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
179
+ top: floatingFocus.y + cellSize - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
180
+ left: floatingFocus.x - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
181
181
  width: cellSize,
182
182
  height: cellSize,
183
183
  opacity: hasFocus ? 1 : 0,
@@ -61,6 +61,7 @@ const BoardPure = forwardRef<HTMLDivElement, Props>(
61
61
  ) => (
62
62
  <div
63
63
  className={classNames(styles.board, className)}
64
+ data-testid="board"
64
65
  ref={ref}
65
66
  style={style}
66
67
  onBlur={onBlur}
@@ -73,7 +74,7 @@ const BoardPure = forwardRef<HTMLDivElement, Props>(
73
74
 
74
75
  {rows[0].map((_column, index) => (
75
76
  <div
76
- className={styles.coordinateColumn}
77
+ className={styles.coordinate}
77
78
  key={index}
78
79
  style={{
79
80
  width: cellSize,
@@ -107,7 +108,7 @@ const BoardPure = forwardRef<HTMLDivElement, Props>(
107
108
  }}
108
109
  >
109
110
  <div className={styles.iconBackground} />
110
- <Icon className={styles.icon} />
111
+ <Icon aria-hidden="true" className={styles.icon} role="img" />
111
112
  </div>
112
113
  );
113
114
  })}
@@ -116,7 +117,7 @@ const BoardPure = forwardRef<HTMLDivElement, Props>(
116
117
  <Fragment key={y}>
117
118
  {showCoordinates !== 'hidden' && (
118
119
  <div
119
- className={styles.coordinateRow}
120
+ className={styles.coordinate}
120
121
  style={{
121
122
  width: coordinatesSize,
122
123
  height: cellSize,
@@ -23,6 +23,8 @@ const ToggleDirectionButton: FunctionComponent<Props> = ({ className, direction,
23
23
  <Button
24
24
  aria-label={translate('cell.toggle-direction')}
25
25
  className={classNames(styles.button, className)}
26
+ data-direction={direction}
27
+ data-testid="toggle-direction-button"
26
28
  Icon={ArrowDown}
27
29
  iconClassName={classNames(styles.icon, {
28
30
  [styles.right]: direction === 'horizontal',
@@ -8,7 +8,7 @@ import { Provider } from 'react-redux';
8
8
  import { useAppLayout, useMediaQueries } from 'hooks';
9
9
  import { LOCALE_FEATURES } from 'i18n';
10
10
  import { Star } from 'icons';
11
- import { dataUrlToBlob, getTileSizes } from 'lib';
11
+ import { getTileSizes } from 'lib';
12
12
  import { BORDER_COLOR_LIGHT, BORDER_RADIUS, BORDER_WIDTH, COLOR_BACKGROUND, COLOR_BONUS_START } from 'parameters';
13
13
  import { selectConfig, selectLocale, selectShowCoordinates, store, useTypedSelector } from 'state';
14
14
  import { Point } from 'types';
@@ -211,11 +211,7 @@ const useBackgroundImage = () => {
211
211
 
212
212
  const encodedSvg = useMemo(() => globalThis.btoa(backgroundSvg), [backgroundSvg]);
213
213
  const dataUrl = `data:image/svg+xml;base64,${encodedSvg}`;
214
- const blob = useMemo(() => dataUrlToBlob(dataUrl), [dataUrl]);
215
- const blobUrl = useMemo(() => URL.createObjectURL(blob), [blob]);
216
- const url = `url(${blobUrl})`;
217
-
218
- return url;
214
+ return dataUrl;
219
215
  };
220
216
 
221
217
  export default useBackgroundImage;
@@ -2,6 +2,7 @@ import { CSSProperties, useMemo } from 'react';
2
2
 
3
3
  import { useAppLayout } from 'hooks';
4
4
  import { getTileSizes } from 'lib';
5
+ import { BORDER_WIDTH } from 'parameters';
5
6
  import { selectConfig, selectShowCoordinates, useTypedSelector } from 'state';
6
7
 
7
8
  import useBackgroundImage from './useBackgroundImage';
@@ -10,16 +11,21 @@ const useBoardStyle = () => {
10
11
  const config = useTypedSelector(selectConfig);
11
12
  const { cellSize } = useAppLayout();
12
13
  const { tileFontSize } = getTileSizes(cellSize);
13
- const backgroundImage = useBackgroundImage();
14
14
  const showCoordinates = useTypedSelector(selectShowCoordinates);
15
+ const backgroundImage = useBackgroundImage();
16
+ const coordinatesSize = 0.5 * cellSize - BORDER_WIDTH;
15
17
  const boardStyle = useMemo<CSSProperties>(
16
18
  () => ({
17
- backgroundImage,
19
+ backgroundImage: `url(${backgroundImage})`,
18
20
  fontSize: tileFontSize,
19
21
  gridTemplateColumns:
20
- showCoordinates === 'hidden' ? `repeat(${config.boardSize}, 1fr)` : `0.5fr repeat(${config.boardSize}, 1fr)`,
22
+ showCoordinates === 'hidden'
23
+ ? `repeat(${config.boardSize}, 1fr)`
24
+ : `${coordinatesSize}px repeat(${config.boardSize}, 1fr)`,
21
25
  gridTemplateRows:
22
- showCoordinates === 'hidden' ? `repeat(${config.boardSize}, 1fr)` : `0.5fr repeat(${config.boardSize}, 1fr)`,
26
+ showCoordinates === 'hidden'
27
+ ? `repeat(${config.boardSize}, 1fr)`
28
+ : `${coordinatesSize}px repeat(${config.boardSize}, 1fr)`,
23
29
  }),
24
30
  [backgroundImage, config.boardSize, tileFontSize],
25
31
  );
@@ -34,7 +34,7 @@ const Button: FunctionComponent<Props> = ({
34
34
  {...props}
35
35
  >
36
36
  <span className={styles.content}>
37
- {Icon && <Icon className={classNames(styles.icon, iconClassName)} />}
37
+ {Icon && <Icon aria-hidden="true" className={classNames(styles.icon, iconClassName)} role="img" />}
38
38
  {children && <span className={styles.label}>{children}</span>}
39
39
  </span>
40
40
  </button>
@@ -1,14 +1,10 @@
1
1
  @import 'styles/mixins';
2
2
 
3
3
  .dictionary {
4
- @include scrollbars;
5
-
6
4
  position: relative;
7
- max-height: var(--dictionary--height);
8
- height: var(--dictionary--height);
9
- overflow-y: auto;
10
- word-break: break-word;
11
5
  transition: var(--transition);
6
+ word-break: break-word;
7
+ overflow: hidden;
12
8
 
13
9
  &.isAllowed {
14
10
  background-color: var(--color--green--light);
@@ -19,6 +15,13 @@
19
15
  }
20
16
  }
21
17
 
18
+ .content {
19
+ @include scrollbars;
20
+
21
+ height: 100%;
22
+ overflow-y: auto;
23
+ }
24
+
22
25
  .result {
23
26
  transition: var(--transition);
24
27
 
@@ -43,7 +46,7 @@
43
46
  }
44
47
  }
45
48
 
46
- .content {
49
+ .resultContent {
47
50
  padding: var(--spacing--l);
48
51
  }
49
52
 
@@ -25,49 +25,51 @@ const Dictionary: FunctionComponent<Props> = ({ className }) => {
25
25
  [styles.isNotAllowed]: isLastAllowed === false,
26
26
  })}
27
27
  >
28
- {typeof error !== 'undefined' && !isLoading && <EmptyState variant="error">{error.message}</EmptyState>}
28
+ <div className={styles.content}>
29
+ {typeof error !== 'undefined' && !isLoading && <EmptyState variant="error">{error.message}</EmptyState>}
29
30
 
30
- {typeof error === 'undefined' && !isLoading && results.length === 0 && (
31
- <EmptyState variant="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
32
- )}
31
+ {typeof error === 'undefined' && !isLoading && results.length === 0 && (
32
+ <EmptyState variant="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
33
+ )}
33
34
 
34
- {results.map(({ definitions, exists, isAllowed, word }) => (
35
- <div
36
- className={classNames(styles.result, {
37
- [styles.isAllowed]: isAllowed === true,
38
- [styles.isNotAllowed]: isAllowed === false,
39
- })}
40
- key={word}
41
- >
42
- <div className={styles.content}>
43
- {word && <h2 className={styles.word}>{word}</h2>}
35
+ {results.map(({ definitions, exists, isAllowed, word }) => (
36
+ <div
37
+ className={classNames(styles.result, {
38
+ [styles.isAllowed]: isAllowed === true,
39
+ [styles.isNotAllowed]: isAllowed === false,
40
+ })}
41
+ key={word}
42
+ >
43
+ <div className={styles.resultContent}>
44
+ {word && <h2 className={styles.word}>{word}</h2>}
44
45
 
45
- {isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
46
+ {isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
46
47
 
47
- {isAllowed === true && definitions.length === 0 && (
48
- <>
49
- <div>
50
- {exists
51
- ? translate('dictionary.empty-state.no-definitions')
52
- : translate('dictionary.empty-state.no-results')}
53
- </div>
54
- </>
55
- )}
48
+ {isAllowed === true && definitions.length === 0 && (
49
+ <>
50
+ <div>
51
+ {exists
52
+ ? translate('dictionary.empty-state.no-definitions')
53
+ : translate('dictionary.empty-state.no-results')}
54
+ </div>
55
+ </>
56
+ )}
56
57
 
57
- {isAllowed === true && definitions.length > 0 && (
58
- <ul className={styles.definitions}>
59
- {definitions.map((result, index) => (
60
- <li key={index} className={styles.definition}>
61
- {result}
62
- </li>
63
- ))}
64
- </ul>
65
- )}
58
+ {isAllowed === true && definitions.length > 0 && (
59
+ <ul className={styles.definitions}>
60
+ {definitions.map((result, index) => (
61
+ <li key={index} className={styles.definition}>
62
+ {result}
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ )}
66
67
 
67
- {!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
68
+ {!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
69
+ </div>
68
70
  </div>
69
- </div>
70
- ))}
71
+ ))}
72
+ </div>
71
73
 
72
74
  {isLoading && <Loading />}
73
75
  </div>
@@ -19,7 +19,7 @@ const IconButton: FunctionComponent<Props> = ({ className, Icon, tooltip, ...pro
19
19
  <Tooltip tooltip={tooltip}>
20
20
  <button className={classNames(styles.iconButton, className)} type="button" {...props}>
21
21
  <span className={styles.content}>
22
- <Icon className={styles.icon} />
22
+ <Icon aria-hidden="true" className={styles.icon} role="img" />
23
23
  </span>
24
24
  </button>
25
25
  </Tooltip>
@@ -18,7 +18,7 @@ const Link: FunctionComponent<Props> = ({ className, Icon, tooltip, ...props })
18
18
  <Tooltip tooltip={tooltip}>
19
19
  <a className={classNames(styles.iconButton, className)} {...props}>
20
20
  <span className={styles.content}>
21
- <Icon className={styles.icon} />
21
+ <Icon aria-hidden="true" className={styles.icon} role="img" />
22
22
  </span>
23
23
  </a>
24
24
  </Tooltip>
@@ -14,16 +14,16 @@ interface Props {
14
14
  const Arrows: FunctionComponent<Props> = ({ className }) => (
15
15
  <div className={classNames(styles.arrows, className)}>
16
16
  <Key className={classNames(styles.arrow, styles.left)}>
17
- <ArrowLeft />
17
+ <ArrowLeft aria-hidden="true" role="img" />
18
18
  </Key>
19
19
  <Key className={classNames(styles.arrow, styles.up)}>
20
- <ArrowUp />
20
+ <ArrowUp aria-hidden="true" role="img" />
21
21
  </Key>
22
22
  <Key className={classNames(styles.arrow, styles.right)}>
23
- <ArrowRight />
23
+ <ArrowRight aria-hidden="true" role="img" />
24
24
  </Key>
25
25
  <Key className={classNames(styles.arrow, styles.down)}>
26
- <ArrowDown />
26
+ <ArrowDown aria-hidden="true" role="img" />
27
27
  </Key>
28
28
  </div>
29
29
  );
@@ -2,6 +2,7 @@
2
2
  .dim {
3
3
  position: absolute;
4
4
  inset: 0;
5
+ border-radius: inherit;
5
6
  }
6
7
 
7
8
  .loading {
@@ -29,7 +29,7 @@ const Loading: FunctionComponent<Props> = ({ className, wave = true }) => {
29
29
  const content = useMemo(() => prepareContent(message), [message]);
30
30
 
31
31
  return (
32
- <div aria-label={translation} className={classNames(styles.loading, className)} role="status">
32
+ <div aria-label={translation} className={classNames(styles.loading, className)} data-testid="loading" role="status">
33
33
  <div className={styles.dim} />
34
34
  <div className={styles.text}>
35
35
  <PlainTiles className={classNames(styles.tiles)} content={content} dropShadow wave={wave} />
@@ -6,11 +6,12 @@ import styles from './Section.module.scss';
6
6
  interface Props {
7
7
  children: ReactNode;
8
8
  className?: string;
9
+ label: string;
9
10
  title: ReactNode;
10
11
  }
11
12
 
12
- const Section: FunctionComponent<Props> = ({ children, className, title }) => (
13
- <section className={classNames(styles.section, className)}>
13
+ const Section: FunctionComponent<Props> = ({ children, className, label, title }) => (
14
+ <section aria-label={label} className={classNames(styles.section, className)}>
14
15
  <h2 className={styles.heading}>{title}</h2>
15
16
  <div>{children}</div>
16
17
  </section>
@@ -125,6 +125,7 @@ const NavButtons: FunctionComponent<Props> = ({
125
125
 
126
126
  <IconButton
127
127
  aria-label={translate('settings')}
128
+ data-testid="settings-button"
128
129
  className={styles.button}
129
130
  Icon={Cog}
130
131
  tooltip={translate('settings')}
@@ -11,7 +11,7 @@ const CONTENT = [['HTTP', '404']];
11
11
  const NotFound: FunctionComponent = () => (
12
12
  <div className={styles.notFound}>
13
13
  <a className={styles.link} href="/">
14
- <DashCircleFill className={styles.icon} />
14
+ <DashCircleFill aria-hidden="true" className={styles.icon} role="img" />
15
15
  <PlainTiles className={styles.tiles} content={CONTENT} />
16
16
  </a>
17
17
  </div>
@@ -168,6 +168,7 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
168
168
  className={classNames(styles.rack, className, {
169
169
  [styles.hidden]: showInputPrompt,
170
170
  })}
171
+ data-testid="rack"
171
172
  ref={ref}
172
173
  style={{ fontSize: tileFontSize }}
173
174
  onPaste={handlePaste}
@@ -30,7 +30,7 @@ const Radio: FunctionComponent<Props> = ({ checked, children, className, disable
30
30
  onChange={onChange}
31
31
  />
32
32
 
33
- <div className={styles.icon} />
33
+ <div aria-hidden="true" className={styles.icon} role="img" />
34
34
 
35
35
  <div className={styles.content}>{children}</div>
36
36
  </label>
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { FunctionComponent, ReactNode } from 'react';
2
+ import { CSSProperties, FunctionComponent, ReactNode } from 'react';
3
3
 
4
4
  import { selectLocale, useTranslate, useTypedSelector } from 'state';
5
5
  import { TranslationKey } from 'types';
@@ -11,19 +11,23 @@ import styles from './Results.module.scss';
11
11
  interface Props {
12
12
  children?: ReactNode;
13
13
  className?: string;
14
+ dataTestId?: string;
15
+ style?: CSSProperties;
14
16
  translationKey: TranslationKey;
15
17
  tooltip?: string | number;
16
18
  value: string | number;
17
19
  }
18
20
 
19
- const Cell: FunctionComponent<Props> = ({ children, className, translationKey, tooltip, value }) => {
21
+ const Cell: FunctionComponent<Props> = ({ children, className, dataTestId, style, translationKey, tooltip, value }) => {
20
22
  const translate = useTranslate();
21
23
  const locale = useTypedSelector(selectLocale);
22
24
  const formattedValue = value.toLocaleString(locale);
23
25
 
24
26
  return (
25
27
  <Tooltip tooltip={`${translate(translationKey)}: ${tooltip || formattedValue}`}>
26
- <div className={classNames(styles.cell, className)}>{children || formattedValue}</div>
28
+ <div className={classNames(styles.cell, className)} data-testid={dataTestId} style={style}>
29
+ {children || formattedValue}
30
+ </div>
27
31
  </Tooltip>
28
32
  );
29
33
  };
@@ -0,0 +1,99 @@
1
+ import { FunctionComponent } from 'react';
2
+
3
+ import { useAppLayout, useColumns } from 'hooks';
4
+ import { GeoAlt, OneTwoThree, Square, SquareA, SquareB, Squares, Words } from 'icons';
5
+ import { RESULTS_COLUMN_WIDTH } from 'parameters';
6
+ import { ResultColumnId } from 'types';
7
+
8
+ import HeaderButton from './HeaderButton';
9
+ import styles from './Results.module.scss';
10
+
11
+ const Results: FunctionComponent = () => {
12
+ const { resultWordWidth } = useAppLayout();
13
+ const columns = useColumns();
14
+
15
+ return (
16
+ <div className={styles.header}>
17
+ {columns[ResultColumnId.Coordinates] && (
18
+ <HeaderButton
19
+ className={styles.coordinates}
20
+ Icon={GeoAlt}
21
+ id={ResultColumnId.Coordinates}
22
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.Coordinates] }}
23
+ translationKey="settings.showCoordinates"
24
+ />
25
+ )}
26
+
27
+ {columns[ResultColumnId.Word] && (
28
+ <HeaderButton
29
+ className={styles.word}
30
+ id={ResultColumnId.Word}
31
+ style={{ flexBasis: resultWordWidth }}
32
+ translationKey="common.word"
33
+ />
34
+ )}
35
+
36
+ {columns[ResultColumnId.TilesCount] && (
37
+ <HeaderButton
38
+ className={styles.stat}
39
+ Icon={Squares}
40
+ id={ResultColumnId.TilesCount}
41
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.TilesCount] }}
42
+ translationKey="common.tiles"
43
+ />
44
+ )}
45
+
46
+ {columns[ResultColumnId.VowelsCount] && (
47
+ <HeaderButton
48
+ className={styles.stat}
49
+ Icon={SquareA}
50
+ id={ResultColumnId.VowelsCount}
51
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.VowelsCount] }}
52
+ translationKey="common.vowels"
53
+ />
54
+ )}
55
+
56
+ {columns[ResultColumnId.ConsonantsCount] && (
57
+ <HeaderButton
58
+ className={styles.stat}
59
+ Icon={SquareB}
60
+ id={ResultColumnId.ConsonantsCount}
61
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.ConsonantsCount] }}
62
+ translationKey="common.consonants"
63
+ />
64
+ )}
65
+
66
+ {columns[ResultColumnId.BlanksCount] && (
67
+ <HeaderButton
68
+ className={styles.stat}
69
+ Icon={Square}
70
+ id={ResultColumnId.BlanksCount}
71
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.BlanksCount] }}
72
+ translationKey="common.blanks"
73
+ />
74
+ )}
75
+
76
+ {columns[ResultColumnId.WordsCount] && (
77
+ <HeaderButton
78
+ className={styles.stat}
79
+ Icon={Words}
80
+ id={ResultColumnId.WordsCount}
81
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.WordsCount] }}
82
+ translationKey="common.words"
83
+ />
84
+ )}
85
+
86
+ {columns[ResultColumnId.Points] && (
87
+ <HeaderButton
88
+ className={styles.points}
89
+ Icon={OneTwoThree}
90
+ id={ResultColumnId.Points}
91
+ style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.Points] }}
92
+ translationKey="common.points"
93
+ />
94
+ )}
95
+ </div>
96
+ );
97
+ };
98
+
99
+ export default Results;