diff --git a/docs/src/modules/components/AppDrawerNavItem.js b/docs/src/modules/components/AppDrawerNavItem.js index a9cb7ec801d877..8ff3f5daf13422 100644 --- a/docs/src/modules/components/AppDrawerNavItem.js +++ b/docs/src/modules/components/AppDrawerNavItem.js @@ -82,9 +82,9 @@ class AppDrawerNavItem extends React.Component { return ( +
@@ -158,13 +160,7 @@ function HomeSteps(props) { />
- +
diff --git a/docs/src/pages/css-in-js/api/api.md b/docs/src/pages/css-in-js/api/api.md index baac7de8e15c7e..613e4c19e43d32 100644 --- a/docs/src/pages/css-in-js/api/api.md +++ b/docs/src/pages/css-in-js/api/api.md @@ -221,8 +221,9 @@ This `classes` object contains the name of the class names injected in the DOM. Some implementation details that might be interesting to being aware of: - It adds a `classes` property so you can override the injected class names from the outside. -- It adds an `innerRef` property so you can get a reference to the wrapped component. The usage of `innerRef` is identical to `ref`. -- It forwards *non React static* properties so this HOC is more "transparent". +- It forwards refs to the inner component. +- The `innerRef` prop is deprecated. Use `ref` instead. +- It does **not** copy over statics. For instance, it can be used to defined a `getInitialProps()` static method (next.js). #### Arguments @@ -296,7 +297,7 @@ in the render method. #### Returns -`Component`: The new component created. +`Component`: The new component created. Does forward refs to the inner component. #### Examples diff --git a/packages/material-ui-styles/src/RefHolder.js b/packages/material-ui-styles/src/RefHolder.js deleted file mode 100644 index 35c871d4d4ff35..00000000000000 --- a/packages/material-ui-styles/src/RefHolder.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -// This is a temporary component, we should be able to remove it once all our components -// implement forwardRef. -class RefHolder extends React.Component { - render() { - return this.props.children; - } -} - -RefHolder.propTypes = { - children: PropTypes.node, -}; - -export default RefHolder; diff --git a/packages/material-ui-styles/src/withStyles.js b/packages/material-ui-styles/src/withStyles.js index 8268e97f34ac6a..c8b13dbb83a5e2 100644 --- a/packages/material-ui-styles/src/withStyles.js +++ b/packages/material-ui-styles/src/withStyles.js @@ -2,9 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import warning from 'warning'; import hoistNonReactStatics from 'hoist-non-react-statics'; -import { getDisplayName } from '@material-ui/utils'; +import { chainPropTypes, getDisplayName } from '@material-ui/utils'; import makeStyles from './makeStyles'; -import RefHolder from './RefHolder'; import getThemeProps from './getThemeProps'; import useTheme from './useTheme'; @@ -67,11 +66,7 @@ const withStyles = (stylesOrCreator, options = {}) => Component => { } } - return ( - - - - ); + return ; }); WithStyles.propTypes = { @@ -80,9 +75,19 @@ const withStyles = (stylesOrCreator, options = {}) => Component => { */ classes: PropTypes.object, /** + * @deprecated * Use that property to pass a ref callback to the decorated component. */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + innerRef: chainPropTypes(PropTypes.oneOfType([PropTypes.func, PropTypes.object]), props => { + if (props.innerRef == null) { + return null; + } + + return new Error( + 'Material-UI: The `innerRef` prop is deprecated and will be removed in v5. ' + + 'Refs are now automatically forwarded to the inner component.', + ); + }), }; if (process.env.NODE_ENV !== 'production') { diff --git a/packages/material-ui-styles/src/withStyles.test.js b/packages/material-ui-styles/src/withStyles.test.js index 6a06769129ce19..dc773b7b3df612 100644 --- a/packages/material-ui-styles/src/withStyles.test.js +++ b/packages/material-ui-styles/src/withStyles.test.js @@ -7,6 +7,7 @@ import { Input } from '@material-ui/core'; import { createMount } from '@material-ui/core/test-utils'; import { isMuiElement } from '@material-ui/core/utils/reactHelpers'; import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; +import consoleErrorMock from 'test/utils/consoleErrorMock'; import StylesProvider from './StylesProvider'; import ThemeProvider from './ThemeProvider'; import withStyles from './withStyles'; @@ -38,6 +39,68 @@ describe('withStyles', () => { assert.strictEqual(isMuiElement(, ['Input']), true); }); + describe('refs', () => { + it('forwards ref to class components', () => { + // eslint-disable-next-line react/prefer-stateless-function + class TargetComponent extends React.Component { + render() { + return null; + } + } + const StyledTarget = withStyles({})(TargetComponent); + + const ref = React.createRef(); + mount( + <> + + , + ); + assert.instanceOf(ref.current, TargetComponent); + }); + + it('forwards refs to React.forwardRef types', () => { + const StyledTarget = withStyles({})( + // eslint-disable-next-line react/no-multi-comp + React.forwardRef((props, ref) =>
), + ); + + const ref = React.createRef(); + mount( + <> + + , + ); + assert.strictEqual(ref.current.nodeName, 'DIV'); + }); + + describe('innerRef', () => { + beforeEach(() => { + consoleErrorMock.spy(); + }); + + afterEach(() => { + consoleErrorMock.reset(); + PropTypes.resetWarningCache(); + }); + + it('is deprecated', () => { + const ThemedDiv = withStyles({})('div'); + + mount( + <> + + , + ); + + assert.strictEqual(consoleErrorMock.callCount(), 1); + assert.include( + consoleErrorMock.args()[0][0], + 'Warning: Failed prop type: Material-UI: The `innerRef` prop is deprecated', + ); + }); + }); + }); + it('should forward the properties', () => { const Test = props =>
{props.foo}
; Test.propTypes = { diff --git a/packages/material-ui-styles/src/withTheme.js b/packages/material-ui-styles/src/withTheme.js index 1ea9cdb26b0211..0baa514f965d2c 100644 --- a/packages/material-ui-styles/src/withTheme.js +++ b/packages/material-ui-styles/src/withTheme.js @@ -1,9 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import hoistNonReactStatics from 'hoist-non-react-statics'; -import { getDisplayName } from '@material-ui/utils'; +import { chainPropTypes, getDisplayName } from '@material-ui/utils'; import useTheme from './useTheme'; -import RefHolder from './RefHolder'; // Provide the theme object as a property to the input component. // It's an alternative API to useTheme(). @@ -21,18 +20,24 @@ const withTheme = Component => { const WithTheme = React.forwardRef(function WithTheme(props, ref) { const { innerRef, ...other } = props; const theme = useTheme(); - return ( - - - - ); + return ; }); WithTheme.propTypes = { /** + * @deprecated * Use that property to pass a ref callback to the decorated component. */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + innerRef: chainPropTypes(PropTypes.oneOfType([PropTypes.func, PropTypes.object]), props => { + if (props.innerRef == null) { + return null; + } + + return new Error( + 'Material-UI: The `innerRef` prop is deprecated and will be removed in v5. ' + + 'Refs are now automatically forwarded to the inner component.', + ); + }), }; if (process.env.NODE_ENV !== 'production') { diff --git a/packages/material-ui-styles/src/withTheme.test.js b/packages/material-ui-styles/src/withTheme.test.js index da475610632cb3..5548913222de3b 100644 --- a/packages/material-ui-styles/src/withTheme.test.js +++ b/packages/material-ui-styles/src/withTheme.test.js @@ -1,9 +1,11 @@ +/* eslint-disable react/no-multi-comp */ import React from 'react'; import { assert } from 'chai'; import { createMount } from '@material-ui/core/test-utils'; import { Input } from '@material-ui/core'; import { isMuiElement } from '@material-ui/core/utils/reactHelpers'; import PropTypes from 'prop-types'; +import consoleErrorMock from 'test/utils/consoleErrorMock'; import withTheme from './withTheme'; import ThemeProvider from './ThemeProvider'; @@ -52,6 +54,69 @@ describe('withTheme', () => { assert.strictEqual(isMuiElement(, ['Input']), true); }); + describe('refs', () => { + it('forwards ref to class components', () => { + // eslint-disable-next-line react/prefer-stateless-function + class TargetComponent extends React.Component { + render() { + return null; + } + } + const ThemedTarget = withTheme(TargetComponent); + + const ref = React.createRef(); + mount( + <> + + , + ); + + assert.instanceOf(ref.current, TargetComponent); + }); + + it('forwards refs to React.forwardRef types', () => { + const ThemedTarget = withTheme( + React.forwardRef((props, ref) =>
), + ); + + const ref = React.createRef(); + mount( + <> + + , + ); + + assert.strictEqual(ref.current.nodeName, 'DIV'); + }); + + describe('innerRef', () => { + beforeEach(() => { + consoleErrorMock.spy(); + }); + + afterEach(() => { + consoleErrorMock.reset(); + PropTypes.resetWarningCache(); + }); + + it('is deprecated', () => { + const ThemedDiv = withTheme('div'); + + mount( + <> + + , + ); + + assert.strictEqual(consoleErrorMock.callCount(), 1); + assert.include( + consoleErrorMock.args()[0][0], + 'Warning: Failed prop type: Material-UI: The `innerRef` prop is deprecated', + ); + }); + }); + }); + it('should throw is the import is invalid', () => { assert.throw( () => withTheme(undefined),