diff --git a/packages/react-native-web/src/exports/TextInput/__tests__/index-test.js b/packages/react-native-web/src/exports/TextInput/__tests__/index-test.js
index acbe26be1b..2cc9746b51 100644
--- a/packages/react-native-web/src/exports/TextInput/__tests__/index-test.js
+++ b/packages/react-native-web/src/exports/TextInput/__tests__/index-test.js
@@ -8,7 +8,7 @@
import React from 'react';
import TextInput from '..';
import { createEventTarget } from 'dom-event-testing-library';
-import { act, render } from '@testing-library/react';
+import { act, fireEvent, render } from '@testing-library/react';
function findInput(container) {
return container.querySelector('input');
@@ -333,6 +333,53 @@ describe('components/TextInput', () => {
expect(onChange).toHaveBeenCalledTimes(1);
});
+ describe('prop "onContentSizeChange"', () => {
+ // jsdom has no layout, so report a height that only depends on the
+ // content (like a zero-height textarea would) and no width.
+ let scrollHeight;
+ beforeEach(() => {
+ scrollHeight = jest
+ .spyOn(HTMLTextAreaElement.prototype, 'scrollHeight', 'get')
+ .mockImplementation(function () {
+ return this.style.height === '0px'
+ ? this.value.split('\n').length * 10
+ : 0;
+ });
+ });
+ afterEach(() => {
+ scrollHeight.mockRestore();
+ });
+
+ test('is called when the content shrinks', () => {
+ const onContentSizeChange = jest.fn();
+ const { container } = render(
+
+ );
+ const textarea = findTextArea(container);
+ expect(onContentSizeChange).toHaveBeenCalledTimes(1);
+ expect(onContentSizeChange).toHaveBeenLastCalledWith({
+ nativeEvent: { contentSize: { height: 10, width: 0 } }
+ });
+ fireEvent.change(textarea, { target: { value: 'a\nb\nc' } });
+ expect(onContentSizeChange).toHaveBeenCalledTimes(2);
+ expect(onContentSizeChange).toHaveBeenLastCalledWith({
+ nativeEvent: { contentSize: { height: 30, width: 0 } }
+ });
+ fireEvent.change(textarea, { target: { value: 'a' } });
+ expect(onContentSizeChange).toHaveBeenCalledTimes(3);
+ expect(onContentSizeChange).toHaveBeenLastCalledWith({
+ nativeEvent: { contentSize: { height: 10, width: 0 } }
+ });
+ // the textarea itself is left untouched
+ expect(textarea.style.height).toBe('100px');
+ expect(container.querySelectorAll('textarea')).toHaveLength(1);
+ });
+ });
+
test.skip('prop "onChangeText"', () => {
const onChangeText = jest.fn();
const { container } = render();
diff --git a/packages/react-native-web/src/exports/TextInput/index.js b/packages/react-native-web/src/exports/TextInput/index.js
index 8f8de0bf0f..505220a6dd 100644
--- a/packages/react-native-web/src/exports/TextInput/index.js
+++ b/packages/react-native-web/src/exports/TextInput/index.js
@@ -91,6 +91,31 @@ function isEventComposing(nativeEvent) {
let focusTimeout: ?TimeoutID = null;
+/**
+ * The scrollHeight of a textarea is never less than its own height, so it
+ * can't be used to detect that the content has become smaller than the box.
+ * Measure a zero-height copy instead so the box the textarea has been given
+ * (and any scroll position clamped to it) is left alone.
+ */
+function measureContentSize(hostNode) {
+ const clone = hostNode.cloneNode();
+ clone.value = hostNode.value;
+ // Keep the same wrapping width, but take the copy out of the layout.
+ clone.style.width = window.getComputedStyle(hostNode).width;
+ clone.style.height = '0';
+ clone.style.minHeight = '0';
+ clone.style.maxHeight = 'none';
+ clone.style.overflow = 'hidden';
+ clone.style.position = 'fixed';
+ clone.style.top = '0';
+ clone.style.left = '0';
+ clone.style.visibility = 'hidden';
+ hostNode.after(clone);
+ const size = { height: clone.scrollHeight, width: clone.scrollWidth };
+ clone.remove();
+ return size;
+}
+
const TextInput: React.AbstractComponent<
TextInputProps,
HTMLElement & PlatformMethods
@@ -209,8 +234,8 @@ const TextInput: React.AbstractComponent<
const handleContentSizeChange = React.useCallback(
(hostNode) => {
if (multiline && onContentSizeChange && hostNode != null) {
- const newHeight = hostNode.scrollHeight;
- const newWidth = hostNode.scrollWidth;
+ const { height: newHeight, width: newWidth } =
+ measureContentSize(hostNode);
if (
newHeight !== dimensions.current.height ||
newWidth !== dimensions.current.width