@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +201 -0
- package/README.md +88 -0
- package/dist/cjs/components/Rating/Rating.d.ts +28 -0
- package/dist/cjs/components/Rating/Rating.js +53 -0
- package/dist/cjs/components/Rating/index.d.ts +1 -0
- package/dist/cjs/components/Rating/index.js +17 -0
- package/dist/cjs/components/Rating/styles.module.css +4 -0
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.js +17 -0
- package/dist/cjs/constants.d.ts +24 -0
- package/dist/cjs/constants.js +27 -0
- package/dist/cjs/helperComponents/RatingStar/RatingStar.d.ts +29 -0
- package/dist/cjs/helperComponents/RatingStar/RatingStar.js +18 -0
- package/dist/cjs/helperComponents/RatingStar/constants.d.ts +5 -0
- package/dist/cjs/helperComponents/RatingStar/constants.js +8 -0
- package/dist/cjs/helperComponents/RatingStar/index.d.ts +3 -0
- package/dist/cjs/helperComponents/RatingStar/index.js +20 -0
- package/dist/cjs/helperComponents/RatingStar/styles.module.css +210 -0
- package/dist/cjs/helperComponents/RatingStar/types.d.ts +3 -0
- package/dist/cjs/helperComponents/RatingStar/types.js +2 -0
- package/dist/cjs/helperComponents/index.d.ts +1 -0
- package/dist/cjs/helperComponents/index.js +17 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/style.css +214 -0
- package/dist/cjs/types.d.ts +4 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/utils.d.ts +3 -0
- package/dist/cjs/utils.js +19 -0
- package/dist/esm/components/Rating/Rating.d.ts +28 -0
- package/dist/esm/components/Rating/Rating.js +47 -0
- package/dist/esm/components/Rating/index.d.ts +1 -0
- package/dist/esm/components/Rating/index.js +1 -0
- package/dist/esm/components/Rating/styles.module.css +4 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/constants.d.ts +24 -0
- package/dist/esm/constants.js +24 -0
- package/dist/esm/helperComponents/RatingStar/RatingStar.d.ts +29 -0
- package/dist/esm/helperComponents/RatingStar/RatingStar.js +12 -0
- package/dist/esm/helperComponents/RatingStar/constants.d.ts +5 -0
- package/dist/esm/helperComponents/RatingStar/constants.js +5 -0
- package/dist/esm/helperComponents/RatingStar/index.d.ts +3 -0
- package/dist/esm/helperComponents/RatingStar/index.js +2 -0
- package/dist/esm/helperComponents/RatingStar/styles.module.css +210 -0
- package/dist/esm/helperComponents/RatingStar/types.d.ts +3 -0
- package/dist/esm/helperComponents/RatingStar/types.js +1 -0
- package/dist/esm/helperComponents/index.d.ts +1 -0
- package/dist/esm/helperComponents/index.js +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/style.css +214 -0
- package/dist/esm/types.d.ts +4 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +15 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +43 -0
- package/src/components/Rating/Rating.tsx +110 -0
- package/src/components/Rating/index.ts +1 -0
- package/src/components/Rating/styles.module.scss +4 -0
- package/src/components/index.ts +1 -0
- package/src/constants.ts +27 -0
- package/src/helperComponents/RatingStar/RatingStar.tsx +86 -0
- package/src/helperComponents/RatingStar/constants.ts +5 -0
- package/src/helperComponents/RatingStar/index.ts +3 -0
- package/src/helperComponents/RatingStar/styles.module.scss +83 -0
- package/src/helperComponents/RatingStar/types.ts +5 -0
- package/src/helperComponents/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/types.ts +7 -0
- 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 @@
|
|
|
1
|
+
export * from './Rating';
|
package/src/constants.ts
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
export * from './RatingStar';
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
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
|
+
}
|