Skip to content

Commit 4998d69

Browse files
committed
[styles] Forward refs in with* hocs
1 parent 8d46415 commit 4998d69

File tree

8 files changed

+171
-52
lines changed

8 files changed

+171
-52
lines changed

docs/src/modules/components/AppDrawerNavItem.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ class AppDrawerNavItem extends React.Component {
8282
return (
8383
<ListItem className={classes.itemLeaf} disableGutters {...other}>
8484
<Button
85-
component={props => (
86-
<Link naked activeClassName={classes.active} href={href} {...props} />
87-
)}
85+
component={React.forwardRef((props, ref) => (
86+
<Link naked activeClassName={classes.active} href={href} ref={ref} {...props} />
87+
))}
8888
className={clsx(classes.buttonLeaf, `depth-${depth}`)}
8989
disableRipple
9090
onClick={onClick}

docs/src/modules/components/HomeSteps.js

+10-14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ import NoSsr from '@material-ui/core/NoSsr';
1515
import Link from 'docs/src/modules/components/Link';
1616
import compose from 'docs/src/modules/utils/compose';
1717

18+
const InstallationLink = React.forwardRef((buttonProps, ref) => (
19+
<Link naked prefetch href="/getting-started/installation" ref={ref} {...buttonProps} />
20+
));
21+
22+
const UsageLink = React.forwardRef((buttonProps, ref) => (
23+
<Link naked prefetch href="/getting-started/usage" ref={ref} {...buttonProps} />
24+
));
25+
1826
const styles = theme => ({
1927
step: {
2028
border: `12px solid ${theme.palette.background.paper}`,
@@ -124,13 +132,7 @@ function HomeSteps(props) {
124132
/>
125133
</div>
126134
<Divider className={classes.divider} />
127-
<Button
128-
component={buttonProps => (
129-
<Link naked prefetch href="/getting-started/installation" {...buttonProps} />
130-
)}
131-
>
132-
{t('installButton')}
133-
</Button>
135+
<Button component={InstallationLink}>{t('installButton')}</Button>
134136
</Grid>
135137
<Grid item xs={12} md={4} className={classes.step}>
136138
<div className={classes.stepTitle}>
@@ -158,13 +160,7 @@ function HomeSteps(props) {
158160
/>
159161
</div>
160162
<Divider className={classes.divider} />
161-
<Button
162-
component={buttonProps => (
163-
<Link naked prefetch href="/getting-started/usage" {...buttonProps} />
164-
)}
165-
>
166-
{t('usageButton')}
167-
</Button>
163+
<Button component={UsageLink}>{t('usageButton')}</Button>
168164
</Grid>
169165
<Grid item xs={12} md={4} className={clsx(classes.step, classes.rightStep)}>
170166
<div className={classes.stepTitle}>

docs/src/pages/css-in-js/api/api.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,9 @@ This `classes` object contains the name of the class names injected in the DOM.
221221

222222
Some implementation details that might be interesting to being aware of:
223223
- It adds a `classes` property so you can override the injected class names from the outside.
224-
- It adds an `innerRef` property so you can get a reference to the wrapped component. The usage of `innerRef` is identical to `ref`.
225-
- It forwards *non React static* properties so this HOC is more "transparent".
224+
- It forwards refs to the inner component.
225+
- The `innerRef` prop is deprecated. Use `ref` instead.
226+
- It does **not** copy over statics.
226227
For instance, it can be used to defined a `getInitialProps()` static method (next.js).
227228

228229
#### Arguments
@@ -296,7 +297,7 @@ in the render method.
296297

297298
#### Returns
298299

299-
`Component`: The new component created.
300+
`Component`: The new component created. Does forward refs to the inner component.
300301

301302
#### Examples
302303

packages/material-ui-styles/src/RefHolder.js

-16
This file was deleted.

packages/material-ui-styles/src/withStyles.js

+13-8
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import warning from 'warning';
44
import hoistNonReactStatics from 'hoist-non-react-statics';
5-
import { getDisplayName } from '@material-ui/utils';
5+
import { chainPropTypes, getDisplayName } from '@material-ui/utils';
66
import makeStyles from './makeStyles';
7-
import RefHolder from './RefHolder';
87
import getThemeProps from './getThemeProps';
98
import useTheme from './useTheme';
109

@@ -67,11 +66,7 @@ const withStyles = (stylesOrCreator, options = {}) => Component => {
6766
}
6867
}
6968

70-
return (
71-
<RefHolder ref={ref}>
72-
<Component ref={innerRef} classes={classes} {...more} />
73-
</RefHolder>
74-
);
69+
return <Component ref={innerRef || ref} classes={classes} {...more} />;
7570
});
7671

7772
WithStyles.propTypes = {
@@ -80,9 +75,19 @@ const withStyles = (stylesOrCreator, options = {}) => Component => {
8075
*/
8176
classes: PropTypes.object,
8277
/**
78+
* @deprecated
8379
* Use that property to pass a ref callback to the decorated component.
8480
*/
85-
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
81+
innerRef: chainPropTypes(PropTypes.oneOfType([PropTypes.func, PropTypes.object]), props => {
82+
if (props.innerRef == null) {
83+
return null;
84+
}
85+
86+
return new Error(
87+
'Material-UI: The `innerRef` prop is deprecated and will be removed in v5. ' +
88+
'Refs are now automatically forwarded to the inner component.',
89+
);
90+
}),
8691
};
8792

8893
if (process.env.NODE_ENV !== 'production') {

packages/material-ui-styles/src/withStyles.test.js

+63
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Input } from '@material-ui/core';
77
import { createMount } from '@material-ui/core/test-utils';
88
import { isMuiElement } from '@material-ui/core/utils/reactHelpers';
99
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
10+
import consoleErrorMock from 'test/utils/consoleErrorMock';
1011
import StylesProvider from './StylesProvider';
1112
import ThemeProvider from './ThemeProvider';
1213
import withStyles from './withStyles';
@@ -38,6 +39,68 @@ describe('withStyles', () => {
3839
assert.strictEqual(isMuiElement(<StyledInput />, ['Input']), true);
3940
});
4041

42+
describe('refs', () => {
43+
it('forwards ref to class components', () => {
44+
// eslint-disable-next-line react/prefer-stateless-function
45+
class TargetComponent extends React.Component {
46+
render() {
47+
return null;
48+
}
49+
}
50+
const StyledTarget = withStyles({})(TargetComponent);
51+
52+
const ref = React.createRef();
53+
mount(
54+
<>
55+
<StyledTarget ref={ref} />
56+
</>,
57+
);
58+
assert.instanceOf(ref.current, TargetComponent);
59+
});
60+
61+
it('forwards refs to React.forwardRef types', () => {
62+
const StyledTarget = withStyles({})(
63+
// eslint-disable-next-line react/no-multi-comp
64+
React.forwardRef((props, ref) => <div {...props} ref={ref} />),
65+
);
66+
67+
const ref = React.createRef();
68+
mount(
69+
<>
70+
<StyledTarget ref={ref} />
71+
</>,
72+
);
73+
assert.strictEqual(ref.current.nodeName, 'DIV');
74+
});
75+
76+
describe('innerRef', () => {
77+
beforeEach(() => {
78+
consoleErrorMock.spy();
79+
});
80+
81+
afterEach(() => {
82+
consoleErrorMock.reset();
83+
PropTypes.resetWarningCache();
84+
});
85+
86+
it('is deprecated', () => {
87+
const ThemedDiv = withStyles({})('div');
88+
89+
mount(
90+
<>
91+
<ThemedDiv innerRef={React.createRef()} />
92+
</>,
93+
);
94+
95+
assert.strictEqual(consoleErrorMock.callCount(), 1);
96+
assert.include(
97+
consoleErrorMock.args()[0][0],
98+
'Warning: Failed prop type: Material-UI: The `innerRef` prop is deprecated',
99+
);
100+
});
101+
});
102+
});
103+
41104
it('should forward the properties', () => {
42105
const Test = props => <div>{props.foo}</div>;
43106
Test.propTypes = {

packages/material-ui-styles/src/withTheme.js

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import hoistNonReactStatics from 'hoist-non-react-statics';
4-
import { getDisplayName } from '@material-ui/utils';
4+
import { chainPropTypes, getDisplayName } from '@material-ui/utils';
55
import useTheme from './useTheme';
6-
import RefHolder from './RefHolder';
76

87
// Provide the theme object as a property to the input component.
98
// It's an alternative API to useTheme().
@@ -21,18 +20,24 @@ const withTheme = Component => {
2120
const WithTheme = React.forwardRef(function WithTheme(props, ref) {
2221
const { innerRef, ...other } = props;
2322
const theme = useTheme();
24-
return (
25-
<RefHolder ref={ref}>
26-
<Component theme={theme} ref={innerRef} {...other} />
27-
</RefHolder>
28-
);
23+
return <Component theme={theme} ref={innerRef || ref} {...other} />;
2924
});
3025

3126
WithTheme.propTypes = {
3227
/**
28+
* @deprecated
3329
* Use that property to pass a ref callback to the decorated component.
3430
*/
35-
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
31+
innerRef: chainPropTypes(PropTypes.oneOfType([PropTypes.func, PropTypes.object]), props => {
32+
if (props.innerRef == null) {
33+
return null;
34+
}
35+
36+
return new Error(
37+
'Material-UI: The `innerRef` prop is deprecated and will be removed in v5. ' +
38+
'Refs are now automatically forwarded to the inner component.',
39+
);
40+
}),
3641
};
3742

3843
if (process.env.NODE_ENV !== 'production') {

packages/material-ui-styles/src/withTheme.test.js

+65
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
/* eslint-disable react/no-multi-comp */
12
import React from 'react';
23
import { assert } from 'chai';
34
import { createMount } from '@material-ui/core/test-utils';
45
import { Input } from '@material-ui/core';
56
import { isMuiElement } from '@material-ui/core/utils/reactHelpers';
67
import PropTypes from 'prop-types';
8+
import consoleErrorMock from 'test/utils/consoleErrorMock';
79
import withTheme from './withTheme';
810
import ThemeProvider from './ThemeProvider';
911

@@ -52,6 +54,69 @@ describe('withTheme', () => {
5254
assert.strictEqual(isMuiElement(<ThemedInput />, ['Input']), true);
5355
});
5456

57+
describe('refs', () => {
58+
it('forwards ref to class components', () => {
59+
// eslint-disable-next-line react/prefer-stateless-function
60+
class TargetComponent extends React.Component {
61+
render() {
62+
return null;
63+
}
64+
}
65+
const ThemedTarget = withTheme(TargetComponent);
66+
67+
const ref = React.createRef();
68+
mount(
69+
<>
70+
<ThemedTarget ref={ref} />
71+
</>,
72+
);
73+
74+
assert.instanceOf(ref.current, TargetComponent);
75+
});
76+
77+
it('forwards refs to React.forwardRef types', () => {
78+
const ThemedTarget = withTheme(
79+
React.forwardRef((props, ref) => <div {...props} ref={ref} />),
80+
);
81+
82+
const ref = React.createRef();
83+
mount(
84+
<>
85+
<ThemedTarget ref={ref} />
86+
</>,
87+
);
88+
89+
assert.strictEqual(ref.current.nodeName, 'DIV');
90+
});
91+
92+
describe('innerRef', () => {
93+
beforeEach(() => {
94+
consoleErrorMock.spy();
95+
});
96+
97+
afterEach(() => {
98+
consoleErrorMock.reset();
99+
PropTypes.resetWarningCache();
100+
});
101+
102+
it('is deprecated', () => {
103+
const ThemedDiv = withTheme('div');
104+
105+
mount(
106+
<>
107+
<ThemedDiv innerRef={React.createRef()} />
108+
</>,
109+
);
110+
111+
assert.strictEqual(consoleErrorMock.callCount(), 1);
112+
assert.include(
113+
consoleErrorMock.args()[0][0],
114+
'Warning: Failed prop type: Material-UI: The `innerRef` prop is deprecated',
115+
);
116+
});
117+
});
118+
});
119+
55120
it('should throw is the import is invalid', () => {
56121
assert.throw(
57122
() => withTheme(undefined),

0 commit comments

Comments
 (0)