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

feat: support maxLength prop in web #505

Merged
merged 13 commits into from
Oct 7, 2024

Conversation

dominictb
Copy link
Contributor

Details

This PR includes the implementation of maxLength prop in web

Related Issues

Expensify/App#46766

Manual Tests

  1. Paste this text file (over 400k characters)
    default.txt

  2. Check that the text input in both web and native is cut off to 30k length

Linked PRs

Pending App PR

@Skalakid
Copy link
Collaborator

Hello, I'm testing your changes and I found couple of bugs:

  1. The input value isn't matching the input content. The value (in the picture the text under the input) should be the same as the text inside the component

Screenshot 2024-09-26 at 12 14 49

@Skalakid
Copy link
Collaborator

  1. There are some problems with entering text when we reach the maxLength limit. Steps:
  • move the cursor to the end of the text (on the maxLength index)
  • start typing
  • notice that the old text is being replaced
Screen.Recording.2024-09-26.at.12.16.40.mov

@Skalakid
Copy link
Collaborator

  1. Pasting text at the end of the maxLength limit is replacing previous text:
  • go to the end of the text
  • copy previous text
  • start pasting it
  • notice that the value is changing
Screen.Recording.2024-09-26.at.12.27.45.mov

@dominictb
Copy link
Contributor Author

@Skalakid in the latest commit, I have made themaxLength behavior consistent between web and native, which include:

  • For normal input event type, the text input won't change the state (value and cursor position) if the text length is larger than limit
  • For paste event, the final text value will be cut off.

To test the behavior, please modify the maxLength in the example App to small value (like 20) to test

For maxLength=30000, we can see that the performance is significantly improved with this change #504

@dominictb
Copy link
Contributor Author

Small change on the latest commit: I tested the paste text in native, and it seems like the app will cut off the inserted text until it fits the maxLength, or do not paste anything at all (keep the old text), so I updated the web implementation to reflect that.

@Skalakid
Copy link
Collaborator

Skalakid commented Sep 26, 2024

Okay, thank you. Now everything works fine :D

There is one more thing that I wanted to fix there. How about adding value cutting on the first rerender? Currently, when you pass the long text in the value prop and the app loads, the maxLength prop isn't cutting the input value:

Screenshot 2024-09-26 at 13 22 47

(In the image, I have maxLength={5} and default text passed in the value prop)

I know the same thing happens on the default RN TextInput, but I think it's worth fixing in our case. What do you think about it?

@dominictb
Copy link
Contributor Author

dominictb commented Sep 27, 2024

@Skalakid the last 2 commits contain this change as you suggested. Tested on web, Android and iOS.

@Skalakid
Copy link
Collaborator

Bug: Text isn't being replaced when you reach the limit of maxLength prop

Screen.Recording.2024-09-27.at.13.37.50.mov

Expected behavior:

Screen.Recording.2024-09-27.at.13.45.04.mov

@dominictb
Copy link
Contributor Author

@Skalakid fixed!

@Skalakid Skalakid self-requested a review September 30, 2024 09:27
Copy link
Collaborator

@Skalakid Skalakid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, it looks good :D Left some small comments

inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, inputType, contentSelection.current.start),
);

if (pasteContent.current) {
pasteContent.current = null;
}

