@spaced-out/ui-design-system
Version:
Sense UI components library
135 lines (123 loc) • 4.21 kB
Flow
// @flow strict
import * as React from 'react';
import invariant from 'invariant';
import {isArray, isUndefined} from 'lodash';
import type {ColorTypes} from '../../types/typography';
import {classify} from '../../utils/classify';
import {getRatingLabel, RATING_ERRORS, RATINGS} from '../../utils/rating';
import {UnstyledButton} from '../Button';
import type {ChipSemanticType} from '../Chip';
import {Chip, CHIP_SEMANTIC} from '../Chip';
import {ConditionalWrapper} from '../ConditionalWrapper';
import {Icon} from '../Icon';
import {TEXT_COLORS} from '../Text';
import css from './Rating.module.css';
export const RATING_SIZE = Object.freeze({
small: 'small',
medium: 'medium',
});
type ClassNames = $ReadOnly<{wrapper?: string, icon?: string, button?: string}>;
type RatingSize = $Values<typeof RATING_SIZE>;
export type RatingProps = {
size?: RatingSize,
start?: number,
end?: number,
quiet?: boolean,
rating?: number,
labels?: Array<string>,
iconColor?: ColorTypes,
onChange?: (rating: number, ?SyntheticEvent<HTMLElement>) => mixed,
readOnly?: boolean,
hideLabel?: boolean,
iconName?: string,
classNames?: ClassNames,
chipSemantic?: ChipSemanticType,
};
export const Rating: React$AbstractComponent<RatingProps, HTMLDivElement> =
React.forwardRef<RatingProps, HTMLDivElement>(
(
{
size = 'medium',
start = 0,
end = 5,
quiet,
rating = 0,
labels = RATINGS,
onChange,
readOnly,
hideLabel,
iconName = 'star',
iconColor = TEXT_COLORS.favorite,
chipSemantic = CHIP_SEMANTIC.warning,
classNames,
}: RatingProps,
ref,
): React.Node => {
const [hoverIcon, setHoverIcon] = React.useState<number>(-1);
const chipText = getRatingLabel(rating - start, labels);
const totalIcons = end - start;
invariant(totalIcons, JSON.stringify(RATING_ERRORS.INVALID_RANGE));
invariant(
isUndefined(labels) || (isArray(labels) && labels.length),
JSON.stringify(RATING_ERRORS.INVALID_RATING_LABELS),
);
return (
<div
data-testid="Rating"
ref={ref}
className={classify(css.ratingContainer, classNames?.wrapper)}
onBlur={() => !quiet && setHoverIcon(-1)}
>
<div className={css.stars}>
{Array.from({length: totalIcons}).map((_, index) => {
const isHovering = hoverIcon > -1;
const iconCount = index + 1;
const iconType = (
isHovering
? iconCount <= hoverIcon
: iconCount <= rating - start
)
? 'solid'
: 'regular';
return (
<ConditionalWrapper
key={iconCount}
condition={!readOnly}
wrapper={(children) => (
<UnstyledButton
onKeyDown={(e) => {
if (e.key === 'Enter') {
onChange?.(start + iconCount, e);
}
}}
onFocus={() => !quiet && setHoverIcon(iconCount)}
className={classify(css.button, classNames?.button)}
onClick={(e) => onChange?.(start + iconCount, e)}
onMouseEnter={() => !quiet && setHoverIcon(iconCount)}
onMouseLeave={() => !quiet && setHoverIcon(-1)}
>
{children}
</UnstyledButton>
)}
>
<Icon
size={size}
name={iconName}
type={iconType}
color={iconColor}
className={classify(css.icon, classNames?.icon)}
/>
</ConditionalWrapper>
);
})}
</div>
{!hideLabel && (
// $FlowFixMe[incompatible-type]
<Chip size={size} semantic={chipSemantic}>
{chipText}
</Chip>
)}
</div>
);
},
);