diff --git a/docs/pages/api-docs/rating.md b/docs/pages/api-docs/rating.md index 467f6dd107f20e..16df18697b8a94 100644 --- a/docs/pages/api-docs/rating.md +++ b/docs/pages/api-docs/rating.md @@ -1,5 +1,5 @@ --- -filename: /packages/material-ui-lab/src/Rating/Rating.js +filename: /packages/material-ui/src/Rating/Rating.js --- @@ -11,9 +11,9 @@ filename: /packages/material-ui-lab/src/Rating/Rating.js ## Import ```js -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; // or -import { Rating } from '@material-ui/lab'; +import { Rating } from '@material-ui/core'; ``` You can learn more about the difference by [reading this guide](/guides/minimizing-bundle-size/). @@ -76,7 +76,7 @@ You can override the style of the component thanks to one of these customization - With a [global class name](/customization/components/#overriding-styles-with-global-class-names). - With a theme and an [`overrides` property](/customization/globals/#css). -If that's not sufficient, you can check the [implementation of the component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui-lab/src/Rating/Rating.js) for more detail. +If that's not sufficient, you can check the [implementation of the component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/Rating/Rating.js) for more detail. ## Demos diff --git a/docs/src/modules/utils/getJsxPreview.test.js b/docs/src/modules/utils/getJsxPreview.test.js index 38fa388e412402..9926fb233238e2 100644 --- a/docs/src/modules/utils/getJsxPreview.test.js +++ b/docs/src/modules/utils/getJsxPreview.test.js @@ -7,7 +7,7 @@ describe('getJsxPreview', () => { getJsxPreview( ` import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; export default function HalfRating() { return ; diff --git a/docs/src/pages.js b/docs/src/pages.js index 31263dedf5cb11..f2ece91d3aed60 100644 --- a/docs/src/pages.js +++ b/docs/src/pages.js @@ -41,6 +41,7 @@ const pages = [ { pathname: '/components/radio-buttons' }, { pathname: '/components/selects' }, { pathname: '/components/slider' }, + { pathname: '/components/rating' }, { pathname: '/components/switches' }, { pathname: '/components/text-fields' }, { pathname: '/components/transfer-list' }, @@ -119,7 +120,6 @@ const pages = [ { pathname: '/components/about-the-lab' }, { pathname: '/components/autocomplete' }, { pathname: '/components/pagination' }, - { pathname: '/components/rating' }, { pathname: '/components/skeleton' }, { pathname: '/components/slider-styled' }, { pathname: '/components/speed-dial' }, diff --git a/docs/src/pages/components/rating/BasicRating.js b/docs/src/pages/components/rating/BasicRating.js new file mode 100644 index 00000000000000..34d18da7d5daf1 --- /dev/null +++ b/docs/src/pages/components/rating/BasicRating.js @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Rating from '@material-ui/core/Rating'; +import Typography from '@material-ui/core/Typography'; + +const useStyles = makeStyles((theme) => ({ + root: { + '& > legend': { + marginTop: theme.spacing(2), + }, + }, +})); + +export default function BasicRating() { + const classes = useStyles(); + const [value, setValue] = React.useState(2); + + return ( +
+ Controlled + { + setValue(newValue); + }} + /> + Read only + + Disabled + + No rating given + +
+ ); +} diff --git a/docs/src/pages/components/rating/BasicRating.tsx b/docs/src/pages/components/rating/BasicRating.tsx new file mode 100644 index 00000000000000..acbfbb92f7594e --- /dev/null +++ b/docs/src/pages/components/rating/BasicRating.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { makeStyles, createStyles } from '@material-ui/core/styles'; +import Rating from '@material-ui/core/Rating'; +import Typography from '@material-ui/core/Typography'; + +const useStyles = makeStyles((theme) => + createStyles({ + root: { + '& > legend': { + marginTop: theme.spacing(2), + }, + }, + }), +); + +export default function BasicRating() { + const classes = useStyles(); + const [value, setValue] = React.useState(2); + + return ( +
+ Controlled + { + setValue(newValue); + }} + /> + Read only + + Disabled + + No rating given + +
+ ); +} diff --git a/docs/src/pages/components/rating/CustomizedRatings.js b/docs/src/pages/components/rating/CustomizedRating.js similarity index 54% rename from docs/src/pages/components/rating/CustomizedRatings.js rename to docs/src/pages/components/rating/CustomizedRating.js index 4a0f6ca176af5b..72c6bb5725cf0d 100644 --- a/docs/src/pages/components/rating/CustomizedRatings.js +++ b/docs/src/pages/components/rating/CustomizedRating.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Rating from '@material-ui/lab/Rating'; +import { makeStyles, withStyles } from '@material-ui/core/styles'; +import Rating from '@material-ui/core/Rating'; import FavoriteIcon from '@material-ui/icons/Favorite'; import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'; import SentimentVeryDissatisfiedIcon from '@material-ui/icons/SentimentVeryDissatisfied'; @@ -10,7 +10,14 @@ import SentimentSatisfiedIcon from '@material-ui/icons/SentimentSatisfied'; import SentimentSatisfiedAltIcon from '@material-ui/icons/SentimentSatisfiedAltOutlined'; import SentimentVerySatisfiedIcon from '@material-ui/icons/SentimentVerySatisfied'; import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; + +const useStyles = makeStyles((theme) => ({ + root: { + '& > legend': { + marginTop: theme.spacing(2), + }, + }, +})); const StyledRating = withStyles({ iconFilled: { @@ -53,33 +60,29 @@ IconContainer.propTypes = { value: PropTypes.number.isRequired, }; -export default function CustomizedRatings() { +export default function CustomizedRating() { + const classes = useStyles(); + return ( -
- - Custom icon and color - `${value} Heart${value !== 1 ? 's' : ''}`} - precision={0.5} - icon={} - emptyIcon={} - /> - - - 10 stars - - - - Custom icon set - customIcons[value].label} - IconContainerComponent={IconContainer} - /> - +
+ Custom icon and color + `${value} Heart${value !== 1 ? 's' : ''}`} + precision={0.5} + icon={} + emptyIcon={} + /> + 10 stars + + Custom icon set + customIcons[value].label} + IconContainerComponent={IconContainer} + />
); } diff --git a/docs/src/pages/components/rating/CustomizedRatings.tsx b/docs/src/pages/components/rating/CustomizedRating.tsx similarity index 53% rename from docs/src/pages/components/rating/CustomizedRatings.tsx rename to docs/src/pages/components/rating/CustomizedRating.tsx index d21ff4796c457c..ee006400e300db 100644 --- a/docs/src/pages/components/rating/CustomizedRatings.tsx +++ b/docs/src/pages/components/rating/CustomizedRating.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { withStyles } from '@material-ui/core/styles'; -import Rating, { IconContainerProps } from '@material-ui/lab/Rating'; +import { makeStyles, createStyles, withStyles } from '@material-ui/core/styles'; +import Rating, { IconContainerProps } from '@material-ui/core/Rating'; import FavoriteIcon from '@material-ui/icons/Favorite'; import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'; import SentimentVeryDissatisfiedIcon from '@material-ui/icons/SentimentVeryDissatisfied'; @@ -9,7 +9,16 @@ import SentimentSatisfiedIcon from '@material-ui/icons/SentimentSatisfied'; import SentimentSatisfiedAltIcon from '@material-ui/icons/SentimentSatisfiedAltOutlined'; import SentimentVerySatisfiedIcon from '@material-ui/icons/SentimentVerySatisfied'; import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; + +const useStyles = makeStyles((theme) => + createStyles({ + root: { + '& > legend': { + marginTop: theme.spacing(2), + }, + }, + }), +); const StyledRating = withStyles({ iconFilled: { @@ -53,35 +62,31 @@ function IconContainer(props: IconContainerProps) { return {customIcons[value].icon}; } -export default function CustomizedRatings() { +export default function CustomizedRating() { + const classes = useStyles(); + return ( -
- - Custom icon and color - - `${value} Heart${value !== 1 ? 's' : ''}` - } - precision={0.5} - icon={} - emptyIcon={} - /> - - - 10 stars - - - - Custom icon set - customIcons[value].label} - IconContainerComponent={IconContainer} - /> - +
+ Custom icon and color + + `${value} Heart${value !== 1 ? 's' : ''}` + } + precision={0.5} + icon={} + emptyIcon={} + /> + 10 stars + + Custom icon set + customIcons[value].label} + IconContainerComponent={IconContainer} + />
); } diff --git a/docs/src/pages/components/rating/HalfRating.js b/docs/src/pages/components/rating/HalfRating.js index d71955e790816b..6aa5480b2681b3 100644 --- a/docs/src/pages/components/rating/HalfRating.js +++ b/docs/src/pages/components/rating/HalfRating.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme) => ({ diff --git a/docs/src/pages/components/rating/HalfRating.tsx b/docs/src/pages/components/rating/HalfRating.tsx index 31651c5d01a76d..9bdf485d239bf7 100644 --- a/docs/src/pages/components/rating/HalfRating.tsx +++ b/docs/src/pages/components/rating/HalfRating.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme: Theme) => diff --git a/docs/src/pages/components/rating/HoverRating.js b/docs/src/pages/components/rating/HoverRating.js index cf59181710298f..c71b6ec1e36d7f 100644 --- a/docs/src/pages/components/rating/HoverRating.js +++ b/docs/src/pages/components/rating/HoverRating.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import Box from '@material-ui/core/Box'; import StarIcon from '@material-ui/icons/Star'; diff --git a/docs/src/pages/components/rating/HoverRating.tsx b/docs/src/pages/components/rating/HoverRating.tsx index c84fa817b3ae97..4d7679de9802c7 100644 --- a/docs/src/pages/components/rating/HoverRating.tsx +++ b/docs/src/pages/components/rating/HoverRating.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import Box from '@material-ui/core/Box'; import StarIcon from '@material-ui/icons/Star'; diff --git a/docs/src/pages/components/rating/RatingSize.js b/docs/src/pages/components/rating/RatingSize.js index a135df8d039f41..065e05c89a8f2a 100644 --- a/docs/src/pages/components/rating/RatingSize.js +++ b/docs/src/pages/components/rating/RatingSize.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme) => ({ diff --git a/docs/src/pages/components/rating/RatingSize.tsx b/docs/src/pages/components/rating/RatingSize.tsx index b46fd5e963a096..154334958fa5a4 100644 --- a/docs/src/pages/components/rating/RatingSize.tsx +++ b/docs/src/pages/components/rating/RatingSize.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme: Theme) => diff --git a/docs/src/pages/components/rating/SimpleRating.js b/docs/src/pages/components/rating/SimpleRating.js deleted file mode 100644 index c88d73a305a575..00000000000000 --- a/docs/src/pages/components/rating/SimpleRating.js +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; -import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; - -export default function SimpleRating() { - const [value, setValue] = React.useState(2); - - return ( -
- - Controlled - { - setValue(newValue); - }} - /> - - - Read only - - - - Disabled - - - - No rating given - - -
- ); -} diff --git a/docs/src/pages/components/rating/SimpleRating.tsx b/docs/src/pages/components/rating/SimpleRating.tsx deleted file mode 100644 index c0159b871194c6..00000000000000 --- a/docs/src/pages/components/rating/SimpleRating.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import Rating from '@material-ui/lab/Rating'; -import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; - -export default function SimpleRating() { - const [value, setValue] = React.useState(2); - - return ( -
- - Controlled - { - setValue(newValue); - }} - /> - - - Read only - - - - Disabled - - - - No rating given - - -
- ); -} diff --git a/docs/src/pages/components/rating/TextRating.js b/docs/src/pages/components/rating/TextRating.js index 10025fdec899bf..ffe44129fcf91b 100644 --- a/docs/src/pages/components/rating/TextRating.js +++ b/docs/src/pages/components/rating/TextRating.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import Box from '@material-ui/core/Box'; import StarIcon from '@material-ui/icons/Star'; diff --git a/docs/src/pages/components/rating/TextRating.tsx b/docs/src/pages/components/rating/TextRating.tsx index 47ddd9fd8c3f66..cc0b0c8193fddc 100644 --- a/docs/src/pages/components/rating/TextRating.tsx +++ b/docs/src/pages/components/rating/TextRating.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import Box from '@material-ui/core/Box'; import StarIcon from '@material-ui/icons/Star'; diff --git a/docs/src/pages/components/rating/rating.md b/docs/src/pages/components/rating/rating.md index ac782a7c1e8761..b4c88d8d983682 100644 --- a/docs/src/pages/components/rating/rating.md +++ b/docs/src/pages/components/rating/rating.md @@ -3,58 +3,66 @@ title: Rating React component components: Rating githubLabel: 'component: Rating' waiAria: https://www.w3.org/WAI/tutorials/forms/custom-controls/#a-star-rating -packageName: '@material-ui/lab' --- # Rating -

