UNPKG

@spaced-out/ui-design-system

Version:
135 lines (123 loc) 4.21 kB
// @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> ); }, );