if (typeof maxLength === 'number' && parsedText.length > maxLength) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (typeof maxLength === 'number' && parsedText.length > maxLength) {
if (maxLength !== undefined && parsedText.length > maxLength) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

let availableLength = text.length;
const prefix = divRef.current.value.substring(0, contentSelection.current.start);
const suffix = divRef.current.value.substring(contentSelection.current.end);
if (typeof maxLength === 'number') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

const IS_FABRIC = 'nativeFabricUIManager' in global;

const markdownStyle = React.useMemo(() => processMarkdownStyle(props.markdownStyle), [props.markdownStyle]);

const processedText = React.useMemo(() => {
if (typeof maxLength === 'number') {
Copy link
Collaborator

@Skalakid Skalakid Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (typeof maxLength === 'number') {
if (maxLength !== undefined) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}, [value, maxLength]);

React.useLayoutEffect(() => {
if (typeof maxLength !== 'number' || !value || value.length <= maxLength || !onChangeText) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (typeof maxLength !== 'number' || !value || value.length <= maxLength || !onChangeText) {
if (maxLength === undefined || !value || value.length <= maxLength || !onChangeText) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Skalakid
Skalakid previously approved these changes Oct 1, 2024
Copy link
Collaborator

@Skalakid Skalakid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines 65 to 66
value={processedText}
onChangeText={onChangeText}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the component was supposed to be uncontrolled? I don't really like this logic here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you suggest otherwise? For me it looks completely fine. The intention is to cut off the initial value props if it is provided, and if possible, use onChangeText to update the state in the parent component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the component was supposed to be uncontrolled then the value props should be undefined, hence we don't have to worry about this case.

@dominictb
Copy link
Contributor Author

@tomekzaw do you have any further concerns? Could we merge this PR?

@tomekzaw
Copy link
Collaborator

tomekzaw commented Oct 3, 2024

@dominictb Why can't we just pass maxLength prop to TextInput in the native implementation?

@dominictb
Copy link
Contributor Author

@tomekzaw it's coming from here: #505 (comment). Basically for value > maxLength, native component will just render the whole value without cutting off anything, but the user couldn't edit the text box unless they delete a portion of text so that the length is below the limit.

Now we have 2 options:

  • Keep the behavior of the native TextInput, and implement the web to follow that.
  • Update implementation on both web and native to cut off the value text if it exceeds maxLength

@Skalakid favors the second option, so I need to update the native implementation accordingly

@tomekzaw
Copy link
Collaborator

tomekzaw commented Oct 3, 2024

@dominictb Thanks for the response

Basically for value > maxLength, native component will just render the whole value without cutting off anything

Isn't it something we should fix directly in react-native? Or, alternatively, fix this inside E/App for Android and iOS? I'd like MarkdownTextInput to mimic TextInput as closely as possible. So the behavior of maxLength prop should be analogous for TextInput and MarkdownTextInput. If the implementation of maxLength for TextInput is incorrect, we should fix it either in E/App (just like we would do for a regular TextInput) or directly at the source (i.e. in react-native).

@tomekzaw
Copy link
Collaborator

tomekzaw commented Oct 3, 2024

Actually, a regular <input type="text"> on web also behaves the same way as currently – if you set maxlength="10" value="1234567890abcde", all 15 characters are visible but you can't add or remove characters (but you can select all characters and replace the contents as long as the new text is no longer than 10 characters). I think this is the expected behavior.

@dominictb
Copy link
Contributor Author

Alright, I can revert the change. One moment.

@Skalakid
Copy link
Collaborator

Skalakid commented Oct 3, 2024

Okay, @tomekzaw convinced me, let's revert changes on the native side and focus on implementing the maxLength prop on the web only. Let's make sure it will work the same as the native one and won't introduce any new bug

@tomekzaw
Copy link
Collaborator

tomekzaw commented Oct 3, 2024

Basically, maxLength prop for MarkdownTextInput should work the same way as for TextInput.

For Android and iOS, let's just pass this prop to TextInput just like we already do (via {...props}).

For web, we'll need to implement maxLength logic since we're using contenteditable which doesn't support maxlength out-of-the-box.

All other logic should be moved to E/App.

@dominictb
Copy link
Contributor Author

done!

@tomekzaw
Copy link
Collaborator

tomekzaw commented Oct 4, 2024

@dominictb Thanks!

@tomekzaw tomekzaw requested a review from Skalakid October 4, 2024 06:30
Skalakid
Skalakid previously approved these changes Oct 4, 2024
Copy link
Collaborator

@Skalakid Skalakid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dominictb
Copy link
Contributor Author

dominictb commented Oct 4, 2024

Resolved conflicts!

@dominictb
Copy link
Contributor Author

Waiting for your final say @Skalakid.

@Skalakid Skalakid merged commit e81fe7e into Expensify:main Oct 7, 2024
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants