diff --git a/docs/pages/api/rating.md b/docs/pages/api/rating.md index b81884e9141c3e..b12eefc381a582 100644 --- a/docs/pages/api/rating.md +++ b/docs/pages/api/rating.md @@ -27,6 +27,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | | disabled | bool | false | If `true`, the rating will be disabled. | | emptyIcon | node | | The icon to display when empty. | +| emptyLabelText | node | 'Empty' | The label read when the rating input is empty. | | getLabelText | func | function defaultLabelText(value) { return `${value} Star${value !== 1 ? 's' : ''}`;} | 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/).

**Signature:**
`function(value: number) => string`
*value:* The rating label's value to format. | | icon | node | <Star fontSize="inherit" /> | The icon to display. | | IconContainerComponent | elementType | function IconContainer(props) { const { value, ...other } = props; return <span {...other} />;} | The component containing the icon. | diff --git a/docs/src/pages/components/rating/SimpleRating.tsx b/docs/src/pages/components/rating/SimpleRating.tsx index 962133548de12c..ae3f12eb38ea56 100644 --- a/docs/src/pages/components/rating/SimpleRating.tsx +++ b/docs/src/pages/components/rating/SimpleRating.tsx @@ -4,7 +4,7 @@ import Typography from '@material-ui/core/Typography'; import Box from '@material-ui/core/Box'; export default function SimpleRating() { - const [value, setValue] = React.useState(2); + const [value, setValue] = React.useState(2); return (
diff --git a/packages/material-ui-lab/src/Rating/Rating.d.ts b/packages/material-ui-lab/src/Rating/Rating.d.ts index 2d52cb2453b603..2303e30060ff0e 100644 --- a/packages/material-ui-lab/src/Rating/Rating.d.ts +++ b/packages/material-ui-lab/src/Rating/Rating.d.ts @@ -14,7 +14,7 @@ export interface RatingProps IconContainerComponent?: React.ElementType; max?: number; name?: string; - onChange?: (event: React.ChangeEvent<{}>, value: number) => void; + onChange?: (event: React.ChangeEvent<{}>, value: number | null) => void; onChangeActive?: (event: React.ChangeEvent<{}>, value: number) => void; precision?: number; readOnly?: boolean; diff --git a/packages/material-ui-lab/src/Rating/Rating.js b/packages/material-ui-lab/src/Rating/Rating.js index d454c2481e6aad..db1db0f2b65a66 100644 --- a/packages/material-ui-lab/src/Rating/Rating.js +++ b/packages/material-ui-lab/src/Rating/Rating.js @@ -22,6 +22,10 @@ function getDecimalPrecision(num) { } function roundValueToPrecision(value, precision) { + if (value == null) { + return value; + } + const nearest = Math.round(value / precision) * precision; return Number(nearest.toFixed(getDecimalPrecision(precision))); } @@ -134,6 +138,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { className, disabled = false, emptyIcon, + emptyLabelText = 'Empty', getLabelText = defaultLabelText, icon = defaultIcon, IconContainerComponent = IconContainer, @@ -146,7 +151,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { precision = 1, readOnly = false, size = 'medium', - value: valueProp2 = null, + value: valueProp = null, ...other } = props; @@ -159,14 +164,14 @@ const Rating = React.forwardRef(function Rating(props, ref) { setDefaultName(`mui-rating-${Math.round(Math.random() * 1e5)}`); }, []); - const valueProp = roundValueToPrecision(valueProp2, precision); + const valueRounded = roundValueToPrecision(valueProp, precision); const theme = useTheme(); const [{ hover, focus }, setState] = React.useState({ hover: -1, focus: -1, }); - let value = valueProp; + let value = valueRounded; if (hover !== -1) { value = hover; } @@ -188,7 +193,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { const rootNode = rootRef.current; const { right, left } = rootNode.getBoundingClientRect(); - const { width } = rootNode.firstChild.getBoundingClientRect(); + const { width } = rootNode.querySelector(`.${classes.label}`).getBoundingClientRect(); let percent; if (theme.direction === 'rtl') { @@ -238,6 +243,23 @@ const Rating = React.forwardRef(function Rating(props, ref) { } }; + 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, + }); + + if (onChange && parseFloat(event.target.value) === valueRounded) { + onChange(event, null); + } + }; + const handleFocus = event => { if (isFocusVisible(event)) { setFocusVisible(true); @@ -306,6 +328,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { onFocus={handleFocus} onBlur={handleBlur} onChange={handleChange} + onClick={handleClear} value={propsItem.value} id={id} type="radio" @@ -336,18 +359,18 @@ const Rating = React.forwardRef(function Rating(props, ref) { aria-label={readOnly ? getLabelText(value) : null} {...other} > - {!readOnly && !disabled && value == null && ( + {!readOnly && !disabled && valueRounded == null && ( - )} @@ -390,7 +413,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { filled: itemDecimalValue <= value, hover: itemDecimalValue <= hover, focus: itemDecimalValue <= focus, - checked: itemDecimalValue === valueProp, + checked: itemDecimalValue === valueRounded, }, ); })} @@ -407,7 +430,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { filled: itemValue <= value, hover: itemValue <= hover, focus: itemValue <= focus, - checked: itemValue === valueProp, + checked: itemValue === valueRounded, }, ); })} @@ -433,6 +456,10 @@ Rating.propTypes = { * The icon to display when empty. */ emptyIcon: PropTypes.node, + /** + * The label read when the rating input is 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. * diff --git a/packages/material-ui-lab/src/Rating/Rating.test.js b/packages/material-ui-lab/src/Rating/Rating.test.js index 17baf3b1b6184b..b1a0b6b9ac624f 100644 --- a/packages/material-ui-lab/src/Rating/Rating.test.js +++ b/packages/material-ui-lab/src/Rating/Rating.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { expect } from 'chai'; -import { stub } from 'sinon'; +import { stub, spy } from 'sinon'; import { createMount, getClasses } from '@material-ui/core/test-utils'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; import { createClientRender, fireEvent } from 'test/utils/createClientRender'; @@ -66,4 +66,36 @@ describe('', () => { }); expect(container.querySelectorAll(`.${classes.iconHover}`).length).to.equal(2); }); + + it('should clear the rating', () => { + const handleChange = spy(); + const { getByLabelText } = render(); + + const input = getByLabelText('2 Stars'); + fireEvent.click(input, { + clientX: 1, + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal(null); + }); + + it('should select the rating', () => { + const handleChange = spy(); + const { getByLabelText } = render(); + + const input = getByLabelText('3 Stars'); + fireEvent.click(input); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal(3); + }); + + it('should select the empty input if value is null', () => { + const { container, getByLabelText } = render(); + const input = getByLabelText('Empty'); + const checked = container.querySelector('input[name="rating-test"]:checked'); + expect(input).to.equal(checked); + expect(input.value).to.equal(''); + }); }); diff --git a/packages/material-ui/src/locale/index.js b/packages/material-ui/src/locale/index.js index 6b73984cb0ee3c..88186e8005b087 100644 --- a/packages/material-ui/src/locale/index.js +++ b/packages/material-ui/src/locale/index.js @@ -17,6 +17,7 @@ export const azAZ = { return `${value} ${pluralForm}`; }, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Silmək', @@ -46,6 +47,7 @@ export const csCZ = { } return `${value} hvězdiček`; }, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Vymazat', @@ -67,6 +69,7 @@ export const deDE = { }, MuiRating: { getLabelText: value => `${value} ${value !== 1 ? 'Sterne' : 'Stern'}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Leeren', @@ -91,6 +94,7 @@ export const enUS = {}; }, MuiRating: { getLabelText: value => `${value} Star${value !== 1 ? 's' : ''}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Clear', @@ -112,6 +116,7 @@ export const esES = { }, MuiRating: { getLabelText: value => `${value} Estrella${value !== 1 ? 's' : ''}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Limpiar', @@ -133,6 +138,7 @@ export const faIR = { }, MuiRating: { getLabelText: value => `${value} ستاره`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'پاک‌کردن', @@ -154,6 +160,7 @@ export const frFR = { }, MuiRating: { getLabelText: value => `${value} Etoile${value !== 1 ? 's' : ''}`, + emptyLabelText: 'Vide', }, MuiAutocomplete: { clearText: 'Vider', @@ -176,6 +183,7 @@ export const idID = { }, MuiRating: { getLabelText: value => `${value} Bintang`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Hapus', @@ -197,6 +205,7 @@ export const itIT = { }, MuiRating: { getLabelText: value => `${value} Stell${value !== 1 ? 'a' : 'e'}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Svuota', @@ -218,6 +227,7 @@ export const jaJP = { }, MuiRating: { getLabelText: value => `${value} ${value !== 1 ? '出演者' : '星'}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'クリア', @@ -239,6 +249,7 @@ export const koKR = { }, MuiRating: { getLabelText: value => `${value} 점`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: '지우기', @@ -260,6 +271,7 @@ export const nlNL = { }, MuiRating: { getLabelText: value => `${value} Ster${value !== 1 ? 'ren' : ''}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Wissen', @@ -292,6 +304,7 @@ export const plPL = { return `${value} ${pluralForm}`; }, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Wyczyść', @@ -313,6 +326,7 @@ export const ptBR = { }, MuiRating: { getLabelText: value => `${value} Estrela${value !== 1 ? 's' : ''}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Limpar', @@ -334,6 +348,7 @@ export const ptPT = { }, MuiRating: { getLabelText: value => `${value} Estrela${value !== 1 ? 's' : ''}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Limpar', @@ -355,6 +370,7 @@ export const roRO = { }, MuiRating: { getLabelText: value => `${value} St${value !== 1 ? 'ele' : 'ea'}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Șterge', @@ -387,6 +403,7 @@ export const ruRU = { return `${value} ${pluralForm}`; }, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Очистить', @@ -416,6 +433,7 @@ export const skSK = { } return `${value} hviezdičiek`; }, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Vymazať', @@ -437,6 +455,7 @@ export const svSE = { }, MuiRating: { getLabelText: value => `${value} ${value !== 1 ? 'Stjärnor' : 'Stjärna'}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Rensa', @@ -459,6 +478,7 @@ export const trTR = { }, MuiRating: { getLabelText: value => `${value} Yıldız`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Temizle', @@ -491,6 +511,7 @@ export const ukUA = { return `${value} ${pluralForm}`; }, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: 'Очистити', @@ -512,6 +533,7 @@ export const zhCN = { }, MuiRating: { getLabelText: value => `${value} 星${value !== 1 ? '星' : ''}`, + emptyLabelText: 'Empty', }, MuiAutocomplete: { clearText: '明确',