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 (150) hide show
  1. package/.eslintrc.js +13 -4
  2. package/.github/workflows/build.yml +6 -14
  3. package/.github/workflows/cypress.yml +28 -0
  4. package/.github/workflows/eslint.yml +9 -16
  5. package/.github/workflows/jest.yml +21 -0
  6. package/.nx/cache/nx_files.nxt +0 -0
  7. package/.nx/workspace-data/file-map.json +639 -543
  8. package/.nx/workspace-data/lockfile.hash +1 -1
  9. package/.nx/workspace-data/parsed-lock-file.json +6131 -5552
  10. package/.nx/workspace-data/project-graph.json +4276 -2896
  11. package/README.md +5 -3
  12. package/bin/scrabble-solver.js +0 -0
  13. package/cypress/e2e/app.spec.cy.ts +118 -0
  14. package/cypress/e2e/bugs/112-character-bonus.ts +40 -0
  15. package/cypress/e2e/bugs/129-esc-sidebar.ts +23 -0
  16. package/cypress/e2e/bugs/2-q-tile-in-english.ts +33 -0
  17. package/cypress/e2e/bugs/3-x-tile-in-polish.ts +33 -0
  18. package/cypress/e2e/bugs/30-click-inside-radio-icon.ts +26 -0
  19. package/cypress/e2e/bugs/333-disabled-radio-cursor.ts +24 -0
  20. package/cypress/e2e/bugs/342-results-sorting.ts +48 -0
  21. package/cypress/e2e/bugs/348-results-word-padding.ts +74 -0
  22. package/cypress/e2e/bugs.spec.cy.ts +8 -0
  23. package/cypress/support/commands.ts +4 -0
  24. package/cypress/support/e2e.ts +1 -0
  25. package/cypress/support/index.ts +1 -0
  26. package/cypress/support/lib/actions.ts +57 -0
  27. package/cypress/support/lib/index.ts +2 -0
  28. package/cypress/support/lib/selectors.ts +70 -0
  29. package/cypress/tsconfig.json +9 -0
  30. package/cypress.config.ts +9 -0
  31. package/jest.config.js +2 -3
  32. package/lerna.json +1 -1
  33. package/package.json +29 -22
  34. package/packages/configs/package.json +3 -3
  35. package/packages/constants/package.json +1 -1
  36. package/packages/dictionaries/package-lock.json +2 -2
  37. package/packages/dictionaries/package.json +4 -4
  38. package/packages/logger/package-lock.json +3 -3
  39. package/packages/logger/package.json +2 -2
  40. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  41. package/packages/scrabble-solver/.next/build-manifest.json +14 -14
  42. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  43. package/packages/scrabble-solver/.next/cache/eslint/.cache_8dgz12 +1 -1
  44. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  45. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  46. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  47. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/0.pack +0 -0
  48. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  49. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  50. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  51. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  52. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  53. package/packages/scrabble-solver/.next/prerender-manifest.js +1 -1
  54. package/packages/scrabble-solver/.next/prerender-manifest.json +1 -1
  55. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  56. package/packages/scrabble-solver/.next/server/chunks/807.js +1 -1
  57. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  58. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  59. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  60. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  61. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  62. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  63. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  64. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  65. package/packages/scrabble-solver/.next/static/{N8hSsS6Ppzlj3ebHMSZvI → TxfjxrH5h31sPQxPhrJzG}/_buildManifest.js +1 -1
  66. package/packages/scrabble-solver/.next/static/chunks/{main-b5b360c6afb66b05.js → main-8b0b4e610892a916.js} +1 -1
  67. package/packages/scrabble-solver/.next/static/chunks/pages/{404-0c9f3e0f8b15f487.js → 404-b0c2ccded2455be0.js} +1 -1
  68. package/packages/scrabble-solver/.next/static/chunks/pages/_app-2912876c7b6e698e.js +17 -0
  69. package/packages/scrabble-solver/.next/static/chunks/pages/index-86d9ad372c48c8b7.js +1 -0
  70. package/packages/scrabble-solver/.next/static/chunks/webpack-c4acd79e87956a0e.js +1 -0
  71. package/packages/scrabble-solver/.next/static/css/14625ff7b4d265d0.css +2 -0
  72. package/packages/scrabble-solver/.next/trace +45 -45
  73. package/packages/scrabble-solver/package-lock.json +6 -6
  74. package/packages/scrabble-solver/package.json +13 -13
  75. package/packages/scrabble-solver/public/service-worker.js +1 -1
  76. package/packages/scrabble-solver/src/components/Alert/Alert.tsx +1 -1
  77. package/packages/scrabble-solver/src/components/Board/Board.module.scss +3 -9
  78. package/packages/scrabble-solver/src/components/Board/Board.tsx +2 -2
  79. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +4 -3
  80. package/packages/scrabble-solver/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +2 -0
  81. package/packages/scrabble-solver/src/components/Board/hooks/useBackgroundImage.tsx +2 -6
  82. package/packages/scrabble-solver/src/components/Board/hooks/useBoardStyle.ts +10 -4
  83. package/packages/scrabble-solver/src/components/Button/Button.tsx +1 -1
  84. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +10 -7
  85. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +38 -36
  86. package/packages/scrabble-solver/src/components/IconButton/IconButton.tsx +1 -1
  87. package/packages/scrabble-solver/src/components/IconButton/Link.tsx +1 -1
  88. package/packages/scrabble-solver/src/components/Keys/Arrows/Arrows.tsx +4 -4
  89. package/packages/scrabble-solver/src/components/Loading/Loading.module.scss +1 -0
  90. package/packages/scrabble-solver/src/components/Loading/Loading.tsx +1 -1
  91. package/packages/scrabble-solver/src/components/Modal/components/Section/Section.tsx +3 -2
  92. package/packages/scrabble-solver/src/components/NavButtons/NavButtons.tsx +1 -0
  93. package/packages/scrabble-solver/src/components/NotFound/NotFound.tsx +1 -1
  94. package/packages/scrabble-solver/src/components/Rack/Rack.tsx +1 -0
  95. package/packages/scrabble-solver/src/components/Radio/Radio.tsx +1 -1
  96. package/packages/scrabble-solver/src/components/Results/Cell.tsx +7 -3
  97. package/packages/scrabble-solver/src/components/Results/Header.tsx +99 -0
  98. package/packages/scrabble-solver/src/components/Results/HeaderButton.tsx +18 -13
  99. package/packages/scrabble-solver/src/components/Results/Result.tsx +23 -16
  100. package/packages/scrabble-solver/src/components/Results/Results.module.scss +27 -18
  101. package/packages/scrabble-solver/src/components/Results/Results.tsx +3 -9
  102. package/packages/scrabble-solver/src/components/Results/types.ts +0 -8
  103. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +1 -1
  104. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +1 -1
  105. package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +2 -1
  106. package/packages/scrabble-solver/src/hooks/index.ts +1 -0
  107. package/packages/scrabble-solver/src/hooks/useAppLayout.ts +12 -1
  108. package/packages/scrabble-solver/src/hooks/useColumns.ts +47 -0
  109. package/packages/scrabble-solver/src/icons/GeoAlt.svg +5 -0
  110. package/packages/scrabble-solver/src/icons/OneTwoThree.svg +4 -0
  111. package/packages/scrabble-solver/src/icons/SquareA.svg +6 -0
  112. package/packages/scrabble-solver/src/icons/SquareB.svg +6 -0
  113. package/packages/scrabble-solver/src/icons/Squares.svg +34 -0
  114. package/packages/scrabble-solver/src/icons/Words.svg +22 -0
  115. package/packages/scrabble-solver/src/icons/index.ts +6 -0
  116. package/packages/scrabble-solver/src/lib/groupResults.ts +1 -1
  117. package/packages/scrabble-solver/src/lib/index.ts +0 -1
  118. package/packages/scrabble-solver/src/lib/sortResults.ts +10 -10
  119. package/packages/scrabble-solver/src/modals/KeyMapModal/KeyMapModal.tsx +8 -9
  120. package/packages/scrabble-solver/src/modals/MenuModal/MenuModal.tsx +1 -1
  121. package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +1 -0
  122. package/packages/scrabble-solver/src/modals/SettingsModal/SettingsModal.tsx +5 -5
  123. package/packages/scrabble-solver/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +1 -1
  124. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +4 -2
  125. package/packages/scrabble-solver/src/parameters/index.ts +12 -0
  126. package/packages/scrabble-solver/src/state/selectors.ts +26 -1
  127. package/packages/scrabble-solver/src/state/slices/resultsInitialState.ts +2 -2
  128. package/packages/scrabble-solver/src/state/slices/resultsSlice.ts +2 -2
  129. package/packages/scrabble-solver/src/state/useTranslate.ts +5 -1
  130. package/packages/scrabble-solver/src/styles/variables.scss +1 -0
  131. package/packages/scrabble-solver/src/types/index.ts +11 -2
  132. package/packages/solver/package-lock.json +2 -2
  133. package/packages/solver/package.json +5 -5
  134. package/packages/types/package.json +2 -2
  135. package/packages/word-definitions/build/lib/request.d.ts +0 -1
  136. package/packages/word-definitions/package-lock.json +2 -2
  137. package/packages/word-definitions/package.json +2 -2
  138. package/packages/word-lists/package-lock.json +3 -3
  139. package/packages/word-lists/package.json +3 -3
  140. package/tsconfig.jest.json +2 -1
  141. package/.github/workflows/test.yml +0 -28
  142. package/packages/scrabble-solver/.next/static/chunks/pages/_app-8246f5b39b6a5e59.js +0 -17
  143. package/packages/scrabble-solver/.next/static/chunks/pages/index-65bfe83d121535ab.js +0 -1
  144. package/packages/scrabble-solver/.next/static/chunks/webpack-6ef43a8d4a395f49.js +0 -1
  145. package/packages/scrabble-solver/.next/static/css/2f727b21d1331ea5.css +0 -2
  146. package/packages/scrabble-solver/src/components/Results/getCoordinatesColumn.ts +0 -14
  147. package/packages/scrabble-solver/src/components/Results/getLocaleColumns.ts +0 -58
  148. package/packages/scrabble-solver/src/components/Results/useColumns.ts +0 -44
  149. package/packages/scrabble-solver/src/lib/dataUrlToBlob.ts +0 -20
  150. /package/packages/scrabble-solver/.next/static/{N8hSsS6Ppzlj3ebHMSZvI → TxfjxrH5h31sPQxPhrJzG}/_ssgManifest.js +0 -0
