@mantine/core
Version:
React components library focused on usability, accessibility and developer experience
1 lines • 13 kB
Source Map (JSON)
{"version":3,"file":"Rating.cjs","names":["createVarsResolver","getSize","getThemeColor","factory","useProps","useStyles","useDirection","RatingItem","RatingProvider","Box","classes"],"sources":["../../../src/components/Rating/Rating.tsx"],"sourcesContent":["import { useRef, useState } from 'react';\nimport { clamp, useId, useMergedRef, useUncontrolled } from '@mantine/hooks';\nimport {\n Box,\n BoxProps,\n createVarsResolver,\n ElementProps,\n factory,\n Factory,\n getSize,\n getThemeColor,\n MantineColor,\n MantineSize,\n StylesApiProps,\n useDirection,\n useProps,\n useStyles,\n} from '../../core';\nimport { RatingProvider } from './Rating.context';\nimport { RatingItem } from './RatingItem/RatingItem';\nimport classes from './Rating.module.css';\n\nfunction roundValueTo(value: number, to: number) {\n const rounded = Math.round(value / to) * to;\n const precision = `${to}`.split('.')[1]?.length || 0;\n return Number(rounded.toFixed(precision));\n}\n\nexport type RatingStylesNames =\n | 'root'\n | 'starSymbol'\n | 'input'\n | 'label'\n | 'symbolBody'\n | 'symbolGroup';\n\nexport type RatingCssVariables = {\n root: '--rating-size' | '--rating-color';\n};\n\nexport interface RatingProps\n extends BoxProps, StylesApiProps<RatingFactory>, ElementProps<'div', 'onChange'> {\n /** Uncontrolled component default value */\n defaultValue?: number;\n\n /** Controlled component value */\n value?: number;\n\n /** Called when value changes */\n onChange?: (value: number) => void;\n\n /** Icon displayed for unselected rating items. Can be a function that receives the rating value. */\n emptySymbol?: React.ReactNode | ((value: number) => React.ReactNode);\n\n /** Icon displayed for selected rating items. Can be a function that receives the rating value. */\n fullSymbol?: React.ReactNode | ((value: number) => React.ReactNode);\n\n /** Number of fractions each item can be divided into, default is 1 */\n fractions?: number;\n\n /** Controls component size @default 'sm' */\n size?: MantineSize | number | (string & {});\n\n /** Number of rating items (stars), default is 5 */\n count?: number;\n\n /** Called when rating item is hovered. Receives -1 when hover ends. */\n onHover?: (value: number) => void;\n\n /** Function to generate aria-label for each rating value. Receives the rating value as argument, default is (value) => String(value) */\n getSymbolLabel?: (index: number) => string;\n\n /** Name attribute for form submission. If not provided, a unique id will be generated. */\n name?: string;\n\n /** When true, rating cannot be changed by user interaction, default is false */\n readOnly?: boolean;\n\n /** When true, clicking the same rating value clears the rating to 0, default is false */\n allowClear?: boolean;\n\n /** When true, only the clicked rating item is highlighted, not all items up to the selected value, default is false */\n highlightSelectedOnly?: boolean;\n\n /** Key of theme.colors or any CSS color value, default is 'yellow' */\n color?: MantineColor;\n}\n\nexport type RatingFactory = Factory<{\n props: RatingProps;\n ref: HTMLDivElement;\n stylesNames: RatingStylesNames;\n vars: RatingCssVariables;\n}>;\n\nconst defaultProps = {\n size: 'sm',\n getSymbolLabel: (value) => `${value}`,\n count: 5,\n fractions: 1,\n color: 'yellow',\n} satisfies Partial<RatingProps>;\n\nconst varsResolver = createVarsResolver<RatingFactory>((theme, { size, color }) => ({\n root: {\n '--rating-size': getSize(size, 'rating-size'),\n '--rating-color': getThemeColor(color, theme),\n },\n}));\n\nexport const Rating = factory<RatingFactory>((_props) => {\n const props = useProps('Rating', defaultProps, _props);\n const {\n classNames,\n className,\n style,\n styles,\n unstyled,\n vars,\n name,\n id,\n value,\n defaultValue,\n onChange,\n fractions,\n count,\n onMouseEnter,\n readOnly,\n allowClear,\n onMouseMove,\n onHover,\n onMouseLeave,\n onTouchStart,\n onTouchEnd,\n size,\n variant,\n getSymbolLabel,\n color,\n emptySymbol,\n fullSymbol,\n highlightSelectedOnly,\n attributes,\n ref,\n ...others\n } = props;\n\n const getStyles = useStyles<RatingFactory>({\n name: 'Rating',\n classes,\n props,\n className,\n style,\n classNames,\n styles,\n unstyled,\n attributes,\n vars,\n varsResolver,\n });\n\n const { dir } = useDirection();\n\n const _name = useId(name);\n const _id = useId(id);\n const rootRef = useRef<HTMLDivElement>(null);\n\n const [_value, setValue] = useUncontrolled({\n value,\n defaultValue,\n finalValue: 0,\n onChange,\n });\n\n const [hovered, setHovered] = useState(-1);\n const [isOutside, setOutside] = useState(true);\n\n const _fractions = Math.floor(fractions);\n const _count = Math.floor(count);\n\n const decimalUnit = 1 / _fractions;\n const stableValueRounded = roundValueTo(_value, decimalUnit);\n const finalValue = hovered !== -1 ? hovered : stableValueRounded;\n\n const getRatingFromCoordinates = (x: number) => {\n if (!rootRef.current) {\n return 0;\n }\n\n const { left, right, width } = rootRef.current.getBoundingClientRect();\n const symbolWidth = width / _count;\n\n const hoverPosition = dir === 'rtl' ? right - x : x - left;\n const hoverValue = hoverPosition / symbolWidth;\n\n return clamp(roundValueTo(hoverValue + decimalUnit / 2, decimalUnit), decimalUnit, _count);\n };\n\n const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {\n onMouseEnter?.(event);\n !readOnly && setOutside(false);\n };\n\n const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {\n onMouseMove?.(event);\n\n if (readOnly) {\n return;\n }\n\n const rounded = getRatingFromCoordinates(event.clientX);\n\n setHovered(rounded);\n rounded !== hovered && onHover?.(rounded);\n };\n\n const handleMouseLeave = (event: React.MouseEvent<HTMLDivElement>) => {\n onMouseLeave?.(event);\n\n if (readOnly) {\n return;\n }\n\n setHovered(-1);\n setOutside(true);\n hovered !== -1 && onHover?.(-1);\n };\n\n const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {\n const { touches } = event;\n if (touches.length !== 1) {\n return;\n }\n\n if (!readOnly) {\n const touch = touches[0];\n setValue(getRatingFromCoordinates(touch.clientX));\n }\n\n onTouchStart?.(event);\n };\n\n const handleTouchEnd = (event: React.TouchEvent<HTMLDivElement>) => {\n event.preventDefault();\n\n onTouchEnd?.(event);\n };\n\n const handleItemBlur = () => isOutside && setHovered(-1);\n\n const handleInputChange = (event: React.ChangeEvent<HTMLInputElement> | number) => {\n if (!readOnly) {\n if (typeof event === 'number') {\n setHovered(event);\n } else {\n setHovered(parseFloat(event.target.value));\n }\n }\n };\n\n const handleChange = (event: React.ChangeEvent<HTMLInputElement> | number) => {\n if (!readOnly) {\n const newValue = typeof event === 'number' ? event : parseFloat(event.target.value);\n\n // If allowClear is true and clicking the same value, reset to 0\n if (allowClear && newValue === stableValueRounded) {\n setValue(0);\n } else {\n setValue(newValue);\n }\n }\n };\n\n const items = Array(_count)\n .fill(0)\n .map((_, index) => {\n const integerValue = index + 1;\n const fractionItems = Array.from(new Array(index === 0 ? _fractions + 1 : _fractions));\n const isGroupActive = !readOnly && Math.ceil(hovered) === integerValue;\n\n return (\n <div\n key={integerValue}\n data-active={isGroupActive || undefined}\n {...getStyles('symbolGroup')}\n >\n {fractionItems.map((__, fractionIndex) => {\n const fractionValue = decimalUnit * (index === 0 ? fractionIndex : fractionIndex + 1);\n const symbolValue = roundValueTo(integerValue - 1 + fractionValue, decimalUnit);\n\n return (\n <RatingItem\n key={`${integerValue}-${symbolValue}`}\n getSymbolLabel={getSymbolLabel}\n emptyIcon={emptySymbol}\n fullIcon={fullSymbol}\n full={\n highlightSelectedOnly ? symbolValue === finalValue : symbolValue <= finalValue\n }\n active={symbolValue === finalValue}\n checked={symbolValue === stableValueRounded}\n readOnly={readOnly}\n fractionValue={fractionValue}\n value={symbolValue}\n name={_name}\n onChange={handleChange}\n onBlur={handleItemBlur}\n onInputChange={handleInputChange}\n id={`${_id}-${index}-${fractionIndex}`}\n />\n );\n })}\n </div>\n );\n });\n\n return (\n <RatingProvider value={{ getStyles }}>\n <Box\n ref={useMergedRef(rootRef, ref)}\n {...getStyles('root')}\n onMouseMove={handleMouseMove}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onTouchStart={handleTouchStart}\n onTouchEnd={handleTouchEnd}\n variant={variant}\n size={size}\n id={_id}\n {...others}\n >\n {items}\n </Box>\n </RatingProvider>\n );\n});\n\nRating.classes = classes;\nRating.varsResolver = varsResolver;\nRating.displayName = '@mantine/core/Rating';\n"],"mappings":"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"}