(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 && (
-
- {getLabelText(0)}
+
+ {emptyLabelText}
)}
@@ -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: '明确',