@@ -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;
@@ -1,42 +1,47 @@
1
1
  import classNames from 'classnames';
2
- import { ReactElement, useCallback } from 'react';
2
+ import { CSSProperties, FunctionComponent, ReactElement, SVGAttributes, useCallback } from 'react';
3
3
  import { useDispatch } from 'react-redux';
4
4
 
5
5
  import { SortDown, SortUp } from 'icons';
6
6
  import { resultsSlice, selectResultsSort, useTranslate, useTypedSelector } from 'state';
7
- import { SortDirection } from 'types';
7
+ import { ResultColumnId, SortDirection, TranslationKey } from 'types';
8
8
 
9
9
  import { Tooltip } from '../Tooltip';
10
10
 
11
11
  import styles from './Results.module.scss';
12
- import { Column } from './types';
13
12
 
14
13
  interface Props {
15
- column: Column;
14
+ className: string;
15
+ Icon?: FunctionComponent<SVGAttributes<SVGElement>>;
16
+ id: ResultColumnId;
17
+ translationKey: TranslationKey;
18
+ style?: CSSProperties;
16
19
  }
17
20
 
18
- const HeaderButton = ({ column }: Props): ReactElement => {
21
+ const HeaderButton = ({ className, Icon, id, translationKey, style }: Props): ReactElement => {
19
22
  const dispatch = useDispatch();
20
23
  const translate = useTranslate();
21
24
  const sort = useTypedSelector(selectResultsSort);
22
25
 
23
26
  const handleClick = useCallback(() => {
24
- dispatch(resultsSlice.actions.sort(column.id));
25
- }, [column.id, dispatch]);
27
+ dispatch(resultsSlice.actions.sort(id));
28
+ }, [dispatch, id]);
26
29
 
