Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TextareaAutosize] Fix infinite render loop #16635

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,14 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
getStyleValue(computedStyle, 'border-top-width');

// The height of the inner content
const innerHeight = inputShallow.scrollHeight;
const innerHeight = inputShallow.scrollHeight - padding;

// Measure height of a textarea with a single row
inputShallow.value = 'x';
let singleRowHeight = inputShallow.scrollHeight;
singleRowHeight -= padding;
const singleRowHeight = inputShallow.scrollHeight - padding;

// The height of the outer content
let outerHeight = innerHeight - padding;
let outerHeight = innerHeight;

if (rows != null) {
outerHeight = Math.max(Number(rows) * singleRowHeight, outerHeight);
Expand All @@ -65,15 +64,20 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
}
outerHeight = Math.max(outerHeight, singleRowHeight);

outerHeight += boxSizing === 'border-box' ? padding + border : 0;
// Take the box sizing into account for applying this value as a style.
const outerHeightStyle = outerHeight + (boxSizing === 'border-box' ? padding + border : 0);

setState(prevState => {
// Need a large enough different to update the height.
// This prevents infinite rendering loop.
if (outerHeight > 0 && Math.abs((prevState.outerHeight || 0) - outerHeight) > 1) {
if (
outerHeightStyle > 0 &&
Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1
) {
return {
innerHeight,
outerHeight,
outerHeightStyle,
};
}

Expand Down Expand Up @@ -116,7 +120,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
// Apply the rows prop to get a "correct" first SSR paint
rows={rows || 1}
style={{
height: state.outerHeight,
height: state.outerHeightStyle,
// Need a large enough different to allow scrolling.
// This prevents infinite rendering loop.
overflow: Math.abs(state.outerHeight - state.innerHeight) <= 1 ? 'hidden' : null,
Expand Down
32 changes: 19 additions & 13 deletions packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { createMount } from '@material-ui/core/test-utils';
import describeConformance from '@material-ui/core/test-utils/describeConformance';
import TextareaAutosize from './TextareaAutosize';

function getHeight(wrapper) {
function getStyle(wrapper) {
return wrapper
.find('textarea')
.at(0)
.props().style.height;
.props().style;
}

describe('<TextareaAutosize />', () => {
Expand Down Expand Up @@ -78,7 +78,10 @@ describe('<TextareaAutosize />', () => {

it('should handle the resize event', () => {
const wrapper = mount(<TextareaAutosize />);
assert.strictEqual(getHeight(wrapper), undefined);
assert.deepEqual(getStyle(wrapper), {
height: undefined,
overflow: null,
});
setLayout(wrapper, {
getComputedStyle: {
'box-sizing': 'content-box',
Expand All @@ -89,14 +92,17 @@ describe('<TextareaAutosize />', () => {
window.dispatchEvent(new window.Event('resize', {}));
clock.tick(166);
wrapper.update();
assert.strictEqual(getHeight(wrapper), 30);
assert.deepEqual(getStyle(wrapper), {
height: 30,
overflow: 'hidden',
});
});
});

it('should update when uncontrolled', () => {
const handleChange = spy();
const wrapper = mount(<TextareaAutosize onChange={handleChange} />);
assert.strictEqual(getHeight(wrapper), undefined);
assert.deepEqual(getStyle(wrapper), { height: undefined, overflow: null });
setLayout(wrapper, {
getComputedStyle: {
'box-sizing': 'content-box',
Expand All @@ -109,14 +115,14 @@ describe('<TextareaAutosize />', () => {
.at(0)
.simulate('change');
wrapper.update();
assert.strictEqual(getHeight(wrapper), 30);
assert.deepEqual(getStyle(wrapper), { height: 30, overflow: 'hidden' });
assert.strictEqual(handleChange.callCount, 1);
});

it('should take the border into account with border-box', () => {
const border = 5;
const wrapper = mount(<TextareaAutosize />);
assert.strictEqual(getHeight(wrapper), undefined);
assert.deepEqual(getStyle(wrapper), { height: undefined, overflow: null });
setLayout(wrapper, {
getComputedStyle: {
'box-sizing': 'border-box',
Expand All @@ -127,7 +133,7 @@ describe('<TextareaAutosize />', () => {
});
wrapper.setProps();
wrapper.update();
assert.strictEqual(getHeight(wrapper), 30 + border);
assert.deepEqual(getStyle(wrapper), { height: 30 + border, overflow: 'hidden' });
});

it('should take the padding into account with content-box', () => {
Expand All @@ -143,7 +149,7 @@ describe('<TextareaAutosize />', () => {
});
wrapper.setProps();
wrapper.update();
assert.strictEqual(getHeight(wrapper), 30 - padding);
assert.deepEqual(getStyle(wrapper), { height: 30 - padding, overflow: 'hidden' });
});

it('should have at least height of "rows"', () => {
Expand All @@ -159,7 +165,7 @@ describe('<TextareaAutosize />', () => {
});
wrapper.setProps();
wrapper.update();
assert.strictEqual(getHeight(wrapper), lineHeight * rows);
assert.deepEqual(getStyle(wrapper), { height: lineHeight * rows, overflow: null });
});

it('should have at max "rowsMax" rows', () => {
Expand All @@ -175,7 +181,7 @@ describe('<TextareaAutosize />', () => {
});
wrapper.setProps();
wrapper.update();
assert.strictEqual(getHeight(wrapper), lineHeight * rowsMax);
assert.deepEqual(getStyle(wrapper), { height: lineHeight * rowsMax, overflow: null });
});

it('should update its height when the "rowsMax" prop changes', () => {
Expand All @@ -190,10 +196,10 @@ describe('<TextareaAutosize />', () => {
});
wrapper.setProps();
wrapper.update();
assert.strictEqual(getHeight(wrapper), lineHeight * 3);
assert.deepEqual(getStyle(wrapper), { height: lineHeight * 3, overflow: null });
wrapper.setProps({ rowsMax: 2 });
wrapper.update();
assert.strictEqual(getHeight(wrapper), lineHeight * 2);
assert.deepEqual(getStyle(wrapper), { height: lineHeight * 2, overflow: null });
});
});
});