necolas/react-native-web

TextInput: autoexpandable

gut4 opened this issue ยท 12 comments

gut4 commented

In react-native TextInput is always autoexpandable. See facebook/react-native@dabb78b

I think simplest way to implement this is to use https://github.com/rpearce/react-expanding-textarea
It's just update textarea height based on el.scrollHeight:

  _handleChange(e) {
    const { onChange } = this.props
    if (onChange) onChange(e)
    this._adjustTextarea(e)
  }


  _adjustTextarea({ target = this.el }) {
    target.style.height = 0
    target.style.height = `${target.scrollHeight}px`
  }
}

@gut4 There is a problem with such an implementation. It will break the window scroll occasionally. It's probably much harder to implement with fake height placeholder etc. I had to switch to contentEditable.

Here is how you do it:

import React, { useState } from 'react';
import { TextInput } from 'react-native';

export default function TextField(props) {
  const [scrollHeight, setScrollHeight] = useState(null);
  return (
    <TextInput
      style={{ height: scrollHeight }}
      onChange={(e) => setScrollHeight(e.target.scrollHeight)}
      {...props}
    />
  );
}

@woodpav This with multiline works great to increase the size of the text input, but it unfortunately doesn't shrink it when you remove text.

Using the solution proposed by @gut4 worked in both direction for me.

import * as React from 'react';
import { TextInput } from 'react-native';

export default function App() {
  
  const _handleChange = (e) => {
	e.target.style.height = 0
	e.target.style.height = `${e.target.scrollHeight}px`
  };

  return (
    <TextInput
      multiline
      onChange={_handleChange}
    />
  );
}


I'm curious if this approach translates across platforms, i.e. can we rely on scrollHeight to be exposed and updated immediately when e.target.style.height is set to zero? My approach is slightly more involved, measuring a hidden Text element to detect shrinking heights, i.e.:

const [height, setHeight] = useState(props.initialHeight);

return(
    <View>
        <TextInput 
            {...props} 
            style={[props.style, {height: height}]}
            onContentSizeChange={e => setHeight(e.nativeEvent.contentSize.height)}
        />
        <Text 
            onLayout={e => e.nativeEvent.layout.height < height && setHeight(e.nativeEvent.layout.height)} 
            style={[props.style, {position: 'absolute', visibility: 'hidden'}]} 
            pointerEvents={'none'}
        >
            {`${props.value || props.defaultValue || props.placeholder || ''}\n`}
        </Text>
    </View>
)

@necolas Are there any plans getting this natively supported by react native web at some point?

Could we use https://www.npmjs.com/package/react-textarea-autosize or a similar approach to get closer to the behaviour of the iOS/Android counterparts of TextInput with multiline enabled?

Ya I have changed the handleContentSizeChange function in dist/exports/TextInput. I want this to happen automatically for all multiline TextInputs in my app. Working for me. Thanks @divonelnc!

  var handleContentSizeChange = React.useCallback(function (hostNode) {
    if (multiline && hostNode != null) {
        const newHeight = hostNode.scrollHeight
        const newWidth = hostNode.scrollWidth;

        hostNode.style.height = 0;
        hostNode.style.height = `${hostNode.scrollHeight}px`;

      if (onContentSizeChange && (newHeight !== dimensions.current.height || newWidth !== dimensions.current.width)) {
        dimensions.current.height = newHeight;
        dimensions.current.width = newWidth;
        onContentSizeChange({
          nativeEvent: {
            contentSize: {
              height: dimensions.current.height,
              width: dimensions.current.width
            }
          }
        });
      }
    }
  }, [multiline, onContentSizeChange]);

@divonelnc's onChange workaround was almost perfect for me, except that I noticed that onChange() is only called when text is changed by typing, not when the TextInput's value is changed programatically. In our example, the TextInput expands and contracts as text is typed and deleted, but stays expanded after the value is cleared when sending a message in our chat interface is completed. Curious if anyone has any ideas to try. Thank you!

Somehow I can't believe this issue is lingering since 2017 (the non-autogrowth of TextEdit in Web), and somehow it's still not part of the core package.

What needs to be done to have this behaviour baked in the the core release?

@woodpav your answer fails to account for border width.

import React, { useState } from 'react';
import { TextInput } from 'react-native';

export default function TextField(props) {
  const [scrollHeight, setScrollHeight] = useState(null);
  return (
    <TextInput
      style={{ height: scrollHeight }}
      onChange={(e) => setScrollHeight(e.target.offsetHeight - e.target.clientHeight + e.target.scrollHeight)}
      {...props}
    />
  );
}

I've taken what's here and adapted it for my own use-case. It seems to work well, resizing upon initialization and any subsequent changes:

import * as React from 'react';
import { TextInput } from 'react-native';

export default function App() {
  
  const adjustTextInputSize = (evt) => {
    const el = evt?.target || evt?.nativeEvent?.target;
    if (el) {
      el.style.height = 0;
      const newHeight = el.offsetHeight - el.clientHeight + el.scrollHeight;
      el.style.height = `${newHeight}px`;
    }
  };

  return (
    <TextInput
      multiline
      onChange={adjustTextInputSize}
      onLayout={adjustTextInputSize}
    />
  );
}

@willstepp, your solution works perfectly, but I am struggling with one case on the web expo app.
I use onSubmitEditing method in TextInput, and after pressing the "Enter" button, TextInput doesn't resize.
Don't you have any ideas on how to handle it?

Screen.Recording.2023-06-06.at.21.41.33.mov
aryo commented

@arhipy97 you can extract this part into a fn:

const resetHeight = el => {
  if (el) {
    el.style.height = 0;
    const newHeight = el.offsetHeight - el.clientHeight + el.scrollHeight;
    el.style.height = `${newHeight}px`;
  }
}

and pass in a ref to the TextInput so you can either hook into the onSubmit or use an effect to reset it:

useEffect(() => {
  if (!value) {
    resetHeight(inputRef.current);
  }
}, [value]);