diff --git a/packages/material-ui-lab/src/Rating/Rating.js b/packages/material-ui-lab/src/Rating/Rating.js index a89cbce9a9b0c2..e6db0c9ee472b1 100644 --- a/packages/material-ui-lab/src/Rating/Rating.js +++ b/packages/material-ui-lab/src/Rating/Rating.js @@ -3,7 +3,12 @@ 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 } from '@material-ui/core/utils'; +import { + capitalize, + useForkRef, + useIsFocusVisible, + unstable_useId as useId, +} from '@material-ui/core/utils'; import Star from '../internal/svg-icons/Star'; function clamp(value, min, max) { @@ -160,14 +165,7 @@ const Rating = React.forwardRef(function Rating(props, ref) { ...other } = props; - const [defaultName, setDefaultName] = React.useState(); - const name = nameProp || defaultName; - React.useEffect(() => { - // Fallback to this default id when possible. - // Use the random value for client-side rendering only. - // We can't use it server-side. - setDefaultName(`mui-rating-${Math.round(Math.random() * 1e5)}`); - }, []); + const name = useId(nameProp); const { current: isControlled } = React.useRef(valueProp !== undefined); const [valueState, setValueState] = React.useState(defaultValue); diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js index 6ab214b589c167..3cb1ca0c548371 100644 --- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js @@ -1,7 +1,12 @@ /* eslint-disable no-constant-condition */ import * as React from 'react'; import PropTypes from 'prop-types'; -import { setRef, useEventCallback, useControlled } from '@material-ui/core/utils'; +import { + setRef, + useEventCallback, + useControlled, + unstable_useId as useId, +} from '@material-ui/core/utils'; // https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript // Give up on IE 11 support for this feature @@ -103,14 +108,7 @@ export default function useAutocomplete(props) { value: valueProp, } = props; - const [defaultId, setDefaultId] = React.useState(); - const id = idProp || defaultId; - React.useEffect(() => { - // Fallback to this default id when possible. - // Use the random value for client-side rendering only. - // We can't use it server-side. - setDefaultId(`mui-autocomplete-${Math.round(Math.random() * 1e5)}`); - }, []); + const id = useId(idProp); const ignoreFocus = React.useRef(false); const firstFocus = React.useRef(true); diff --git a/packages/material-ui/src/RadioGroup/RadioGroup.js b/packages/material-ui/src/RadioGroup/RadioGroup.js index 29ddee6a9a63b6..e1990463b62ad2 100644 --- a/packages/material-ui/src/RadioGroup/RadioGroup.js +++ b/packages/material-ui/src/RadioGroup/RadioGroup.js @@ -4,6 +4,7 @@ import FormGroup from '../FormGroup'; import useForkRef from '../utils/useForkRef'; import useControlled from '../utils/useControlled'; import RadioGroupContext from './RadioGroupContext'; +import useId from '../utils/unstable_useId'; const RadioGroup = React.forwardRef(function RadioGroup(props, ref) { const { @@ -52,14 +53,7 @@ const RadioGroup = React.forwardRef(function RadioGroup(props, ref) { } }; - const [defaultName, setDefaultName] = React.useState(); - const name = nameProp || defaultName; - React.useEffect(() => { - // Fallback to this default name when possible. - // Use the random value for client-side rendering only. - // We can't use it server-side. - setDefaultName(`mui-radiogroup-${Math.round(Math.random() * 1e5)}`); - }, []); + const name = useId(nameProp); return ( diff --git a/packages/material-ui/src/RadioGroup/RadioGroup.test.js b/packages/material-ui/src/RadioGroup/RadioGroup.test.js index 46ad01337d2210..803f60e5e29fe0 100644 --- a/packages/material-ui/src/RadioGroup/RadioGroup.test.js +++ b/packages/material-ui/src/RadioGroup/RadioGroup.test.js @@ -93,8 +93,8 @@ describe('', () => { , ); - assert.match(findRadio(wrapper, 'zero').props().name, /^mui-radiogroup-[0-9]+/); - assert.match(findRadio(wrapper, 'one').props().name, /^mui-radiogroup-[0-9]+/); + assert.match(findRadio(wrapper, 'zero').props().name, /^mui-[0-9]+/); + assert.match(findRadio(wrapper, 'one').props().name, /^mui-[0-9]+/); }); describe('imperative focus()', () => { @@ -306,7 +306,7 @@ describe('', () => { const radioGroupRef = React.createRef(); const { setProps } = render(); - expect(radioGroupRef.current.name).to.match(/^mui-radiogroup-[0-9]+/); + expect(radioGroupRef.current.name).to.match(/^mui-[0-9]+/); setProps({ name: 'anotherGroup' }); expect(radioGroupRef.current).to.have.property('name', 'anotherGroup'); diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js index d93060f96371b9..da40b014f41254 100644 --- a/packages/material-ui/src/Tooltip/Tooltip.js +++ b/packages/material-ui/src/Tooltip/Tooltip.js @@ -9,6 +9,7 @@ import capitalize from '../utils/capitalize'; import Grow from '../Grow'; import Popper from '../Popper'; import useForkRef from '../utils/useForkRef'; +import useId from '../utils/unstable_useId'; import setRef from '../utils/setRef'; import useIsFocusVisible from '../utils/useIsFocusVisible'; import useControlled from '../utils/useControlled'; @@ -245,18 +246,7 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) { }, [title, childNode, isControlled]); } - const [defaultId, setDefaultId] = React.useState(); - const id = idProp || defaultId; - React.useEffect(() => { - if (!open || defaultId) { - return; - } - - // Fallback to this default id when possible. - // Use the random value for client-side rendering only. - // We can't use it server-side. - setDefaultId(`mui-tooltip-${Math.round(Math.random() * 1e5)}`); - }, [open, defaultId]); + const id = useId(idProp); React.useEffect(() => { return () => { diff --git a/packages/material-ui/src/utils/index.js b/packages/material-ui/src/utils/index.js index 43d0be386eb1ea..915b1c19697571 100644 --- a/packages/material-ui/src/utils/index.js +++ b/packages/material-ui/src/utils/index.js @@ -12,4 +12,6 @@ export { default as unsupportedProp } from './unsupportedProp'; export { default as useControlled } from './useControlled'; export { default as useEventCallback } from './useEventCallback'; export { default as useForkRef } from './useForkRef'; +// eslint-disable-next-line camelcase +export { default as unstable_useId } from './unstable_useId'; export { default as useIsFocusVisible } from './useIsFocusVisible'; diff --git a/packages/material-ui/src/utils/unstable_useId.js b/packages/material-ui/src/utils/unstable_useId.js new file mode 100644 index 00000000000000..93ecd59564b5f7 --- /dev/null +++ b/packages/material-ui/src/utils/unstable_useId.js @@ -0,0 +1,18 @@ +import * as React from 'react'; + +/** + * Private module reserved for @material-ui/x packages. + */ +export default function useId(idOverride) { + const [defaultId, setDefaultId] = React.useState(idOverride); + const id = idOverride || defaultId; + React.useEffect(() => { + if (defaultId == null) { + // Fallback to this default id when possible. + // Use the random value for client-side rendering only. + // We can't use it server-side. + setDefaultId(`mui-${Math.round(Math.random() * 1e5)}`); + } + }, [defaultId]); + return id; +} diff --git a/packages/material-ui/src/utils/unstable_useId.test.js b/packages/material-ui/src/utils/unstable_useId.test.js new file mode 100644 index 00000000000000..ed2422e4d5bae4 --- /dev/null +++ b/packages/material-ui/src/utils/unstable_useId.test.js @@ -0,0 +1,36 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { expect } from 'chai'; +import { createClientRender } from 'test/utils/createClientRender'; +import useId from './unstable_useId'; + +const TestComponent = ({ id: idProp }) => { + const id = useId(idProp); + return {id}; +}; + +TestComponent.propTypes = { + id: PropTypes.string, +}; + +describe('unstable_useId', () => { + const render = createClientRender(); + + it('returns the provided ID', () => { + const { getByText, setProps } = render(); + + expect(getByText('some-id')).to.not.be.null; + + setProps({ id: 'another-id' }); + expect(getByText('another-id')).to.not.be.null; + }); + + it("generates an ID if one isn't provided", () => { + const { getByText, setProps } = render(); + + expect(getByText(/^mui-[0-9]+$/)).to.not.be.null; + + setProps({ id: 'another-id' }); + expect(getByText('another-id')).to.not.be.null; + }); +});