Ratings provide insight regarding others’ opinions and experiences with a product. Users can also rate products they’ve purchased.

+

Ratings provide insight regarding others’ opinions and experiences, and can allow the user to submit a rating of thier own.

{{"component": "modules/components/ComponentLinkHeader.js"}} -## Simple ratings +## Basic rating -{{"demo": "pages/components/rating/SimpleRating.js"}} +{{"demo": "pages/components/rating/BasicRating.js"}} -## Customized ratings +## Rating precision -Here are some examples of customizing the component. You can learn more about this in the -[overrides documentation page](/customization/components/). +The rating can display any float number with the `value` prop. +Use the `precision` prop to define the minimum increment value change allowed. -{{"demo": "pages/components/rating/CustomizedRatings.js"}} +{{"demo": "pages/components/rating/HalfRating.js"}} ## Hover feedback -You can display a label on hover to help users pick the correct rating value. +You can display a label on hover to help the user pick the correct rating value. The demo uses the `onChangeActive` prop. {{"demo": "pages/components/rating/HoverRating.js"}} -## Half ratings +## Sizes -The rating can display any float number with the `value` prop. -Use the `precision` prop to define the minimum increment value change allowed. +For larger or smaller ratings use the `size` prop. -{{"demo": "pages/components/rating/HalfRating.js"}} +{{"demo": "pages/components/rating/RatingSize.js"}} -## Sizes +## Customized rating -Fancy larger or smaller ratings? Use the `size` prop. +Here are some examples of customizing the component. You can learn more about this in the +[overrides documentation page](/customization/components/). -{{"demo": "pages/components/rating/RatingSize.js"}} +{{"demo": "pages/components/rating/CustomizedRating.js"}} ## Accessibility -(WAI tutorial: https://www.w3.org/WAI/tutorials/forms/custom-controls/#a-star-rating) +([WAI tutorial](https://www.w3.org/WAI/tutorials/forms/custom-controls/#a-star-rating)) The accessibility of this component relies on: -- A radio group is used with its fields visually hidden. - It contains six radio buttons, one for each star and another for 0 stars, which is checked by default. Make sure you are providing a `name` prop that is unique to the parent form. -- The labels for the radio buttons contain actual text (“1 Star”, “2 Stars”, …), make sure you provide a `getLabelText` prop when the page language is not English. +- A radio group with its fields visually hidden. + It contains six radio buttons, one for each star, and another for 0 stars that is checked by default. Be sure to provide a value for the `name` prop that is unique to the parent form. +- Labels for the radio buttons containing actual text (“1 Star”, “2 Stars”, …). + Be sure to provide a suitable function to the `getLabelText` prop when the page is in a language other than English. You can use the [included locales](https://material-ui.com/guides/localization/), or provide your own. +- A visually distinct appearance for the rating icons. + By default, the rating component uses both a difference of color and shape (filled and empty icons)to indicate the value. In the event that you are using color as the only means to indicate the value, the information should also be also displayed as text, as in this demo. This is important to match [success Criterion 1.4.1](https://www.w3.org/TR/WCAG21/#use-of-color) of WCAG2.1. -By default, the rating component uses both a difference of color and shape between the filled and empty icons to indicate the value. +{{"demo": "pages/components/rating/TextRating.js"}} -In the event that you are using color as the only means to indicate the value, the information should also be also displayed as text, as in this demo. This is important to match [success Criterion 1.4.1](https://www.w3.org/TR/WCAG21/#use-of-color) of WCAG2.1. +### ARIA -{{"demo": "pages/components/rating/TextRating.js"}} +The read only rating has a role of "img", and an aria-label that describes the displayed rating. + +### Keyboard + +Because the rating component uses radio buttons, keyboard interaction follows the native browser behavior. Tab will focus the current rating, and cursor keys control the selected rating. + +The read only rating is not focusable. diff --git a/docs/src/pages/guides/localization/Locales.js b/docs/src/pages/guides/localization/Locales.js index 885aaab87b6c63..0ce944466cca18 100644 --- a/docs/src/pages/guides/localization/Locales.js +++ b/docs/src/pages/guides/localization/Locales.js @@ -1,7 +1,7 @@ import * as React from 'react'; import TablePagination from '@material-ui/core/TablePagination'; import Pagination from '@material-ui/lab/Pagination'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import Autocomplete from '@material-ui/lab/Autocomplete'; import TextField from '@material-ui/core/TextField'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; diff --git a/docs/src/pages/guides/localization/Locales.tsx b/docs/src/pages/guides/localization/Locales.tsx index d4263865c39820..55cc9b7d582ce8 100644 --- a/docs/src/pages/guides/localization/Locales.tsx +++ b/docs/src/pages/guides/localization/Locales.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import TablePagination from '@material-ui/core/TablePagination'; import Pagination from '@material-ui/lab/Pagination'; -import Rating from '@material-ui/lab/Rating'; +import Rating from '@material-ui/core/Rating'; import Autocomplete from '@material-ui/lab/Autocomplete'; import TextField from '@material-ui/core/TextField'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; diff --git a/docs/src/pages/guides/migration-v4/migration-v4.md b/docs/src/pages/guides/migration-v4/migration-v4.md index f679b23667fa48..05d904545851cd 100644 --- a/docs/src/pages/guides/migration-v4/migration-v4.md +++ b/docs/src/pages/guides/migration-v4/migration-v4.md @@ -524,6 +524,13 @@ const theme = createMuitheme({ ### Rating +- Move the component from the lab to the core. The component is now stable. + + ```diff + -import Rating from '@material-ui/lab/Rating'; + +import Rating from '@material-ui/core/Rating'; + ``` + - Change the default empty icon to improve accessibility. If you have a custom `icon` prop but no `emptyIcon` prop, you can restore the previous behavior with: diff --git a/packages/material-ui-lab/src/Rating/Rating.d.ts b/packages/material-ui-lab/src/Rating/Rating.d.ts index 092a46394c44ce..1e8656f51f520e 100644 --- a/packages/material-ui-lab/src/Rating/Rating.d.ts +++ b/packages/material-ui-lab/src/Rating/Rating.d.ts @@ -1,151 +1,2 @@ -import * as React from 'react'; -import { InternalStandardProps as StandardProps } from '@material-ui/core'; - -export interface IconContainerProps extends React.HTMLAttributes { - value: number; -} - -export interface RatingProps - extends StandardProps, 'children' | 'onChange'> { - /** - * Override or extend the styles applied to the component. - */ - classes?: { - /** Styles applied to the root element. */ - root?: string; - /** Styles applied to the root element if `size="small"`. */ - sizeSmall?: string; - /** Styles applied to the root element if `size="large"`. */ - sizeLarge?: string; - /** Styles applied to the root element if `readOnly={true}`. */ - readOnly?: string; - /** Pseudo-class applied to the root element if `disabled={true}`. */ - disabled?: string; - /** Pseudo-class applied to the root element if keyboard focused. */ - focusVisible?: string; - /** Visually hide an element. */ - visuallyHidden?: string; - /** Styles applied to the label elements. */ - label?: string; - /** Styles applied to the label of the "no value" input when it is active. */ - labelEmptyValueActive?: string; - /** Styles applied to the icon wrapping elements. */ - icon?: string; - /** Styles applied to the icon wrapping elements when empty. */ - iconEmpty?: string; - /** Styles applied to the icon wrapping elements when filled. */ - iconFilled?: string; - /** Styles applied to the icon wrapping elements when hover. */ - iconHover?: string; - /** Styles applied to the icon wrapping elements when focus. */ - iconFocus?: string; - /** Styles applied to the icon wrapping elements when active. */ - iconActive?: string; - /** Styles applied to the icon wrapping elements when decimals are necessary. */ - decimal?: string; - }; - /** - * The default value. Use when the component is not controlled. - * @default null - */ - defaultValue?: number; - /** - * If `true`, the rating will be disabled. - * @default false - */ - disabled?: boolean; - /** - * The icon to display when empty. - * @default - */ - emptyIcon?: React.ReactNode; - /** - * The label read when the rating input is empty. - * @default 'Empty' - */ - emptyLabelText?: React.ReactNode; - /** - * Accepts a function which returns a string value that provides a user-friendly name for the current value of the rating. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * - * @param {number} value The rating label's value to format. - * @returns {string} - * - * @default function defaultLabelText(value) { - * return `${value} Star${value !== 1 ? 's' : ''}`; - * } - */ - getLabelText?: (value: number) => string; - /** - * The icon to display. - * @default - */ - icon?: React.ReactNode; - /** - * The component containing the icon. - * @default function IconContainer(props) { - * const { value, ...other } = props; - * return ; - * } - */ - IconContainerComponent?: React.ElementType; - /** - * Maximum rating. - * @default 5 - */ - max?: number; - /** - * The name attribute of the radio `input` elements. - * This input `name` should be unique within the page. - * Being unique within a form is insufficient since the `name` is used to generated IDs. - */ - name?: string; - /** - * Callback fired when the value changes. - * - * @param {object} event The event source of the callback. - * @param {number} value The new value. - */ - onChange?: (event: React.SyntheticEvent, value: number | null) => void; - /** - * Callback function that is fired when the hover state changes. - * - * @param {object} event The event source of the callback. - * @param {number} value The new value. - */ - onChangeActive?: (event: React.SyntheticEvent, value: number) => void; - /** - * The minimum increment value change allowed. - * @default 1 - */ - precision?: number; - /** - * Removes all hover effects and pointer events. - * @default false - */ - readOnly?: boolean; - /** - * The size of the rating. - * @default 'medium' - */ - size?: 'small' | 'medium' | 'large'; - /** - * The rating value. - */ - value?: number | null; -} - -export type RatingClassKey = keyof NonNullable; - -/** - * - * Demos: - * - * - [Rating](https://material-ui.com/components/rating/) - * - * API: - * - * - [Rating API](https://material-ui.com/api/rating/) - */ -export default function Rating(props: RatingProps): JSX.Element; +export { default } from '@material-ui/core/Rating'; +export * from '@material-ui/core/Rating'; diff --git a/packages/material-ui-lab/src/Rating/Rating.js b/packages/material-ui-lab/src/Rating/Rating.js index 60ab67e1fc0ec5..96167414ea3915 100644 --- a/packages/material-ui-lab/src/Rating/Rating.js +++ b/packages/material-ui-lab/src/Rating/Rating.js @@ -1,581 +1,24 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { chainPropTypes } from '@material-ui/utils'; -import { useTheme, withStyles } from '@material-ui/core/styles'; -import { - capitalize, - useForkRef, - useIsFocusVisible, - useControlled, - unstable_useId as useId, -} from '@material-ui/core/utils'; -import { visuallyHidden } from '@material-ui/system'; -import Star from '../internal/svg-icons/Star'; -import StarBorder from '../internal/svg-icons/StarBorder'; - -function clamp(value, min, max) { - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; -} - -function getDecimalPrecision(num) { - const decimalPart = num.toString().split('.')[1]; - return decimalPart ? decimalPart.length : 0; -} - -function roundValueToPrecision(value, precision) { - if (value == null) { - return value; - } - - const nearest = Math.round(value / precision) * precision; - return Number(nearest.toFixed(getDecimalPrecision(precision))); -} - -export const styles = (theme) => ({ - /* Styles applied to the root element. */ - root: { - display: 'inline-flex', - // Required to position the pristine input absolutely - position: 'relative', - fontSize: theme.typography.pxToRem(24), - color: '#faaf00', - cursor: 'pointer', - textAlign: 'left', - WebkitTapHighlightColor: 'transparent', - '&$disabled': { - opacity: theme.palette.action.disabledOpacity, - pointerEvents: 'none', - }, - '&$focusVisible $iconActive': { - outline: '1px solid #999', - }, - }, - /* Styles applied to the root element if `size="small"`. */ - sizeSmall: { - fontSize: theme.typography.pxToRem(18), - }, - /* Styles applied to the root element if `size="large"`. */ - sizeLarge: { - fontSize: theme.typography.pxToRem(30), - }, - /* Styles applied to the root element if `readOnly={true}`. */ - readOnly: { - pointerEvents: 'none', - }, - /* Pseudo-class applied to the root element if `disabled={true}`. */ - disabled: {}, - /* Pseudo-class applied to the root element if keyboard focused. */ - focusVisible: {}, - /* Visually hide an element. */ - visuallyHidden, - /* Styles applied to the label elements. */ - label: { - cursor: 'inherit', - }, - /* Styles applied to the label of the "no value" input when it is active. */ - labelEmptyValueActive: { - top: 0, - bottom: 0, - position: 'absolute', - outline: '1px solid #999', - width: '100%', - }, - /* Styles applied to the icon wrapping elements. */ - icon: { - // Fit wrapper to actual icon size. - display: 'flex', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shortest, - }), - // Fix mouseLeave issue. - // https://github.com/facebook/react/issues/4492 - pointerEvents: 'none', - }, - /* Styles applied to the icon wrapping elements when empty. */ - iconEmpty: { - color: theme.palette.action.disabled, - }, - /* Styles applied to the icon wrapping elements when filled. */ - iconFilled: {}, - /* Styles applied to the icon wrapping elements when hover. */ - iconHover: {}, - /* Styles applied to the icon wrapping elements when focus. */ - iconFocus: {}, - /* Styles applied to the icon wrapping elements when active. */ - iconActive: { - transform: 'scale(1.2)', - }, - /* Styles applied to the icon wrapping elements when decimals are necessary. */ - decimal: { - position: 'relative', - }, -}); - -function IconContainer(props) { - const { value, ...other } = props; - return ; -} - -IconContainer.propTypes = { - value: PropTypes.number.isRequired, -}; - -const defaultIcon = ; -const defaultEmptyIcon = ; - -function defaultLabelText(value) { - return `${value} Star${value !== 1 ? 's' : ''}`; -} - -const Rating = React.forwardRef(function Rating(props, ref) { - const { - classes, - className, - defaultValue = null, - disabled = false, - emptyIcon = defaultEmptyIcon, - emptyLabelText = 'Empty', - getLabelText = defaultLabelText, - icon = defaultIcon, - IconContainerComponent = IconContainer, - max = 5, - name: nameProp, - onChange, - onChangeActive, - onMouseLeave, - onMouseMove, - precision = 1, - readOnly = false, - size = 'medium', - value: valueProp, - ...other - } = props; - - const name = useId(nameProp); - - const [valueDerived, setValueState] = useControlled({ - controlled: valueProp, - default: defaultValue, - name: 'Rating', - }); - - const valueRounded = roundValueToPrecision(valueDerived, precision); - const theme = useTheme(); - const [{ hover, focus }, setState] = React.useState({ - hover: -1, - focus: -1, - }); - - let value = valueRounded; - if (hover !== -1) { - value = hover; - } - if (focus !== -1) { - value = focus; - } - - const { - isFocusVisibleRef, - onBlur: handleBlurVisible, - onFocus: handleFocusVisible, - ref: focusVisibleRef, - } = useIsFocusVisible(); - const [focusVisible, setFocusVisible] = React.useState(false); - - const rootRef = React.useRef(); - const handleFocusRef = useForkRef(focusVisibleRef, rootRef); - const handleRef = useForkRef(handleFocusRef, ref); - - const handleMouseMove = (event) => { - if (onMouseMove) { - onMouseMove(event); - } - - const rootNode = rootRef.current; - const { right, left } = rootNode.getBoundingClientRect(); - const { width } = rootNode.firstChild.getBoundingClientRect(); - let percent; - - if (theme.direction === 'rtl') { - percent = (right - event.clientX) / (width * max); - } else { - percent = (event.clientX - left) / (width * max); - } - - let newHover = roundValueToPrecision(max * percent + precision / 2, precision); - newHover = clamp(newHover, precision, max); - - setState((prev) => - prev.hover === newHover && prev.focus === newHover - ? prev - : { - hover: newHover, - focus: newHover, - }, +import React from 'react'; +import Rating from '@material-ui/core/Rating'; + +let warnedOnce = false; + +/** + * @ignore - do not document. + */ +export default React.forwardRef(function DeprecatedRating(props, ref) { + if (!warnedOnce) { + console.warn( + [ + 'Material-UI: The Rating component was moved from the lab to the core.', + '', + "You should use `import { Rating } from '@material-ui/core'`", + "or `import Rating from '@material-ui/core/Rating'`", + ].join('\n'), ); - setFocusVisible(false); - - if (onChangeActive && hover !== newHover) { - onChangeActive(event, newHover); - } - }; - - const handleMouseLeave = (event) => { - if (onMouseLeave) { - onMouseLeave(event); - } - - const newHover = -1; - setState({ - hover: newHover, - focus: newHover, - }); - - if (onChangeActive && hover !== newHover) { - onChangeActive(event, newHover); - } - }; - - const handleChange = (event) => { - const newValue = parseFloat(event.target.value); - - setValueState(newValue); - - if (onChange) { - onChange(event, newValue); - } - }; - - const handleClear = (event) => { - // Ignore keyboard events - // https://github.com/facebook/react/issues/7407 - if (event.clientX === 0 && event.clientY === 0) { - return; - } - - setState({ - hover: -1, - focus: -1, - }); - - setValueState(null); - - if (onChange && parseFloat(event.target.value) === valueRounded) { - onChange(event, null); - } - }; - - const handleFocus = (event) => { - handleFocusVisible(event); - if (isFocusVisibleRef.current === true) { - setFocusVisible(true); - } - - const newFocus = parseFloat(event.target.value); - setState((prev) => ({ - hover: prev.hover, - focus: newFocus, - })); - - if (onChangeActive && focus !== newFocus) { - onChangeActive(event, newFocus); - } - }; - - const handleBlur = (event) => { - if (hover !== -1) { - return; - } - - handleBlurVisible(event); - if (isFocusVisibleRef.current === false) { - setFocusVisible(false); - } - - const newFocus = -1; - setState((prev) => ({ - hover: prev.hover, - focus: newFocus, - })); - - if (onChangeActive && focus !== newFocus) { - onChangeActive(event, newFocus); - } - }; - - const [emptyValueFocused, setEmptyValueFocused] = React.useState(false); - - const item = (state, labelProps) => { - const id = `${name}-${String(state.value).replace('.', '-')}`; - const container = ( - - {emptyIcon && !state.filled ? emptyIcon : icon} - - ); - - if (readOnly) { - return ( - - {container} - - ); - } - - return ( - - - - - ); - }; - - return ( - - {Array.from(new Array(max)).map((_, index) => { - const itemValue = index + 1; - - if (precision < 1) { - const items = Array.from(new Array(1 / precision)); - return ( - - {items.map(($, indexDecimal) => { - const itemDecimalValue = roundValueToPrecision( - itemValue - 1 + (indexDecimal + 1) * precision, - precision, - ); - - return item( - { - value: itemDecimalValue, - filled: itemDecimalValue <= value, - hover: itemDecimalValue <= hover, - focus: itemDecimalValue <= focus, - checked: itemDecimalValue === valueRounded, - }, - { - style: - items.length - 1 === indexDecimal - ? {} - : { - width: - itemDecimalValue === value - ? `${(indexDecimal + 1) * precision * 100}%` - : '0%', - overflow: 'hidden', - zIndex: 1, - position: 'absolute', - }, - }, - ); - })} - - ); - } + warnedOnce = true; + } - return item({ - value: itemValue, - active: itemValue === value && (hover !== -1 || focus !== -1), - filled: itemValue <= value, - hover: itemValue <= hover, - focus: itemValue <= focus, - checked: itemValue === valueRounded, - }); - })} - {!readOnly && !disabled && valueRounded == null && ( - - )} - - ); + return ; }); - -Rating.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | - // ---------------------------------------------------------------------- - /** - * Override or extend the styles applied to the component. - */ - classes: PropTypes.object, - /** - * @ignore - */ - className: PropTypes.string, - /** - * The default value. Use when the component is not controlled. - * @default null - */ - defaultValue: PropTypes.number, - /** - * If `true`, the rating will be disabled. - * @default false - */ - disabled: PropTypes.bool, - /** - * The icon to display when empty. - * @default - */ - emptyIcon: PropTypes.node, - /** - * The label read when the rating input is empty. - * @default 'Empty' - */ - emptyLabelText: PropTypes.node, - /** - * Accepts a function which returns a string value that provides a user-friendly name for the current value of the rating. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * - * @param {number} value The rating label's value to format. - * @returns {string} - * - * @default function defaultLabelText(value) { - * return `${value} Star${value !== 1 ? 's' : ''}`; - * } - */ - getLabelText: PropTypes.func, - /** - * The icon to display. - * @default - */ - icon: PropTypes.node, - /** - * The component containing the icon. - * @default function IconContainer(props) { - * const { value, ...other } = props; - * return ; - * } - */ - IconContainerComponent: PropTypes.elementType, - /** - * Maximum rating. - * @default 5 - */ - max: PropTypes.number, - /** - * The name attribute of the radio `input` elements. - * This input `name` should be unique within the page. - * Being unique within a form is insufficient since the `name` is used to generated IDs. - */ - name: PropTypes.string, - /** - * Callback fired when the value changes. - * - * @param {object} event The event source of the callback. - * @param {number} value The new value. - */ - onChange: PropTypes.func, - /** - * Callback function that is fired when the hover state changes. - * - * @param {object} event The event source of the callback. - * @param {number} value The new value. - */ - onChangeActive: PropTypes.func, - /** - * @ignore - */ - onMouseLeave: PropTypes.func, - /** - * @ignore - */ - onMouseMove: PropTypes.func, - /** - * The minimum increment value change allowed. - * @default 1 - */ - precision: chainPropTypes(PropTypes.number, (props) => { - if (props.precision < 0.1) { - return new Error( - [ - 'Material-UI: The prop `precision` should be above 0.1.', - 'A value below this limit has an imperceptible impact.', - ].join('\n'), - ); - } - return null; - }), - /** - * Removes all hover effects and pointer events. - * @default false - */ - readOnly: PropTypes.bool, - /** - * The size of the rating. - * @default 'medium' - */ - size: PropTypes.oneOf(['large', 'medium', 'small']), - /** - * The rating value. - */ - value: PropTypes.number, -}; - -export default withStyles(styles, { name: 'MuiRating' })(Rating); diff --git a/packages/material-ui-lab/src/themeAugmentation/components.d.ts b/packages/material-ui-lab/src/themeAugmentation/components.d.ts index 9e3e3bd7a7cbef..cec70484dcb348 100644 --- a/packages/material-ui-lab/src/themeAugmentation/components.d.ts +++ b/packages/material-ui-lab/src/themeAugmentation/components.d.ts @@ -19,10 +19,6 @@ export interface LabComponents { styleOverrides?: ComponentsOverrides['MuiPaginationItem']; variants?: ComponentsVariants['MuiPaginationItem']; }; - MuiRating?: { - defaultProps?: ComponentsProps['MuiRating']; - styleOverrides?: ComponentsOverrides['MuiRating']; - }; MuiSkeleton?: { defaultProps?: ComponentsProps['MuiSkeleton']; styleOverrides?: ComponentsOverrides['MuiSkeleton']; diff --git a/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts b/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts index 01e87f683316cc..a4b887b0436656 100644 --- a/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts +++ b/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts @@ -2,7 +2,6 @@ import { AutocompleteClassKey } from '../Autocomplete'; import { AvatarGroupClassKey } from '../AvatarGroup'; import { PaginationClassKey } from '../Pagination'; import { PaginationItemClassKey } from '../PaginationItem'; -import { RatingClassKey } from '../Rating'; import { SkeletonClassKey } from '../Skeleton'; import { SpeedDialClassKey } from '../SpeedDial'; import { SpeedDialActionClassKey } from '../SpeedDialAction'; @@ -26,7 +25,6 @@ export interface LabComponentNameToClassKey { MuiAvatarGroup: AvatarGroupClassKey; MuiPagination: PaginationClassKey; MuiPaginationItem: PaginationItemClassKey; - MuiRating: RatingClassKey; MuiSkeleton: SkeletonClassKey; MuiSpeedDial: SpeedDialClassKey; MuiSpeedDialAction: SpeedDialActionClassKey; diff --git a/packages/material-ui-lab/src/themeAugmentation/props.d.ts b/packages/material-ui-lab/src/themeAugmentation/props.d.ts index c13dbfc17e1dcd..673a2b85f0db3c 100644 --- a/packages/material-ui-lab/src/themeAugmentation/props.d.ts +++ b/packages/material-ui-lab/src/themeAugmentation/props.d.ts @@ -2,7 +2,6 @@ import { AutocompleteProps } from '../Autocomplete'; import { AvatarGroupProps } from '../AvatarGroup'; import { PaginationProps } from '../Pagination'; import { PaginationItemProps } from '../PaginationItem'; -import { RatingProps } from '../Rating'; import { SkeletonProps } from '../Skeleton'; import { SpeedDialProps } from '../SpeedDial'; import { SpeedDialActionProps } from '../SpeedDialAction'; @@ -26,7 +25,6 @@ export interface LabComponentsPropsList { MuiAvatarGroup: AvatarGroupProps; MuiPagination: PaginationProps; MuiPaginationItem: PaginationItemProps; - MuiRating: RatingProps; MuiSkeleton: SkeletonProps; MuiSpeedDial: SpeedDialProps; MuiSpeedDialAction: SpeedDialActionProps; diff --git a/packages/material-ui/src/Rating/Rating.d.ts b/packages/material-ui/src/Rating/Rating.d.ts new file mode 100644 index 00000000000000..963c4ec17a3d4f --- /dev/null +++ b/packages/material-ui/src/Rating/Rating.d.ts @@ -0,0 +1,151 @@ +import * as React from 'react'; +import { InternalStandardProps as StandardProps } from '..'; + +export interface IconContainerProps extends React.HTMLAttributes { + value: number; +} + +export interface RatingProps + extends StandardProps, 'children' | 'onChange'> { + /** + * Override or extend the styles applied to the component. + */ + classes?: { + /** Styles applied to the root element. */ + root?: string; + /** Styles applied to the root element if `size="small"`. */ + sizeSmall?: string; + /** Styles applied to the root element if `size="large"`. */ + sizeLarge?: string; + /** Styles applied to the root element if `readOnly={true}`. */ + readOnly?: string; + /** Pseudo-class applied to the root element if `disabled={true}`. */ + disabled?: string; + /** Pseudo-class applied to the root element if keyboard focused. */ + focusVisible?: string; + /** Visually hide an element. */ + visuallyHidden?: string; + /** Styles applied to the label elements. */ + label?: string; + /** Styles applied to the label of the "no value" input when it is active. */ + labelEmptyValueActive?: string; + /** Styles applied to the icon wrapping elements. */ + icon?: string; + /** Styles applied to the icon wrapping elements when empty. */ + iconEmpty?: string; + /** Styles applied to the icon wrapping elements when filled. */ + iconFilled?: string; + /** Styles applied to the icon wrapping elements when hover. */ + iconHover?: string; + /** Styles applied to the icon wrapping elements when focus. */ + iconFocus?: string; + /** Styles applied to the icon wrapping elements when active. */ + iconActive?: string; + /** Styles applied to the icon wrapping elements when decimals are necessary. */ + decimal?: string; + }; + /** + * The default value. Use when the component is not controlled. + * @default null + */ + defaultValue?: number; + /** + * If `true`, the rating will be disabled. + * @default false + */ + disabled?: boolean; + /** + * The icon to display when empty. + * @default + */ + emptyIcon?: React.ReactNode; + /** + * The label read when the rating input is empty. + * @default 'Empty' + */ + emptyLabelText?: React.ReactNode; + /** + * Accepts a function which returns a string value that provides a user-friendly name for the current value of the rating. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * + * @param {number} value The rating label's value to format. + * @returns {string} + * + * @default function defaultLabelText(value) { + * return `${value} Star${value !== 1 ? 's' : ''}`; + * } + */ + getLabelText?: (value: number) => string; + /** + * The icon to display. + * @default + */ + icon?: React.ReactNode; + /** + * The component containing the icon. + * @default function IconContainer(props) { + * const { value, ...other } = props; + * return ; + * } + */ + IconContainerComponent?: React.ElementType; + /** + * Maximum rating. + * @default 5 + */ + max?: number; + /** + * The name attribute of the radio `input` elements. + * This input `name` should be unique within the page. + * Being unique within a form is insufficient since the `name` is used to generated IDs. + */ + name?: string; + /** + * Callback fired when the value changes. + * + * @param {object} event The event source of the callback. + * @param {number} value The new value. + */ + onChange?: (event: React.SyntheticEvent, value: number | null) => void; + /** + * Callback function that is fired when the hover state changes. + * + * @param {object} event The event source of the callback. + * @param {number} value The new value. + */ + onChangeActive?: (event: React.SyntheticEvent, value: number) => void; + /** + * The minimum increment value change allowed. + * @default 1 + */ + precision?: number; + /** + * Removes all hover effects and pointer events. + * @default false + */ + readOnly?: boolean; + /** + * The size of the rating. + * @default 'medium' + */ + size?: 'small' | 'medium' | 'large'; + /** + * The rating value. + */ + value?: number | null; +} + +export type RatingClassKey = keyof NonNullable; + +/** + * + * Demos: + * + * - [Rating](https://material-ui.com/components/rating/) + * + * API: + * + * - [Rating API](https://material-ui.com/api/rating/) + */ +export default function Rating(props: RatingProps): JSX.Element; diff --git a/packages/material-ui/src/Rating/Rating.js b/packages/material-ui/src/Rating/Rating.js new file mode 100644 index 00000000000000..e89278b2b8a7d3 --- /dev/null +++ b/packages/material-ui/src/Rating/Rating.js @@ -0,0 +1,581 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { visuallyHidden } from '@material-ui/system'; +import { chainPropTypes } from '@material-ui/utils'; +import { useTheme, withStyles } from '../styles'; +import { + capitalize, + useForkRef, + useIsFocusVisible, + useControlled, + unstable_useId as useId, +} from '../utils'; +import Star from '../internal/svg-icons/Star'; +import StarBorder from '../internal/svg-icons/StarBorder'; + +function clamp(value, min, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} + +function getDecimalPrecision(num) { + const decimalPart = num.toString().split('.')[1]; + return decimalPart ? decimalPart.length : 0; +} + +function roundValueToPrecision(value, precision) { + if (value == null) { + return value; + } + + const nearest = Math.round(value / precision) * precision; + return Number(nearest.toFixed(getDecimalPrecision(precision))); +} + +export const styles = (theme) => ({ + /* Styles applied to the root element. */ + root: { + display: 'inline-flex', + // Required to position the pristine input absolutely + position: 'relative', + fontSize: theme.typography.pxToRem(24), + color: '#faaf00', + cursor: 'pointer', + textAlign: 'left', + WebkitTapHighlightColor: 'transparent', + '&$disabled': { + opacity: theme.palette.action.disabledOpacity, + pointerEvents: 'none', + }, + '&$focusVisible $iconActive': { + outline: '1px solid #999', + }, + }, + /* Styles applied to the root element if `size="small"`. */ + sizeSmall: { + fontSize: theme.typography.pxToRem(18), + }, + /* Styles applied to the root element if `size="large"`. */ + sizeLarge: { + fontSize: theme.typography.pxToRem(30), + }, + /* Styles applied to the root element if `readOnly={true}`. */ + readOnly: { + pointerEvents: 'none', + }, + /* Pseudo-class applied to the root element if `disabled={true}`. */ + disabled: {}, + /* Pseudo-class applied to the root element if keyboard focused. */ + focusVisible: {}, + /* Visually hide an element. */ + visuallyHidden, + /* Styles applied to the label elements. */ + label: { + cursor: 'inherit', + }, + /* Styles applied to the label of the "no value" input when it is active. */ + labelEmptyValueActive: { + top: 0, + bottom: 0, + position: 'absolute', + outline: '1px solid #999', + width: '100%', + }, + /* Styles applied to the icon wrapping elements. */ + icon: { + // Fit wrapper to actual icon size. + display: 'flex', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }), + // Fix mouseLeave issue. + // https://github.com/facebook/react/issues/4492 + pointerEvents: 'none', + }, + /* Styles applied to the icon wrapping elements when empty. */ + iconEmpty: { + color: theme.palette.action.disabled, + }, + /* Styles applied to the icon wrapping elements when filled. */ + iconFilled: {}, + /* Styles applied to the icon wrapping elements when hover. */ + iconHover: {}, + /* Styles applied to the icon wrapping elements when focus. */ + iconFocus: {}, + /* Styles applied to the icon wrapping elements when active. */ + iconActive: { + transform: 'scale(1.2)', + }, + /* Styles applied to the icon wrapping elements when decimals are necessary. */ + decimal: { + position: 'relative', + }, +}); + +function IconContainer(props) { + const { value, ...other } = props; + return ; +} + +IconContainer.propTypes = { + value: PropTypes.number.isRequired, +}; + +const defaultIcon = ; +const defaultEmptyIcon = ; + +function defaultLabelText(value) { + return `${value} Star${value !== 1 ? 's' : ''}`; +} + +const Rating = React.forwardRef(function Rating(props, ref) { + const { + classes, + className, + defaultValue = null, + disabled = false, + emptyIcon = defaultEmptyIcon, + emptyLabelText = 'Empty', + getLabelText = defaultLabelText, + icon = defaultIcon, + IconContainerComponent = IconContainer, + max = 5, + name: nameProp, + onChange, + onChangeActive, + onMouseLeave, + onMouseMove, + precision = 1, + readOnly = false, + size = 'medium', + value: valueProp, + ...other + } = props; + + const name = useId(nameProp); + + const [valueDerived, setValueState] = useControlled({ + controlled: valueProp, + default: defaultValue, + name: 'Rating', + }); + + const valueRounded = roundValueToPrecision(valueDerived, precision); + const theme = useTheme(); + const [{ hover, focus }, setState] = React.useState({ + hover: -1, + focus: -1, + }); + + let value = valueRounded; + if (hover !== -1) { + value = hover; + } + if (focus !== -1) { + value = focus; + } + + const { + isFocusVisibleRef, + onBlur: handleBlurVisible, + onFocus: handleFocusVisible, + ref: focusVisibleRef, + } = useIsFocusVisible(); + const [focusVisible, setFocusVisible] = React.useState(false); + + const rootRef = React.useRef(); + const handleFocusRef = useForkRef(focusVisibleRef, rootRef); + const handleRef = useForkRef(handleFocusRef, ref); + + const handleMouseMove = (event) => { + if (onMouseMove) { + onMouseMove(event); + } + + const rootNode = rootRef.current; + const { right, left } = rootNode.getBoundingClientRect(); + const { width } = rootNode.firstChild.getBoundingClientRect(); + let percent; + + if (theme.direction === 'rtl') { + percent = (right - event.clientX) / (width * max); + } else { + percent = (event.clientX - left) / (width * max); + } + + let newHover = roundValueToPrecision(max * percent + precision / 2, precision); + newHover = clamp(newHover, precision, max); + + setState((prev) => + prev.hover === newHover && prev.focus === newHover + ? prev + : { + hover: newHover, + focus: newHover, + }, + ); + + setFocusVisible(false); + + if (onChangeActive && hover !== newHover) { + onChangeActive(event, newHover); + } + }; + + const handleMouseLeave = (event) => { + if (onMouseLeave) { + onMouseLeave(event); + } + + const newHover = -1; + setState({ + hover: newHover, + focus: newHover, + }); + + if (onChangeActive && hover !== newHover) { + onChangeActive(event, newHover); + } + }; + + const handleChange = (event) => { + const newValue = parseFloat(event.target.value); + + setValueState(newValue); + + if (onChange) { + onChange(event, newValue); + } + }; + + const handleClear = (event) => { + // Ignore keyboard events + // https://github.com/facebook/react/issues/7407 + if (event.clientX === 0 && event.clientY === 0) { + return; + } + + setState({ + hover: -1, + focus: -1, + }); + + setValueState(null); + + if (onChange && parseFloat(event.target.value) === valueRounded) { + onChange(event, null); + } + }; + + const handleFocus = (event) => { + handleFocusVisible(event); + if (isFocusVisibleRef.current === true) { + setFocusVisible(true); + } + + const newFocus = parseFloat(event.target.value); + setState((prev) => ({ + hover: prev.hover, + focus: newFocus, + })); + + if (onChangeActive && focus !== newFocus) { + onChangeActive(event, newFocus); + } + }; + + const handleBlur = (event) => { + if (hover !== -1) { + return; + } + + handleBlurVisible(event); + if (isFocusVisibleRef.current === false) { + setFocusVisible(false); + } + + const newFocus = -1; + setState((prev) => ({ + hover: prev.hover, + focus: newFocus, + })); + + if (onChangeActive && focus !== newFocus) { + onChangeActive(event, newFocus); + } + }; + + const [emptyValueFocused, setEmptyValueFocused] = React.useState(false); + + const item = (state, labelProps) => { + const id = `${name}-${String(state.value).replace('.', '-')}`; + const container = ( + + {emptyIcon && !state.filled ? emptyIcon : icon} + + ); + + if (readOnly) { + return ( + + {container} + + ); + } + + return ( + + + + + ); + }; + + return ( + + {Array.from(new Array(max)).map((_, index) => { + const itemValue = index + 1; + + if (precision < 1) { + const items = Array.from(new Array(1 / precision)); + return ( + + {items.map(($, indexDecimal) => { + const itemDecimalValue = roundValueToPrecision( + itemValue - 1 + (indexDecimal + 1) * precision, + precision, + ); + + return item( + { + value: itemDecimalValue, + filled: itemDecimalValue <= value, + hover: itemDecimalValue <= hover, + focus: itemDecimalValue <= focus, + checked: itemDecimalValue === valueRounded, + }, + { + style: + items.length - 1 === indexDecimal + ? {} + : { + width: + itemDecimalValue === value + ? `${(indexDecimal + 1) * precision * 100}%` + : '0%', + overflow: 'hidden', + zIndex: 1, + position: 'absolute', + }, + }, + ); + })} + + ); + } + + return item({ + value: itemValue, + active: itemValue === value && (hover !== -1 || focus !== -1), + filled: itemValue <= value, + hover: itemValue <= hover, + focus: itemValue <= focus, + checked: itemValue === valueRounded, + }); + })} + {!readOnly && !disabled && valueRounded == null && ( + + )} + + ); +}); + +Rating.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The default value. Use when the component is not controlled. + * @default null + */ + defaultValue: PropTypes.number, + /** + * If `true`, the rating will be disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * The icon to display when empty. + * @default + */ + emptyIcon: PropTypes.node, + /** + * The label read when the rating input is empty. + * @default 'Empty' + */ + emptyLabelText: PropTypes.node, + /** + * Accepts a function which returns a string value that provides a user-friendly name for the current value of the rating. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * + * @param {number} value The rating label's value to format. + * @returns {string} + * + * @default function defaultLabelText(value) { + * return `${value} Star${value !== 1 ? 's' : ''}`; + * } + */ + getLabelText: PropTypes.func, + /** + * The icon to display. + * @default + */ + icon: PropTypes.node, + /** + * The component containing the icon. + * @default function IconContainer(props) { + * const { value, ...other } = props; + * return ; + * } + */ + IconContainerComponent: PropTypes.elementType, + /** + * Maximum rating. + * @default 5 + */ + max: PropTypes.number, + /** + * The name attribute of the radio `input` elements. + * This input `name` should be unique within the page. + * Being unique within a form is insufficient since the `name` is used to generated IDs. + */ + name: PropTypes.string, + /** + * Callback fired when the value changes. + * + * @param {object} event The event source of the callback. + * @param {number} value The new value. + */ + onChange: PropTypes.func, + /** + * Callback function that is fired when the hover state changes. + * + * @param {object} event The event source of the callback. + * @param {number} value The new value. + */ + onChangeActive: PropTypes.func, + /** + * @ignore + */ + onMouseLeave: PropTypes.func, + /** + * @ignore + */ + onMouseMove: PropTypes.func, + /** + * The minimum increment value change allowed. + * @default 1 + */ + precision: chainPropTypes(PropTypes.number, (props) => { + if (props.precision < 0.1) { + return new Error( + [ + 'Material-UI: The prop `precision` should be above 0.1.', + 'A value below this limit has an imperceptible impact.', + ].join('\n'), + ); + } + return null; + }), + /** + * Removes all hover effects and pointer events. + * @default false + */ + readOnly: PropTypes.bool, + /** + * The size of the rating. + * @default 'medium' + */ + size: PropTypes.oneOf(['large', 'medium', 'small']), + /** + * The rating value. + */ + value: PropTypes.number, +}; + +export default withStyles(styles, { name: 'MuiRating' })(Rating); diff --git a/packages/material-ui-lab/src/Rating/Rating.test.js b/packages/material-ui/src/Rating/Rating.test.js similarity index 100% rename from packages/material-ui-lab/src/Rating/Rating.test.js rename to packages/material-ui/src/Rating/Rating.test.js diff --git a/packages/material-ui/src/Rating/index.d.ts b/packages/material-ui/src/Rating/index.d.ts new file mode 100644 index 00000000000000..88cba844f6235c --- /dev/null +++ b/packages/material-ui/src/Rating/index.d.ts @@ -0,0 +1,2 @@ +export { default } from './Rating'; +export * from './Rating'; diff --git a/packages/material-ui/src/Rating/index.js b/packages/material-ui/src/Rating/index.js new file mode 100644 index 00000000000000..4da188eae322b9 --- /dev/null +++ b/packages/material-ui/src/Rating/index.js @@ -0,0 +1 @@ +export { default } from './Rating'; diff --git a/packages/material-ui/src/index.d.ts b/packages/material-ui/src/index.d.ts index 35108fbe2391f8..a31f94411c7c4f 100644 --- a/packages/material-ui/src/index.d.ts +++ b/packages/material-ui/src/index.d.ts @@ -320,6 +320,9 @@ export * from './Radio'; export { default as RadioGroup } from './RadioGroup'; export * from './RadioGroup'; +export { default as Rating } from './Rating'; +export * from './Rating'; + export { default as ScopedCssBaseline } from './ScopedCssBaseline'; export * from './ScopedCssBaseline'; diff --git a/packages/material-ui/src/index.js b/packages/material-ui/src/index.js index 0cb8381dc7924c..e26709678125c5 100644 --- a/packages/material-ui/src/index.js +++ b/packages/material-ui/src/index.js @@ -246,6 +246,9 @@ export * from './Radio'; export { default as RadioGroup } from './RadioGroup'; export * from './RadioGroup'; +export { default as Rating } from './Rating'; +export * from './Rating'; + export { default as ScopedCssBaseline } from './ScopedCssBaseline'; export * from './ScopedCssBaseline'; diff --git a/packages/material-ui-lab/src/internal/svg-icons/Star.js b/packages/material-ui/src/internal/svg-icons/Star.js similarity index 79% rename from packages/material-ui-lab/src/internal/svg-icons/Star.js rename to packages/material-ui/src/internal/svg-icons/Star.js index 511a39c75401ad..4d5f2e5fa9c25e 100644 --- a/packages/material-ui-lab/src/internal/svg-icons/Star.js +++ b/packages/material-ui/src/internal/svg-icons/Star.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import { createSvgIcon } from '@material-ui/core/utils'; +import createSvgIcon from '../../utils/createSvgIcon'; /** * @ignore - internal component. diff --git a/packages/material-ui-lab/src/internal/svg-icons/StarBorder.js b/packages/material-ui/src/internal/svg-icons/StarBorder.js similarity index 85% rename from packages/material-ui-lab/src/internal/svg-icons/StarBorder.js rename to packages/material-ui/src/internal/svg-icons/StarBorder.js index 83ba76d5a25850..aae01550ff0b4c 100644 --- a/packages/material-ui-lab/src/internal/svg-icons/StarBorder.js +++ b/packages/material-ui/src/internal/svg-icons/StarBorder.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import { createSvgIcon } from '@material-ui/core/utils'; +import createSvgIcon from '../../utils/createSvgIcon'; /** * @ignore - internal component. diff --git a/packages/material-ui/src/locale/index.ts b/packages/material-ui/src/locale/index.ts index eb57b41c83fae0..4e0a2c1e97736a 100644 --- a/packages/material-ui/src/locale/index.ts +++ b/packages/material-ui/src/locale/index.ts @@ -3,6 +3,9 @@ import { ComponentsPropsList } from '../styles/props'; export interface Localization { components?: { + MuiAlert?: { + defaultProps: Pick; + }; MuiBreadcrumbs?: { defaultProps: Pick }; MuiTablePagination?: { defaultProps: Pick< @@ -10,14 +13,11 @@ export interface Localization { 'labelRowsPerPage' | 'labelDisplayedRows' | 'getItemAriaLabel' >; }; - // The core package has no dependencies on the @material-ui/lab components. - // We can't use ComponentsPropsList, we have to duplicate and inline the definitions. MuiRating?: { - defaultProps: { - emptyLabelText?: string; - getLabelText?: (value: number) => string; - }; + defaultProps: Pick; }; + // The core package has no dependencies on the @material-ui/lab components. + // We can't use ComponentsPropsList, we have to duplicate and inline the definitions. MuiAutocomplete?: { defaultProps: { clearText?: string; @@ -27,11 +27,6 @@ export interface Localization { openText?: string; }; }; - MuiAlert?: { - defaultProps: { - closeText?: string; - }; - }; MuiPagination?: { defaultProps: { 'aria-label'?: string; diff --git a/packages/material-ui/src/styles/overrides.d.ts b/packages/material-ui/src/styles/overrides.d.ts index 7e35ad6e14a672..1d2210977bf35b 100644 --- a/packages/material-ui/src/styles/overrides.d.ts +++ b/packages/material-ui/src/styles/overrides.d.ts @@ -67,6 +67,7 @@ import { OutlinedInputClassKey } from '../OutlinedInput'; import { PaperClassKey } from '../Paper'; import { PopoverClassKey } from '../Popover'; import { RadioClassKey } from '../Radio'; +import { RatingClassKey } from '../Rating'; import { ScopedCssBaselineClassKey } from '../ScopedCssBaseline'; import { SelectClassKey } from '../Select'; import { SliderClassKey } from '../Slider'; @@ -181,6 +182,7 @@ export interface ComponentNameToClassKey { MuiPaper: PaperClassKey; MuiPopover: PopoverClassKey; MuiRadio: RadioClassKey; + MuiRating: RatingClassKey; MuiScopedCssBaseline: ScopedCssBaselineClassKey; MuiSelect: SelectClassKey; MuiSlider: SliderClassKey; diff --git a/packages/material-ui/src/styles/props.d.ts b/packages/material-ui/src/styles/props.d.ts index 73cb9aaa6370c8..7c4f6ef1f94040 100644 --- a/packages/material-ui/src/styles/props.d.ts +++ b/packages/material-ui/src/styles/props.d.ts @@ -71,6 +71,7 @@ import { PaperProps } from '../Paper'; import { PopoverProps } from '../Popover'; import { RadioGroupProps } from '../RadioGroup'; import { RadioProps } from '../Radio'; +import { RatingProps } from '../Rating'; import { ScopedCssBaselineProps } from '../ScopedCssBaseline'; import { SelectProps } from '../Select'; import { SliderProps } from '../Slider'; @@ -181,6 +182,7 @@ export interface ComponentsPropsList { MuiPopover: PopoverProps; MuiRadio: RadioProps; MuiRadioGroup: RadioGroupProps; + MuiRating: RatingProps; MuiScopedCssBaseline: ScopedCssBaselineProps; MuiSelect: SelectProps; MuiSlider: SliderProps;