1 | 'use client';
|
2 |
|
3 | import * as React from 'react';
|
4 | import PropTypes from 'prop-types';
|
5 | import clsx from 'clsx';
|
6 | import clamp from '@mui/utils/clamp';
|
7 | import visuallyHidden from '@mui/utils/visuallyHidden';
|
8 | import chainPropTypes from '@mui/utils/chainPropTypes';
|
9 | import composeClasses from '@mui/utils/composeClasses';
|
10 | import { useRtl } from '@mui/system/RtlProvider';
|
11 | import isFocusVisible from '@mui/utils/isFocusVisible';
|
12 | import { capitalize, useForkRef, useControlled, unstable_useId as useId } from "../utils/index.js";
|
13 | import Star from "../internal/svg-icons/Star.js";
|
14 | import StarBorder from "../internal/svg-icons/StarBorder.js";
|
15 | import { styled } from "../zero-styled/index.js";
|
16 | import memoTheme from "../utils/memoTheme.js";
|
17 | import { useDefaultProps } from "../DefaultPropsProvider/index.js";
|
18 | import slotShouldForwardProp from "../styles/slotShouldForwardProp.js";
|
19 | import ratingClasses, { getRatingUtilityClass } from "./ratingClasses.js";
|
20 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
21 | function getDecimalPrecision(num) {
|
22 | const decimalPart = num.toString().split('.')[1];
|
23 | return decimalPart ? decimalPart.length : 0;
|
24 | }
|
25 | function roundValueToPrecision(value, precision) {
|
26 | if (value == null) {
|
27 | return value;
|
28 | }
|
29 | const nearest = Math.round(value / precision) * precision;
|
30 | return Number(nearest.toFixed(getDecimalPrecision(precision)));
|
31 | }
|
32 | const useUtilityClasses = ownerState => {
|
33 | const {
|
34 | classes,
|
35 | size,
|
36 | readOnly,
|
37 | disabled,
|
38 | emptyValueFocused,
|
39 | focusVisible
|
40 | } = ownerState;
|
41 | const slots = {
|
42 | root: ['root', `size${capitalize(size)}`, disabled && 'disabled', focusVisible && 'focusVisible', readOnly && 'readOnly'],
|
43 | label: ['label', 'pristine'],
|
44 | labelEmptyValue: [emptyValueFocused && 'labelEmptyValueActive'],
|
45 | icon: ['icon'],
|
46 | iconEmpty: ['iconEmpty'],
|
47 | iconFilled: ['iconFilled'],
|
48 | iconHover: ['iconHover'],
|
49 | iconFocus: ['iconFocus'],
|
50 | iconActive: ['iconActive'],
|
51 | decimal: ['decimal'],
|
52 | visuallyHidden: ['visuallyHidden']
|
53 | };
|
54 | return composeClasses(slots, getRatingUtilityClass, classes);
|
55 | };
|
56 | const RatingRoot = styled('span', {
|
57 | name: 'MuiRating',
|
58 | slot: 'Root',
|
59 | overridesResolver: (props, styles) => {
|
60 | const {
|
61 | ownerState
|
62 | } = props;
|
63 | return [{
|
64 | [`& .${ratingClasses.visuallyHidden}`]: styles.visuallyHidden
|
65 | }, styles.root, styles[`size${capitalize(ownerState.size)}`], ownerState.readOnly && styles.readOnly];
|
66 | }
|
67 | })(memoTheme(({
|
68 | theme
|
69 | }) => ({
|
70 | display: 'inline-flex',
|
71 |
|
72 | position: 'relative',
|
73 | fontSize: theme.typography.pxToRem(24),
|
74 | color: '#faaf00',
|
75 | cursor: 'pointer',
|
76 | textAlign: 'left',
|
77 | width: 'min-content',
|
78 | WebkitTapHighlightColor: 'transparent',
|
79 | [`&.${ratingClasses.disabled}`]: {
|
80 | opacity: (theme.vars || theme).palette.action.disabledOpacity,
|
81 | pointerEvents: 'none'
|
82 | },
|
83 | [`&.${ratingClasses.focusVisible} .${ratingClasses.iconActive}`]: {
|
84 | outline: '1px solid #999'
|
85 | },
|
86 | [`& .${ratingClasses.visuallyHidden}`]: visuallyHidden,
|
87 | variants: [{
|
88 | props: {
|
89 | size: 'small'
|
90 | },
|
91 | style: {
|
92 | fontSize: theme.typography.pxToRem(18)
|
93 | }
|
94 | }, {
|
95 | props: {
|
96 | size: 'large'
|
97 | },
|
98 | style: {
|
99 | fontSize: theme.typography.pxToRem(30)
|
100 | }
|
101 | }, {
|
102 |
|
103 | props: ({
|
104 | ownerState
|
105 | }) => ownerState.readOnly,
|
106 | style: {
|
107 | pointerEvents: 'none'
|
108 | }
|
109 | }]
|
110 | })));
|
111 | const RatingLabel = styled('label', {
|
112 | name: 'MuiRating',
|
113 | slot: 'Label',
|
114 | overridesResolver: ({
|
115 | ownerState
|
116 | }, styles) => [styles.label, ownerState.emptyValueFocused && styles.labelEmptyValueActive]
|
117 | })({
|
118 | cursor: 'inherit',
|
119 | variants: [{
|
120 | props: ({
|
121 | ownerState
|
122 | }) => ownerState.emptyValueFocused,
|
123 | style: {
|
124 | top: 0,
|
125 | bottom: 0,
|
126 | position: 'absolute',
|
127 | outline: '1px solid #999',
|
128 | width: '100%'
|
129 | }
|
130 | }]
|
131 | });
|
132 | const RatingIcon = styled('span', {
|
133 | name: 'MuiRating',
|
134 | slot: 'Icon',
|
135 | overridesResolver: (props, styles) => {
|
136 | const {
|
137 | ownerState
|
138 | } = props;
|
139 | return [styles.icon, ownerState.iconEmpty && styles.iconEmpty, ownerState.iconFilled && styles.iconFilled, ownerState.iconHover && styles.iconHover, ownerState.iconFocus && styles.iconFocus, ownerState.iconActive && styles.iconActive];
|
140 | }
|
141 | })(memoTheme(({
|
142 | theme
|
143 | }) => ({
|
144 |
|
145 | display: 'flex',
|
146 | transition: theme.transitions.create('transform', {
|
147 | duration: theme.transitions.duration.shortest
|
148 | }),
|
149 |
|
150 |
|
151 | pointerEvents: 'none',
|
152 | variants: [{
|
153 | props: ({
|
154 | ownerState
|
155 | }) => ownerState.iconActive,
|
156 | style: {
|
157 | transform: 'scale(1.2)'
|
158 | }
|
159 | }, {
|
160 | props: ({
|
161 | ownerState
|
162 | }) => ownerState.iconEmpty,
|
163 | style: {
|
164 | color: (theme.vars || theme).palette.action.disabled
|
165 | }
|
166 | }]
|
167 | })));
|
168 | const RatingDecimal = styled('span', {
|
169 | name: 'MuiRating',
|
170 | slot: 'Decimal',
|
171 | shouldForwardProp: prop => slotShouldForwardProp(prop) && prop !== 'iconActive',
|
172 | overridesResolver: (props, styles) => {
|
173 | const {
|
174 | iconActive
|
175 | } = props;
|
176 | return [styles.decimal, iconActive && styles.iconActive];
|
177 | }
|
178 | })({
|
179 | position: 'relative',
|
180 | variants: [{
|
181 | props: ({
|
182 | iconActive
|
183 | }) => iconActive,
|
184 | style: {
|
185 | transform: 'scale(1.2)'
|
186 | }
|
187 | }]
|
188 | });
|
189 | function IconContainer(props) {
|
190 | const {
|
191 | value,
|
192 | ...other
|
193 | } = props;
|
194 | return _jsx("span", {
|
195 | ...other
|
196 | });
|
197 | }
|
198 | process.env.NODE_ENV !== "production" ? IconContainer.propTypes = {
|
199 | value: PropTypes.number.isRequired
|
200 | } : void 0;
|
201 | function RatingItem(props) {
|
202 | const {
|
203 | classes,
|
204 | disabled,
|
205 | emptyIcon,
|
206 | focus,
|
207 | getLabelText,
|
208 | highlightSelectedOnly,
|
209 | hover,
|
210 | icon,
|
211 | IconContainerComponent,
|
212 | isActive,
|
213 | itemValue,
|
214 | labelProps,
|
215 | name,
|
216 | onBlur,
|
217 | onChange,
|
218 | onClick,
|
219 | onFocus,
|
220 | readOnly,
|
221 | ownerState,
|
222 | ratingValue,
|
223 | ratingValueRounded
|
224 | } = props;
|
225 | const isFilled = highlightSelectedOnly ? itemValue === ratingValue : itemValue <= ratingValue;
|
226 | const isHovered = itemValue <= hover;
|
227 | const isFocused = itemValue <= focus;
|
228 | const isChecked = itemValue === ratingValueRounded;
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 | const id = `${name}-${useId()}`;
|
235 | const container = _jsx(RatingIcon, {
|
236 | as: IconContainerComponent,
|
237 | value: itemValue,
|
238 | className: clsx(classes.icon, isFilled ? classes.iconFilled : classes.iconEmpty, isHovered && classes.iconHover, isFocused && classes.iconFocus, isActive && classes.iconActive),
|
239 | ownerState: {
|
240 | ...ownerState,
|
241 | iconEmpty: !isFilled,
|
242 | iconFilled: isFilled,
|
243 | iconHover: isHovered,
|
244 | iconFocus: isFocused,
|
245 | iconActive: isActive
|
246 | },
|
247 | children: emptyIcon && !isFilled ? emptyIcon : icon
|
248 | });
|
249 | if (readOnly) {
|
250 | return _jsx("span", {
|
251 | ...labelProps,
|
252 | children: container
|
253 | });
|
254 | }
|
255 | return _jsxs(React.Fragment, {
|
256 | children: [_jsxs(RatingLabel, {
|
257 | ownerState: {
|
258 | ...ownerState,
|
259 | emptyValueFocused: undefined
|
260 | },
|
261 | htmlFor: id,
|
262 | ...labelProps,
|
263 | children: [container, _jsx("span", {
|
264 | className: classes.visuallyHidden,
|
265 | children: getLabelText(itemValue)
|
266 | })]
|
267 | }), _jsx("input", {
|
268 | className: classes.visuallyHidden,
|
269 | onFocus: onFocus,
|
270 | onBlur: onBlur,
|
271 | onChange: onChange,
|
272 | onClick: onClick,
|
273 | disabled: disabled,
|
274 | value: itemValue,
|
275 | id: id,
|
276 | type: "radio",
|
277 | name: name,
|
278 | checked: isChecked
|
279 | })]
|
280 | });
|
281 | }
|
282 | process.env.NODE_ENV !== "production" ? RatingItem.propTypes = {
|
283 | classes: PropTypes.object.isRequired,
|
284 | disabled: PropTypes.bool.isRequired,
|
285 | emptyIcon: PropTypes.node,
|
286 | focus: PropTypes.number.isRequired,
|
287 | getLabelText: PropTypes.func.isRequired,
|
288 | highlightSelectedOnly: PropTypes.bool.isRequired,
|
289 | hover: PropTypes.number.isRequired,
|
290 | icon: PropTypes.node,
|
291 | IconContainerComponent: PropTypes.elementType.isRequired,
|
292 | isActive: PropTypes.bool.isRequired,
|
293 | itemValue: PropTypes.number.isRequired,
|
294 | labelProps: PropTypes.object,
|
295 | name: PropTypes.string,
|
296 | onBlur: PropTypes.func.isRequired,
|
297 | onChange: PropTypes.func.isRequired,
|
298 | onClick: PropTypes.func.isRequired,
|
299 | onFocus: PropTypes.func.isRequired,
|
300 | ownerState: PropTypes.object.isRequired,
|
301 | ratingValue: PropTypes.number,
|
302 | ratingValueRounded: PropTypes.number,
|
303 | readOnly: PropTypes.bool.isRequired
|
304 | } : void 0;
|
305 | const defaultIcon = _jsx(Star, {
|
306 | fontSize: "inherit"
|
307 | });
|
308 | const defaultEmptyIcon = _jsx(StarBorder, {
|
309 | fontSize: "inherit"
|
310 | });
|
311 | function defaultLabelText(value) {
|
312 | return `${value || '0'} Star${value !== 1 ? 's' : ''}`;
|
313 | }
|
314 | const Rating = React.forwardRef(function Rating(inProps, ref) {
|
315 | const props = useDefaultProps({
|
316 | name: 'MuiRating',
|
317 | props: inProps
|
318 | });
|
319 | const {
|
320 | className,
|
321 | defaultValue = null,
|
322 | disabled = false,
|
323 | emptyIcon = defaultEmptyIcon,
|
324 | emptyLabelText = 'Empty',
|
325 | getLabelText = defaultLabelText,
|
326 | highlightSelectedOnly = false,
|
327 | icon = defaultIcon,
|
328 | IconContainerComponent = IconContainer,
|
329 | max = 5,
|
330 | name: nameProp,
|
331 | onChange,
|
332 | onChangeActive,
|
333 | onMouseLeave,
|
334 | onMouseMove,
|
335 | precision = 1,
|
336 | readOnly = false,
|
337 | size = 'medium',
|
338 | value: valueProp,
|
339 | ...other
|
340 | } = props;
|
341 | const name = useId(nameProp);
|
342 | const [valueDerived, setValueState] = useControlled({
|
343 | controlled: valueProp,
|
344 | default: defaultValue,
|
345 | name: 'Rating'
|
346 | });
|
347 | const valueRounded = roundValueToPrecision(valueDerived, precision);
|
348 | const isRtl = useRtl();
|
349 | const [{
|
350 | hover,
|
351 | focus
|
352 | }, setState] = React.useState({
|
353 | hover: -1,
|
354 | focus: -1
|
355 | });
|
356 | let value = valueRounded;
|
357 | if (hover !== -1) {
|
358 | value = hover;
|
359 | }
|
360 | if (focus !== -1) {
|
361 | value = focus;
|
362 | }
|
363 | const [focusVisible, setFocusVisible] = React.useState(false);
|
364 | const rootRef = React.useRef();
|
365 | const handleRef = useForkRef(rootRef, ref);
|
366 | const handleMouseMove = event => {
|
367 | if (onMouseMove) {
|
368 | onMouseMove(event);
|
369 | }
|
370 | const rootNode = rootRef.current;
|
371 | const {
|
372 | right,
|
373 | left,
|
374 | width: containerWidth
|
375 | } = rootNode.getBoundingClientRect();
|
376 | let percent;
|
377 | if (isRtl) {
|
378 | percent = (right - event.clientX) / containerWidth;
|
379 | } else {
|
380 | percent = (event.clientX - left) / containerWidth;
|
381 | }
|
382 | let newHover = roundValueToPrecision(max * percent + precision / 2, precision);
|
383 | newHover = clamp(newHover, precision, max);
|
384 | setState(prev => prev.hover === newHover && prev.focus === newHover ? prev : {
|
385 | hover: newHover,
|
386 | focus: newHover
|
387 | });
|
388 | setFocusVisible(false);
|
389 | if (onChangeActive && hover !== newHover) {
|
390 | onChangeActive(event, newHover);
|
391 | }
|
392 | };
|
393 | const handleMouseLeave = event => {
|
394 | if (onMouseLeave) {
|
395 | onMouseLeave(event);
|
396 | }
|
397 | const newHover = -1;
|
398 | setState({
|
399 | hover: newHover,
|
400 | focus: newHover
|
401 | });
|
402 | if (onChangeActive && hover !== newHover) {
|
403 | onChangeActive(event, newHover);
|
404 | }
|
405 | };
|
406 | const handleChange = event => {
|
407 | let newValue = event.target.value === '' ? null : parseFloat(event.target.value);
|
408 |
|
409 |
|
410 |
|
411 | if (hover !== -1) {
|
412 | newValue = hover;
|
413 | }
|
414 | setValueState(newValue);
|
415 | if (onChange) {
|
416 | onChange(event, newValue);
|
417 | }
|
418 | };
|
419 | const handleClear = event => {
|
420 |
|
421 |
|
422 | if (event.clientX === 0 && event.clientY === 0) {
|
423 | return;
|
424 | }
|
425 | setState({
|
426 | hover: -1,
|
427 | focus: -1
|
428 | });
|
429 | setValueState(null);
|
430 | if (onChange && parseFloat(event.target.value) === valueRounded) {
|
431 | onChange(event, null);
|
432 | }
|
433 | };
|
434 | const handleFocus = event => {
|
435 | if (isFocusVisible(event.target)) {
|
436 | setFocusVisible(true);
|
437 | }
|
438 | const newFocus = parseFloat(event.target.value);
|
439 | setState(prev => ({
|
440 | hover: prev.hover,
|
441 | focus: newFocus
|
442 | }));
|
443 | };
|
444 | const handleBlur = event => {
|
445 | if (hover !== -1) {
|
446 | return;
|
447 | }
|
448 | if (!isFocusVisible(event.target)) {
|
449 | setFocusVisible(false);
|
450 | }
|
451 | const newFocus = -1;
|
452 | setState(prev => ({
|
453 | hover: prev.hover,
|
454 | focus: newFocus
|
455 | }));
|
456 | };
|
457 | const [emptyValueFocused, setEmptyValueFocused] = React.useState(false);
|
458 | const ownerState = {
|
459 | ...props,
|
460 | defaultValue,
|
461 | disabled,
|
462 | emptyIcon,
|
463 | emptyLabelText,
|
464 | emptyValueFocused,
|
465 | focusVisible,
|
466 | getLabelText,
|
467 | icon,
|
468 | IconContainerComponent,
|
469 | max,
|
470 | precision,
|
471 | readOnly,
|
472 | size
|
473 | };
|
474 | const classes = useUtilityClasses(ownerState);
|
475 | return _jsxs(RatingRoot, {
|
476 | ref: handleRef,
|
477 | onMouseMove: handleMouseMove,
|
478 | onMouseLeave: handleMouseLeave,
|
479 | className: clsx(classes.root, className, readOnly && 'MuiRating-readOnly'),
|
480 | ownerState: ownerState,
|
481 | role: readOnly ? 'img' : null,
|
482 | "aria-label": readOnly ? getLabelText(value) : null,
|
483 | ...other,
|
484 | children: [Array.from(new Array(max)).map((_, index) => {
|
485 | const itemValue = index + 1;
|
486 | const ratingItemProps = {
|
487 | classes,
|
488 | disabled,
|
489 | emptyIcon,
|
490 | focus,
|
491 | getLabelText,
|
492 | highlightSelectedOnly,
|
493 | hover,
|
494 | icon,
|
495 | IconContainerComponent,
|
496 | name,
|
497 | onBlur: handleBlur,
|
498 | onChange: handleChange,
|
499 | onClick: handleClear,
|
500 | onFocus: handleFocus,
|
501 | ratingValue: value,
|
502 | ratingValueRounded: valueRounded,
|
503 | readOnly,
|
504 | ownerState
|
505 | };
|
506 | const isActive = itemValue === Math.ceil(value) && (hover !== -1 || focus !== -1);
|
507 | if (precision < 1) {
|
508 | const items = Array.from(new Array(1 / precision));
|
509 | return _jsx(RatingDecimal, {
|
510 | className: clsx(classes.decimal, isActive && classes.iconActive),
|
511 | ownerState: ownerState,
|
512 | iconActive: isActive,
|
513 | children: items.map(($, indexDecimal) => {
|
514 | const itemDecimalValue = roundValueToPrecision(itemValue - 1 + (indexDecimal + 1) * precision, precision);
|
515 | return _jsx(RatingItem, {
|
516 | ...ratingItemProps,
|
517 |
|
518 | isActive: false,
|
519 | itemValue: itemDecimalValue,
|
520 | labelProps: {
|
521 | style: items.length - 1 === indexDecimal ? {} : {
|
522 | width: itemDecimalValue === value ? `${(indexDecimal + 1) * precision * 100}%` : '0%',
|
523 | overflow: 'hidden',
|
524 | position: 'absolute'
|
525 | }
|
526 | }
|
527 | }, itemDecimalValue);
|
528 | })
|
529 | }, itemValue);
|
530 | }
|
531 | return _jsx(RatingItem, {
|
532 | ...ratingItemProps,
|
533 | isActive: isActive,
|
534 | itemValue: itemValue
|
535 | }, itemValue);
|
536 | }), !readOnly && !disabled && _jsxs(RatingLabel, {
|
537 | className: clsx(classes.label, classes.labelEmptyValue),
|
538 | ownerState: ownerState,
|
539 | children: [_jsx("input", {
|
540 | className: classes.visuallyHidden,
|
541 | value: "",
|
542 | id: `${name}-empty`,
|
543 | type: "radio",
|
544 | name: name,
|
545 | checked: valueRounded == null,
|
546 | onFocus: () => setEmptyValueFocused(true),
|
547 | onBlur: () => setEmptyValueFocused(false),
|
548 | onChange: handleChange
|
549 | }), _jsx("span", {
|
550 | className: classes.visuallyHidden,
|
551 | children: emptyLabelText
|
552 | })]
|
553 | })]
|
554 | });
|
555 | });
|
556 | process.env.NODE_ENV !== "production" ? Rating.propTypes = {
|
557 |
|
558 |
|
559 |
|
560 |
|
561 | |
562 |
|
563 |
|
564 | classes: PropTypes.object,
|
565 | |
566 |
|
567 |
|
568 | className: PropTypes.string,
|
569 | |
570 |
|
571 |
|
572 |
|
573 | defaultValue: PropTypes.number,
|
574 | |
575 |
|
576 |
|
577 |
|
578 | disabled: PropTypes.bool,
|
579 | |
580 |
|
581 |
|
582 |
|
583 | emptyIcon: PropTypes.node,
|
584 | |
585 |
|
586 |
|
587 |
|
588 | emptyLabelText: PropTypes.node,
|
589 | |
590 |
|
591 |
|
592 |
|
593 |
|
594 |
|
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 | getLabelText: PropTypes.func,
|
601 | |
602 |
|
603 |
|
604 |
|
605 | highlightSelectedOnly: PropTypes.bool,
|
606 | |
607 |
|
608 |
|
609 |
|
610 | icon: PropTypes.node,
|
611 | |
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
617 |
|
618 | IconContainerComponent: PropTypes.elementType,
|
619 | |
620 |
|
621 |
|
622 |
|
623 | max: PropTypes.number,
|
624 | |
625 |
|
626 |
|
627 |
|
628 |
|
629 | name: PropTypes.string,
|
630 | |
631 |
|
632 |
|
633 |
|
634 |
|
635 | onChange: PropTypes.func,
|
636 | |
637 |
|
638 |
|
639 |
|
640 |
|
641 | onChangeActive: PropTypes.func,
|
642 | |
643 |
|
644 |
|
645 | onMouseLeave: PropTypes.func,
|
646 | |
647 |
|
648 |
|
649 | onMouseMove: PropTypes.func,
|
650 | |
651 |
|
652 |
|
653 |
|
654 | precision: chainPropTypes(PropTypes.number, props => {
|
655 | if (props.precision < 0.1) {
|
656 | return new Error(['MUI: The prop `precision` should be above 0.1.', 'A value below this limit has an imperceptible impact.'].join('\n'));
|
657 | }
|
658 | return null;
|
659 | }),
|
660 | |
661 |
|
662 |
|
663 |
|
664 | readOnly: PropTypes.bool,
|
665 | |
666 |
|
667 |
|
668 |
|
669 | size: PropTypes .oneOfType([PropTypes.oneOf(['small', 'medium', 'large']), PropTypes.string]),
|
670 | |
671 |
|
672 |
|
673 | sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object]),
|
674 | |
675 |
|
676 |
|
677 | value: PropTypes.number
|
678 | } : void 0;
|
679 | export default Rating; |
\ | No newline at end of file |