@cloud-ru/ds-rating 1.0.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 (75) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +88 -0
  4. package/dist/cjs/components/Rating/Rating.d.ts +28 -0
  5. package/dist/cjs/components/Rating/Rating.js +53 -0
  6. package/dist/cjs/components/Rating/index.d.ts +1 -0
  7. package/dist/cjs/components/Rating/index.js +17 -0
  8. package/dist/cjs/components/Rating/styles.module.css +4 -0
  9. package/dist/cjs/components/index.d.ts +1 -0
  10. package/dist/cjs/components/index.js +17 -0
  11. package/dist/cjs/constants.d.ts +24 -0
  12. package/dist/cjs/constants.js +27 -0
  13. package/dist/cjs/helperComponents/RatingStar/RatingStar.d.ts +29 -0
  14. package/dist/cjs/helperComponents/RatingStar/RatingStar.js +18 -0
  15. package/dist/cjs/helperComponents/RatingStar/constants.d.ts +5 -0
  16. package/dist/cjs/helperComponents/RatingStar/constants.js +8 -0
  17. package/dist/cjs/helperComponents/RatingStar/index.d.ts +3 -0
  18. package/dist/cjs/helperComponents/RatingStar/index.js +20 -0
  19. package/dist/cjs/helperComponents/RatingStar/styles.module.css +210 -0
  20. package/dist/cjs/helperComponents/RatingStar/types.d.ts +3 -0
  21. package/dist/cjs/helperComponents/RatingStar/types.js +2 -0
  22. package/dist/cjs/helperComponents/index.d.ts +1 -0
  23. package/dist/cjs/helperComponents/index.js +17 -0
  24. package/dist/cjs/index.d.ts +3 -0
  25. package/dist/cjs/index.js +19 -0
  26. package/dist/cjs/package.json +3 -0
  27. package/dist/cjs/style.css +214 -0
  28. package/dist/cjs/types.d.ts +4 -0
  29. package/dist/cjs/types.js +2 -0
  30. package/dist/cjs/utils.d.ts +3 -0
  31. package/dist/cjs/utils.js +19 -0
  32. package/dist/esm/components/Rating/Rating.d.ts +28 -0
  33. package/dist/esm/components/Rating/Rating.js +47 -0
  34. package/dist/esm/components/Rating/index.d.ts +1 -0
  35. package/dist/esm/components/Rating/index.js +1 -0
  36. package/dist/esm/components/Rating/styles.module.css +4 -0
  37. package/dist/esm/components/index.d.ts +1 -0
  38. package/dist/esm/components/index.js +1 -0
  39. package/dist/esm/constants.d.ts +24 -0
  40. package/dist/esm/constants.js +24 -0
  41. package/dist/esm/helperComponents/RatingStar/RatingStar.d.ts +29 -0
  42. package/dist/esm/helperComponents/RatingStar/RatingStar.js +12 -0
  43. package/dist/esm/helperComponents/RatingStar/constants.d.ts +5 -0
  44. package/dist/esm/helperComponents/RatingStar/constants.js +5 -0
  45. package/dist/esm/helperComponents/RatingStar/index.d.ts +3 -0
  46. package/dist/esm/helperComponents/RatingStar/index.js +2 -0
  47. package/dist/esm/helperComponents/RatingStar/styles.module.css +210 -0
  48. package/dist/esm/helperComponents/RatingStar/types.d.ts +3 -0
  49. package/dist/esm/helperComponents/RatingStar/types.js +1 -0
  50. package/dist/esm/helperComponents/index.d.ts +1 -0
  51. package/dist/esm/helperComponents/index.js +1 -0
  52. package/dist/esm/index.d.ts +3 -0
  53. package/dist/esm/index.js +3 -0
  54. package/dist/esm/style.css +214 -0
  55. package/dist/esm/types.d.ts +4 -0
  56. package/dist/esm/types.js +1 -0
  57. package/dist/esm/utils.d.ts +3 -0
  58. package/dist/esm/utils.js +15 -0
  59. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  60. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  61. package/package.json +43 -0
  62. package/src/components/Rating/Rating.tsx +110 -0
  63. package/src/components/Rating/index.ts +1 -0
  64. package/src/components/Rating/styles.module.scss +4 -0
  65. package/src/components/index.ts +1 -0
  66. package/src/constants.ts +27 -0
  67. package/src/helperComponents/RatingStar/RatingStar.tsx +86 -0
  68. package/src/helperComponents/RatingStar/constants.ts +5 -0
  69. package/src/helperComponents/RatingStar/index.ts +3 -0
  70. package/src/helperComponents/RatingStar/styles.module.scss +83 -0
  71. package/src/helperComponents/RatingStar/types.ts +5 -0
  72. package/src/helperComponents/index.ts +1 -0
  73. package/src/index.ts +3 -0
  74. package/src/types.ts +7 -0
  75. package/src/utils.ts +20 -0
