UNPKG

react-ratings-star

Version:

A fully customizable, interactive, and accessible rating component for React.

270 lines (255 loc) 12.1 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } import PropTypes from "prop-types"; import React, { useRef, useState, useId } from "react"; // The primary Rating component, packed with useful features. var Rating = function Rating(_ref) { var _ref2, _Number; var _ref$value = _ref.value, value = _ref$value === void 0 ? 0 : _ref$value, _ref$max = _ref.max, max = _ref$max === void 0 ? 5 : _ref$max, _ref$onRatingChange = _ref.onRatingChange, onRatingChange = _ref$onRatingChange === void 0 ? function () {} : _ref$onRatingChange, _ref$readOnly = _ref.readOnly, readOnly = _ref$readOnly === void 0 ? false : _ref$readOnly, _ref$size = _ref.size, size = _ref$size === void 0 ? 24 : _ref$size, _ref$fullColor = _ref.fullColor, fullColor = _ref$fullColor === void 0 ? "#FFD700" : _ref$fullColor, _ref$emptyColor = _ref.emptyColor, emptyColor = _ref$emptyColor === void 0 ? "#E0E0E0" : _ref$emptyColor, _ref$customIcon = _ref.customIcon, customIcon = _ref$customIcon === void 0 ? null : _ref$customIcon, _ref$tooltips = _ref.tooltips, tooltips = _ref$tooltips === void 0 ? [] : _ref$tooltips, _ref$className = _ref.className, className = _ref$className === void 0 ? "" : _ref$className, _ref$style = _ref.style, style = _ref$style === void 0 ? {} : _ref$style, _ref$rounding = _ref.rounding, rounding = _ref$rounding === void 0 ? "ceil" : _ref$rounding; var _useState = useState(null), _useState2 = _slicedToArray(_useState, 2), hoverValue = _useState2[0], setHoverValue = _useState2[1]; var _useState3 = useState(false), _useState4 = _slicedToArray(_useState3, 2), isTouching = _useState4[0], setIsTouching = _useState4[1]; var ratingContainerRef = useRef(null); var uniqueId = useId(); // Calculate rating based on mouse/touch position var calculateRatingFromClientX = function calculateRatingFromClientX(clientX) { if (!ratingContainerRef.current) return 0; var _ratingContainerRef$c = ratingContainerRef.current.getBoundingClientRect(), width = _ratingContainerRef$c.width, left = _ratingContainerRef$c.left; var percent = (clientX - left) / width; percent = Math.max(0, Math.min(1, percent)); var raw = percent * max; var rating; if (rounding === "nearest") { rating = Math.round(raw * 2) / 2; } else if (rounding === "floor") { rating = Math.floor(raw * 2) / 2; } else { rating = Math.ceil(raw * 2) / 2; } return Math.max(0, Math.min(max, +rating.toFixed(2))); }; // --- Event handlers --- //* Mouse handlers for the smooth hover effect var handleMouseMove = function handleMouseMove(e) { if (readOnly || isTouching) return; setHoverValue(calculateRatingFromClientX(e.clientX)); }; var handleMouseLeave = function handleMouseLeave() { if (readOnly || isTouching) return; setHoverValue(null); }; var handleClick = function handleClick(e) { if (readOnly) return; e.stopPropagation(); onRatingChange(calculateRatingFromClientX(e.clientX)); }; //* Handlers for a smooth mobile touch experience. var handleTouchStart = function handleTouchStart(e) { var _e$touches; if (readOnly) return; setIsTouching(true); if ((_e$touches = e.touches) !== null && _e$touches !== void 0 && _e$touches[0]) { setHoverValue(calculateRatingFromClientX(e.touches[0].clientX)); } }; var handleTouchMove = function handleTouchMove(e) { var _e$touches2; if (readOnly) return; if ((_e$touches2 = e.touches) !== null && _e$touches2 !== void 0 && _e$touches2[0]) { setHoverValue(calculateRatingFromClientX(e.touches[0].clientX)); } }; var handleTouchEnd = function handleTouchEnd(e) { if (readOnly) return; setIsTouching(false); if (hoverValue !== null) onRatingChange(hoverValue); setHoverValue(null); }; //* Handlers for keyboard navigation for accessibility. var handleKeyDown = function handleKeyDown(e) { if (readOnly) return; var newValue = value; var step = 0.5; switch (e.key) { case "ArrowRight": case "ArrowUp": newValue = Math.min(max, +(value + step).toFixed(2)); break; case "ArrowLeft": case "ArrowDown": newValue = Math.max(0, +(value - step).toFixed(2)); break; case "Home": newValue = 0; break; case "End": newValue = max; break; case "PageUp": newValue = Math.min(max, +(value + 1).toFixed(2)); break; case "PageDown": newValue = Math.max(0, +(value - 1).toFixed(2)); break; case "Enter": case " ": break; default: return; } e.preventDefault(); if (newValue !== value) onRatingChange(newValue); }; //* --- Value & Tooltip Calculation --- var displayValue = (_ref2 = hoverValue !== null && hoverValue !== void 0 ? hoverValue : Number(value)) !== null && _ref2 !== void 0 ? _ref2 : 0; var formattedValue = displayValue % 1 === 0 ? displayValue : displayValue.toFixed(1); var numericTooltip = "".concat(formattedValue, " out of ").concat(max); var descriptiveIndex = Math.ceil(displayValue) - 1; var descriptiveText = tooltips[descriptiveIndex]; var finalTooltip = descriptiveText ? "".concat(formattedValue, " - ").concat(descriptiveText) : numericTooltip; // For screen readers var ariaValueText = finalTooltip; // Default star path (used if no custom icon is provided) var DefaultStar = function DefaultStar() { return /*#__PURE__*/React.createElement("path", { d: "M12 .587l3.668 7.429 8.207 1.192-5.938 5.787 1.401 8.17L12 18.897l-7.338 3.856 1.401-8.17L.125 9.208l8.207-1.192L12 .587z" }); }; var IconComponent = customIcon; return /*#__PURE__*/React.createElement("div", { ref: ratingContainerRef, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onKeyDown: handleKeyDown, role: "slider", "aria-valuenow": (_Number = Number(value)) !== null && _Number !== void 0 ? _Number : 0, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuetext": ariaValueText, "aria-label": "Rating", "aria-readonly": readOnly, tabIndex: readOnly ? -1 : 0, style: _objectSpread({ display: "inline-flex", alignItems: "center", cursor: readOnly ? "default" : "pointer", userSelect: "none" }, style), className: className, title: finalTooltip }, Array.from({ length: max }, function (_, i) { var iconValue = i + 1; var effectiveDisplayValue = Math.min(displayValue, max); var fillPercentage; if (effectiveDisplayValue >= iconValue) { fillPercentage = 100; } else if (effectiveDisplayValue > i) { fillPercentage = (effectiveDisplayValue - i) * 100; } else { fillPercentage = 0; } // FIX: Use the stable uniqueId to create flicker-free SVG IDs. var gradientId = "grad-".concat(uniqueId, "-").concat(iconValue); var maskId = "mask-".concat(uniqueId, "-").concat(iconValue); return /*#__PURE__*/React.createElement("div", { key: iconValue, style: { width: size, height: size }, "aria-hidden": "true" }, /*#__PURE__*/React.createElement("svg", { height: size, width: size, viewBox: "0 0 24 24" }, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("linearGradient", { id: gradientId }, /*#__PURE__*/React.createElement("stop", { offset: "0%", stopColor: fullColor }), /*#__PURE__*/React.createElement("stop", { offset: "".concat(fillPercentage, "%"), stopColor: fullColor }), /*#__PURE__*/React.createElement("stop", { offset: "".concat(fillPercentage, "%"), stopColor: emptyColor }), /*#__PURE__*/React.createElement("stop", { offset: "100%", stopColor: emptyColor })), /*#__PURE__*/React.createElement("mask", { id: maskId }, /*#__PURE__*/React.createElement("g", { fill: "white" }, IconComponent ? /*#__PURE__*/React.createElement(IconComponent, { size: size }) : /*#__PURE__*/React.createElement(DefaultStar, null)))), /*#__PURE__*/React.createElement("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "url(#".concat(gradientId, ")"), mask: "url(#".concat(maskId, ")") }))); })); }; Rating.propTypes = { value: PropTypes.number.isRequired, max: PropTypes.number, onRatingChange: PropTypes.func, readOnly: PropTypes.bool, size: PropTypes.number, fullColor: PropTypes.string, emptyColor: PropTypes.string, customIcon: PropTypes.elementType, tooltips: PropTypes.arrayOf(PropTypes.string), className: PropTypes.string, style: PropTypes.object, rounding: PropTypes.oneOf(["ceil", "floor", "nearest"]) }; export default Rating;