27
30
  return (
28
- <Tooltip tooltip={translate(column.translationKey)}>
31
+ <Tooltip tooltip={translate(translationKey)}>
29
32
  <button
30
- aria-label={translate(column.translationKey)}
31
- className={classNames(styles.headerButton, column.className)}
32
- key={column.id}
33
+ aria-label={translate(translationKey)}
34
+ className={classNames(styles.headerButton, className)}
35
+ style={style}
33
36
  type="button"
34
37
  onClick={handleClick}
35
38
  >
36
39
  <span className={styles.cell}>
37
- <span className={styles.headerButtonLabel}>{translate(column.translationKey)}</span>
40
+ {Icon && <Icon aria-hidden="true" className={styles.headerButtonIcon} role="img" />}
38
41
 
39
- {sort.column === column.id && (
42
+ {!Icon && <span className={styles.headerButtonLabel}>{translate(translationKey)}</span>}
43
+
44
+ {sort.column === id && (
40
45
  <>
41
46
  {sort.direction === SortDirection.Ascending && <SortUp className={styles.sortIcon} />}
42
47
  {sort.direction === SortDirection.Descending && <SortDown className={styles.sortIcon} />}
@@ -2,6 +2,7 @@ import classNames from 'classnames';
2
2
  import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, useMemo, useRef } from 'react';
3
3
  import Highlighter from 'react-highlight-words';
4
4
 
5
+ import { useAppLayout, useColumns } from 'hooks';
5
6
  import { LOCALE_FEATURES } from 'i18n';
6
7
  import { getCoordinates, noop } from 'lib';
7
8
  import {
@@ -11,12 +12,11 @@ import {
11
12
  selectShowCoordinates,
12
13
  useTypedSelector,
13
14
  } from 'state';
14
- import { ResultColumn } from 'types';
15
+ import { ResultColumnId } from 'types';
15
16
 
16
17
  import Cell from './Cell';
17
18
  import styles from './Results.module.scss';
18
19
  import { ResultData } from './types';
19
- import useColumns from './useColumns';
20
20
 
21
21
  interface Props {
22
22
  data: ResultData;
@@ -34,16 +34,16 @@ const Result = ({ data, index, style }: Props): ReactElement => {
34
34
  onMouseEnter = noop,
35
35
  onMouseLeave = noop,
36
36
  } = data;
37
+ const { resultWordWidth } = useAppLayout();
37
38
  const ref = useRef<HTMLButtonElement>(null);
38
39
  const columns = useColumns();
39
40
  const locale = useTypedSelector(selectLocale);
40
41
  const showCoordinates = useTypedSelector(selectShowCoordinates);
41
42
  const query = useTypedSelector(selectResultsQuery);
42
- const { consonants, direction, separator, vowels } = LOCALE_FEATURES[locale];
43
+ const { direction, separator } = LOCALE_FEATURES[locale];
43
44
  const result = results[index];
44
45
  const isMatching = useTypedSelector((state) => selectIsResultMatching(state, index));
45
46
  const words = direction === 'rtl' ? [...result.words].reverse() : result.words;
46
- const enabledColumns = Object.fromEntries(columns.map((column) => [column.id, true]));
47
47
  const coordinates = useMemo(() => getCoordinates(result, showCoordinates), [result, showCoordinates]);
48
48
 
49
49
  const handleClick: MouseEventHandler = (event) => onClick(result, event);
@@ -54,11 +54,13 @@ const Result = ({ data, index, style }: Props): ReactElement => {
54
54
 
55
55
  return (
56
56
  <button
57
+ aria-hidden={isMatching ? undefined : 'true'}
57
58
  aria-label={result.word}
58
59
  className={classNames(styles.result, {
59
60
  [styles.highlighted]: index === highlightedIndex,
60
61
  [styles.notMatching]: !isMatching,
61
62
  })}
63
+ data-testid="result"
62
64
  ref={ref}
63
65
  style={style}
64
66
  type="button"
@@ -69,33 +71,38 @@ const Result = ({ data, index, style }: Props): ReactElement => {
69
71
  onMouseLeave={handleMouseLeave}
70
72
  >
71
73
  <span className={styles.resultContent}>
72
- {enabledColumns[ResultColumn.Coordinates] && (
74
+ {columns[ResultColumnId.Coordinates] && (
73
75
  <Cell className={styles.coordinates} translationKey="settings.showCoordinates" value={coordinates} />
74
76
  )}
75
77
 
76
- {enabledColumns[ResultColumn.Word] && (
77
- <Cell className={styles.word} translationKey="common.word" value={result.word}>
78
+ {columns[ResultColumnId.Word] && (
79
+ <Cell
80
+ className={styles.word}
81
+ style={{ flexBasis: resultWordWidth }}
82
+ translationKey="common.word"
83
+ value={result.word}
84
+ >
78
85
  <Highlighter highlightClassName={styles.highlight} searchWords={[query]} textToHighlight={result.word} />
79
86
  </Cell>
80
87
  )}
81
88
 
82
- {enabledColumns[ResultColumn.TilesCount] && (
89
+ {columns[ResultColumnId.TilesCount] && (
83
90
  <Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
84
91
  )}
85
92
 
86
- {enabledColumns[ResultColumn.ConsonantsCount] && consonants && (
87
- <Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
93
+ {columns[ResultColumnId.VowelsCount] && (
94
+ <Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
88
95
  )}
89
96
 
90
- {enabledColumns[ResultColumn.VowelsCount] && vowels && (
91
- <Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
97
+ {columns[ResultColumnId.ConsonantsCount] && (
98
+ <Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
92
99
  )}
93
100
 
94
- {enabledColumns[ResultColumn.BlanksCount] && (
101
+ {columns[ResultColumnId.BlanksCount] && (
95
102
  <Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
96
103
  )}
97
104
 
98
- {enabledColumns[ResultColumn.WordsCount] && (
105
+ {columns[ResultColumnId.WordsCount] && (
99
106
  <Cell
100
107
  className={styles.stat}
101
108
  translationKey="common.words"
@@ -104,8 +111,8 @@ const Result = ({ data, index, style }: Props): ReactElement => {
104
111
  />
105
112
  )}
106
113
 
107
- {enabledColumns[ResultColumn.Points] && (
108
- <Cell className={styles.points} translationKey="common.points" value={result.points} />
114
+ {columns[ResultColumnId.Points] && (
115
+ <Cell className={styles.points} dataTestId="points" translationKey="common.points" value={result.points} />
109
116
  )}
110
117
  </span>
111
118
  </button>
@@ -44,7 +44,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
44
44
  display: flex;
45
45
  align-items: center;
46
46
  justify-content: space-between;
47
- font-weight: 700;
48
47
  border-bottom: var(--border);
49
48
  border-top-left-radius: inherit;
50
49
  border-top-right-radius: inherit;
@@ -53,11 +52,15 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
53
52
  .headerButton {
54
53
  @include button-reset;
55
54
  @include focus-effect;
55
+
56
56
  cursor: pointer;
57
57
 
58
58
  text-transform: uppercase;
59
- transition: var(--transition);
59
+ transition:
60
+ background-color var(--transition--duration) var(--transition--easing),
61
+ color var(--transition--duration) var(--transition--easing);
60
62
  background-color: var(--color--background);
63
+ height: 100%;
61
64
 
62
65
  &:first-child {
63
66
  border-start-start-radius: inherit;
@@ -72,6 +75,17 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
72
75
  background-color: var(--color--primary);
73
76
  color: var(--color--primary--opposite);
74
77
  }
78
+
79
+ &.points {
80
+ @include scrollbars;
81
+
82
+ overflow-y: scroll;
83
+
84
+ &,
85
+ &:hover {
86
+ scrollbar-color: transparent transparent;
87
+ }
88
+ }
75
89
  }
76
90
 
77
91
  .headerButtonLabel {
@@ -81,6 +95,12 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
81
95
  text-align: start;
82
96
  }
83
97
 
98
+ .headerButtonIcon {
99
+ flex: 0 0 auto;
100
+ width: var(--results--icon--size);
101
+ height: var(--results--icon--size);
102
+ }
103
+
84
104
  .result {
85
105
  @include button-reset;
86
106
 
@@ -98,7 +118,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
98
118
  }
99
119
  }
100
120
 
101
- &.notMatching {
121
+ &[aria-hidden='true'] {
102
122
  color: var(--color--inactive);
103
123
 
104
124
  &:focus,
@@ -120,6 +140,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
120
140
 
121
141
  .word {
122
142
  @include ellipsis;
143
+
123
144
  display: flex;
124
145
  }
125
146
  }
@@ -129,33 +150,22 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
129
150
  align-items: center;
130
151
  justify-content: center;
131
152
  height: 100%;
132
- padding: 0 var(--spacing--s);
133
153
  gap: var(--spacing--s);
134
154
  line-height: var(--results--item--height);
135
155
 
136
- &.word,
156
+ .result &.word,
137
157
  .headerButton.word & {
138
158
  justify-content: flex-start;
139
- }
140
-
141
- .result &:first-child,
142
- .headerButton:first-child & {
143
- text-align: start;
144
159
  padding-inline-start: $row-padding-horizontal;
145
160
  }
146
161
 
147
- .result &:last-child,
148
- .headerButton:last-child & {
149
- padding-inline-end: $row-padding-horizontal;
150
- }
151
-
152
162
  &:last-child {
153
163
  flex: 1;
154
164
  }
155
165
  }
156
166
 
157
167
  .word {
158
- flex: 1 0;
168
+ flex: 0 0;
159
169
  text-transform: uppercase;
160
170
  }
161
171
 
@@ -171,12 +181,11 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
171
181
  $width: 80px;
172
182
 
173
183
  flex: 1 0 $width;
174
- max-width: $width;
175
184
  font-weight: bold;
176
185
  }
177
186
 
178
187
  .coordinates {
179
- $width: 50px;
188
+ $width: 55px;
180
189
 
181
190
  flex: 0 0 $width;
182
191
  max-width: $width;