@@ -0,0 +1,110 @@
1
+ import { extractSupportProps, useUncontrolledProp, WithSupportProps } from '@cloud-ru/ds-utils';
2
+ import cn from 'classnames';
3
+ import { KeyboardEventHandler, useState } from 'react';
4
+
5
+ import { APPEARANCE, DEFAULT_RATING_VALUE, DEFAULT_STAR_COUNT, SIZE, TEST_IDS } from '../../constants';
6
+ import { RatingStar, Value } from '../../helperComponents';
7
+ import { Appearance, Size } from '../../types';
8
+ import { getRatingStarValue, getStarValue } from '../../utils';
9
+ import styles from './styles.module.scss';
10
+
11
+ export type RatingProps = WithSupportProps<{
12
+ /** Размер */
13
+ size?: Size;
14
+ /** Внешний вид (цветовая схема) */
15
+ appearance?: Appearance;
16
+ /** Общее количество звезд */
17
+ count: number;
18
+ /** Значение количества звезд в случае необходимости управления */
19
+ value?: number;
20
+ /** Количество звезд, заполненных по умолчанию */
21
+ defaultValue?: number;
22
+ /** Показывать или нет рейтинг в виде половины звезды */
23
+ allowHalf: boolean;
24
+ /** Разрещает сброс рейтинга при повторном нажатии на звезду */
25
+ allowClear: boolean;
26
+ /** Является ли поле доступным на изменение */
27
+ readonly: boolean;
28
+ /** Колбек, вызываемый на смену состояния */
29
+ onChange?(value: number): void;
30
+ /** CSS-класс */
31
+ className?: string;
32
+ }>;
33
+
34
+ /**
35
+ * Компонент Rating
36
+ */
37
+
38
+ export function Rating({
39
+ count = DEFAULT_STAR_COUNT,
40
+ value,
41
+ defaultValue = DEFAULT_RATING_VALUE,
42
+ allowHalf = false,
43
+ allowClear = false,
44
+ readonly = false,
45
+ appearance = APPEARANCE.Yellow,
46
+ size = SIZE.S,
47
+ className,
48
+ onChange,
49
+ ...rest
50
+ }: RatingProps) {
51
+ const [hoverRating, setHoverRating] = useState<null | number>(null);
52
+ const [rating, setRating] = useUncontrolledProp(value, defaultValue, onChange);
53
+
54
+ function getRating(updatedValue: number): number {
55
+ return rating === updatedValue && allowClear ? 0 : updatedValue;
56
+ }
57
+
58
+ const handleMouseEnter = (starIndex: number) => (value: Value) => {
59
+ if (readonly) {
60
+ return;
61
+ }
62
+ setHoverRating(getStarValue(starIndex, value, allowHalf));
63
+ };
64
+
65
+ const handleMouseLeave = () => {
66
+ if (readonly) {
67
+ return;
68
+ }
69
+ setHoverRating(null);
70
+ };
71
+
72
+ const handleClick = (starIndex: number) => (value: Value) => {
73
+ if (readonly) {
74
+ return;
75
+ }
76
+
77
+ setRating(getRating(getStarValue(starIndex, value, allowHalf)));
78
+ };
79
+
80
+ const handleKeyDown: (index: number) => KeyboardEventHandler<HTMLDivElement> = (starIndex: number) => e => {
81
+ if (e.code === 'Enter' || e.code === 'Space') {
82
+ e.stopPropagation();
83
+ setRating(getRating(starIndex));
84
+ }
85
+ };
86
+
87
+ return (
88
+ <div className={cn(styles.rating, className)} {...extractSupportProps(rest)}>
89
+ {[...Array(count)].map((_, index) => {
90
+ const starIndex = index + 1;
91
+ const value = getRatingStarValue(starIndex, hoverRating ?? rating, allowHalf);
92
+
93
+ return (
94
+ <RatingStar
95
+ key={index}
96
+ value={value}
97
+ size={size}
98
+ readonly={readonly}
99
+ appearance={appearance}
100
+ handleMouseEnter={handleMouseEnter(starIndex)}
101
+ handleMouseLeave={handleMouseLeave}
102
+ handleKeyDown={handleKeyDown(starIndex)}
103
+ handleClick={handleClick(starIndex)}
104
+ data-test-id={`${TEST_IDS.star}-${starIndex}`}
105
+ />
106
+ );
107
+ })}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1 @@
1
+ export * from './Rating';
@@ -0,0 +1,4 @@
1
+ .rating {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ }
@@ -0,0 +1 @@
1
+ export * from './Rating';
@@ -0,0 +1,27 @@
1
+ export const APPEARANCE = {
2
+ Primary: 'primary',
3
+ Neutral: 'neutral',
4
+ Red: 'red',
5
+ Orange: 'orange',
6
+ Yellow: 'yellow',
7
+ Green: 'green',
8
+ Blue: 'blue',
9
+ Violet: 'violet',
10
+ Pink: 'pink',
11
+ } as const;
12
+
13
+ export const SIZE = {
14
+ Xs: 'xs',
15
+ S: 's',
16
+ } as const;
17
+
18
+ export const TEST_IDS = {
19
+ root: 'rating',
20
+ star: 'rating__star',
21
+ starHalfLeft: 'rating__star-half-left',
22
+ starHalfRight: 'rating__star-half-right',
23
+ } as const;
24
+
25
+ export const DEFAULT_STAR_COUNT = 5;
26
+ export const DEFAULT_RATING_VALUE = 0;
27
+ export const HALF_STAR_VALUE = 0.5;
@@ -0,0 +1,86 @@
1
+ import { StarFilledSVG } from '@cloud-ru/ds-icons/interface/system';
2
+ import cn from 'classnames';
3
+ import { KeyboardEventHandler } from 'react';
4
+
5
+ import { APPEARANCE, SIZE, TEST_IDS } from '../../constants';
6
+ import { Appearance, Size } from '../../types';
7
+ import { VALUE } from './constants';
8
+ import styles from './styles.module.scss';
9
+ import { Value } from './types';
10
+
11
+ export type RatingStarProps = {
12
+ /** Значение */
13
+ value?: Value;
14
+ /** Размер */
15
+ size?: Size;
16
+ /** Внешний вид (цветовая схема) */
17
+ appearance?: Appearance;
18
+ /** Является ли поле доступным на изменение */
19
+ readonly?: boolean;
20
+ /** CSS-класс */
21
+ className?: string;
22
+ /** Действие при наведении мыши */
23
+ handleMouseEnter?(value: Value): void;
24
+ /** Действие при уведении мыши */
25
+ handleMouseLeave?(): void;
26
+ /** Действие при нажатии на клавишу клавиатуры */
27
+ handleKeyDown?: KeyboardEventHandler<HTMLDivElement>;
28
+ /** Действие при клике части звезды мышью */
29
+ handleClick?(value: Value): void;
30
+ /** Стабильный идентификатор для e2e */
31
+ 'data-test-id'?: string;
32
+ };
33
+
34
+ /**
35
+ * Компонент RatingStar
36
+ */
37
+
38
+ export function RatingStar({
39
+ value = VALUE.Zero,
40
+ appearance = APPEARANCE.Yellow,
41
+ size = SIZE.S,
42
+ className,
43
+ readonly = false,
44
+ handleMouseEnter,
45
+ handleMouseLeave,
46
+ handleKeyDown,
47
+ handleClick,
48
+ 'data-test-id': dataTestId,
49
+ }: RatingStarProps) {
50
+ return (
51
+ <div
52
+ className={cn(styles.ratingStar, className)}
53
+ onKeyDown={handleKeyDown}
54
+ tabIndex={readonly ? undefined : 0}
55
+ data-value={value}
56
+ data-size={size}
57
+ data-appearance={appearance}
58
+ aria-checked={value !== VALUE.Zero}
59
+ role={readonly ? undefined : 'radio'}
60
+ data-test-id={dataTestId}
61
+ >
62
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- click-handler на обёртке нужен,
63
+ чтобы клик по любому месту половинки (включая SVG-иконку с её pointer-events) вызывал onClick.
64
+ Сама звезда (родитель) имеет role='radio' и tabIndex — это публичный интерактив. */}
65
+ <div
66
+ className={styles.firstStarContainer}
67
+ data-test-id={dataTestId ? TEST_IDS.starHalfLeft : undefined}
68
+ onMouseEnter={handleMouseEnter ? () => handleMouseEnter(VALUE.Half) : undefined}
69
+ onMouseLeave={handleMouseLeave || undefined}
70
+ onClick={handleClick ? () => handleClick(VALUE.Half) : undefined}
71
+ >
72
+ <StarFilledSVG className={styles.icon} />
73
+ </div>
74
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
75
+ <div
76
+ className={styles.secondStarContainer}
77
+ data-test-id={dataTestId ? TEST_IDS.starHalfRight : undefined}
78
+ onMouseEnter={handleMouseEnter ? () => handleMouseEnter(VALUE.Full) : undefined}
79
+ onMouseLeave={handleMouseLeave}
80
+ onClick={handleClick ? () => handleClick(VALUE.Full) : undefined}
81
+ >
82
+ <StarFilledSVG className={styles.icon} />
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,5 @@
1
+ export const VALUE = {
2
+ Zero: '0%',
3
+ Half: '50%',
4
+ Full: '100%',
5
+ } as const;
@@ -0,0 +1,3 @@
1
+ export * from './RatingStar';
2
+ export type { Value } from './types';
3
+ export { VALUE } from './constants';
@@ -0,0 +1,83 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/rating.module' as rating;
3
+
4
+ $sizes: 'xs', 's';
5
+ $appearances: 'primary', 'neutral', 'red', 'orange', 'yellow', 'green', 'blue', 'violet', 'pink';
6
+
7
+ @mixin star-color-value($firstHalfColor, $secondHalfColor) {
8
+ .firstStarContainer {
9
+ color: $firstHalfColor;
10
+ }
11
+
12
+ .secondStarContainer {
13
+ color: $secondHalfColor;
14
+ }
15
+ }
16
+
17
+ .firstStarContainer {
18
+ position: absolute;
19
+ top: 0;
20
+ left: 0;
21
+
22
+ overflow: hidden;
23
+
24
+ width: 50%;
25
+ }
26
+
27
+ .ratingStar {
28
+ cursor: pointer;
29
+ position: relative;
30
+ display: inline-block;
31
+ box-sizing: border-box;
32
+
33
+ // Инлайновый svg садится на базовую линию и при `xs` съезжает вниз на 1px.
34
+ svg {
35
+ display: block;
36
+ }
37
+
38
+ @each $size in $sizes {
39
+ &[data-size='#{$size}'] {
40
+ $width: base.simple-var(rating.$rating, 'anatomy', 'size', $size, 'container', 'min-width');
41
+ $height: base.simple-var(rating.$rating, 'anatomy', 'size', $size, 'container', 'min-height');
42
+
43
+ @include base.composite-var(rating.$rating, 'anatomy', 'size', $size, 'container');
44
+
45
+ svg {
46
+ width: $width !important;
47
+ height: $height !important;
48
+ }
49
+
50
+ .firstStarContainer,
51
+ .secondStarContainer {
52
+ height: base.simple-var(rating.$rating, 'anatomy', 'size', $size, 'container', 'min-height') !important;
53
+ }
54
+ }
55
+ }
56
+
57
+ // TODO: в макете focus-радиус привязан к `bgSurface-cornerRadius` (ошибка дизайна) —
58
+ // после фикса заменить `dimension-2` на корректный radius-токен.
59
+ &:focus-visible {
60
+ border-radius: base.$sn-primitive-dimension-2;
61
+ outline: base.$sn-primitive-strokeWeight-strokeSemiBold solid base.$sn-theme-color-available-complementary;
62
+ outline-offset: calc(-1 * #{base.$sn-primitive-strokeWeight-strokeSemiBold});
63
+ }
64
+
65
+ @each $appearance in $appearances {
66
+ &[data-appearance='#{$appearance}'] {
67
+ $emptyColor: base.$sn-theme-color-neutral-decor;
68
+ $filledColor: base.simple-var(base.$sn-theme, 'color', $appearance, 'accent');
69
+
70
+ &[data-value='0%'] {
71
+ @include star-color-value($emptyColor, $emptyColor);
72
+ }
73
+
74
+ &[data-value='50%'] {
75
+ @include star-color-value($filledColor, $emptyColor);
76
+ }
77
+
78
+ &[data-value='100%'] {
79
+ @include star-color-value($filledColor, $filledColor);
80
+ }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,5 @@
1
+ import { ValueOf } from '@cloud-ru/ds-utils';
2
+
3
+ import { VALUE } from './constants';
4
+
5
+ export type Value = ValueOf<typeof VALUE>;
@@ -0,0 +1 @@
1
+ export * from './RatingStar';
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './components';
2
+ export * from './constants';
3
+ export * from './types';
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { ValueOf } from '@cloud-ru/ds-utils';
2
+
3
+ import { APPEARANCE, SIZE } from './constants';
4
+
5
+ export type Appearance = ValueOf<typeof APPEARANCE>;
6
+
7
+ export type Size = ValueOf<typeof SIZE>;
package/src/utils.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { HALF_STAR_VALUE } from './constants';
2
+ import { VALUE, Value } from './helperComponents';
3
+
4
+ export function getRatingStarValue(starIndex: number, rating: number, allowHalf: boolean): Value {
5
+ const diff = rating - starIndex;
6
+
7
+ if (diff >= 0) {
8
+ return VALUE.Full;
9
+ }
10
+
11
+ if (allowHalf && diff === HALF_STAR_VALUE * -1) {
12
+ return VALUE.Half;
13
+ }
14
+
15
+ return VALUE.Zero;
16
+ }
17
+
18
+ export function getStarValue(starIndex: number, value: Value, allowHalf: boolean): number {
19
+ return allowHalf && value === VALUE.Half ? starIndex - HALF_STAR_VALUE : starIndex;
20
+ }