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