UNPKG

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