Skip to content

Commit

Permalink
async control TextArea component
Browse files Browse the repository at this point in the history
  • Loading branch information
nurseiit committed Aug 1, 2023
1 parent 0953533 commit 93f9f0c
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 30 deletions.
47 changes: 27 additions & 20 deletions packages/core/src/components/forms/asyncControllableInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ import * as React from "react";

import { AbstractPureComponent, DISPLAYNAME_PREFIX } from "../../common";

export type AsyncControllableInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
inputRef?: React.Ref<HTMLInputElement>;
type AsyncControllableElement = HTMLInputElement | HTMLTextAreaElement;

export type AsyncControllableInputProps = React.InputHTMLAttributes<AsyncControllableElement> & {
inputRef?: React.Ref<AsyncControllableElement>;

/**
* HTML tag name to use for rendered input element.
*
* @default "input"
*/
tagName?: "input" | "textarea";
};

type InputValue = AsyncControllableInputProps["value"];
Expand Down Expand Up @@ -54,7 +63,7 @@ export interface AsyncControllableInputState {
}

/**
* A stateful wrapper around the low-level <input> component which works around a
* A stateful wrapper around the low-level <input> or <textarea> components which works around a
* [React bug](https://github.com/facebook/react/issues/3926). This bug is reproduced when an input
* receives CompositionEvents (for example, through IME composition) and has its value prop updated
* asychronously. This might happen if a component chooses to do async validation of a value
Expand Down Expand Up @@ -122,29 +131,27 @@ export class AsyncControllableInput extends AbstractPureComponent<

public render() {
const { isComposing, hasPendingUpdate, value, nextValue } = this.state;
const { inputRef, ...restProps } = this.props;
return (
<input
{...restProps}
ref={inputRef}
// render the pending value even if it is not confirmed by a parent's async controlled update
// so that the cursor does not jump to the end of input as reported in
// https://github.com/palantir/blueprint/issues/4298
value={isComposing || hasPendingUpdate ? nextValue : value}
onCompositionStart={this.handleCompositionStart}
onCompositionEnd={this.handleCompositionEnd}
onChange={this.handleChange}
/>
);
const { inputRef, tagName = "input", ...restProps } = this.props;
return React.createElement(tagName, {
...restProps,
ref: inputRef,
// render the pending value even if it is not confirmed by a parent's async controlled update
// so that the cursor does not jump to the end of input as reported in
// https://github.com/palantir/blueprint/issues/4298
value: isComposing || hasPendingUpdate ? nextValue : value,
onCompositionStart: this.handleCompositionStart,
onCompositionEnd: this.handleCompositionEnd,
onChange: this.handleChange
});
}

private handleCompositionStart = (e: React.CompositionEvent<HTMLInputElement>) => {
private handleCompositionStart = (e: React.CompositionEvent<AsyncControllableElement>) => {
this.cancelPendingCompositionEnd?.();
this.setState({ isComposing: true });
this.props.onCompositionStart?.(e);
};

private handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
private handleCompositionEnd = (e: React.CompositionEvent<AsyncControllableElement>) => {
// In some non-latin languages, a keystroke can end a composition event and immediately afterwards start another.
// This can lead to unexpected characters showing up in the text input. In order to circumvent this problem, we
// use a timeout which creates a delay which merges the two composition events, creating a more natural and predictable UX.
Expand All @@ -157,7 +164,7 @@ export class AsyncControllableInput extends AbstractPureComponent<
this.props.onCompositionEnd?.(e);
};

private handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
private handleChange = (e: React.ChangeEvent<AsyncControllableElement>) => {
const { value } = e.target;

this.setState({ nextValue: value });
Expand Down
30 changes: 20 additions & 10 deletions packages/core/src/components/forms/textArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ import * as React from "react";

import { AbstractPureComponent, Classes, refHandler, setRef } from "../../common";
import { DISPLAYNAME_PREFIX, IntentProps, Props } from "../../common/props";
import { AsyncControllableInput } from "./asyncControllableInput";

export interface TextAreaProps extends IntentProps, Props, React.TextareaHTMLAttributes<HTMLTextAreaElement> {
/**
* Set this to `true` if you will be controlling the `value` of this input with asynchronous updates.
* These may occur if you do not immediately call setState in a parent component with the value from
* the `onChange` handler, or if working with certain libraries like __redux-form__.
*
* @default false
*/
asyncControl?: boolean;

/**
* Whether the component should automatically resize vertically as a user types in the text input.
* This will disable manual resizing in the vertical dimension.
Expand Down Expand Up @@ -143,7 +153,7 @@ export class TextArea extends AbstractPureComponent<TextAreaProps, TextAreaState

public render() {
// eslint-disable-next-line deprecation/deprecation
const { autoResize, className, fill, growVertically, inputRef, intent, large, small, ...htmlProps } =
const { asyncControl, autoResize, className, fill, growVertically, inputRef, intent, large, small, ...htmlProps } =
this.props;

const rootClasses = classNames(
Expand All @@ -170,15 +180,15 @@ export class TextArea extends AbstractPureComponent<TextAreaProps, TextAreaState
};
}

return (
<textarea
{...htmlProps}
className={rootClasses}
onChange={this.handleChange}
ref={this.handleRef}
style={style}
/>
);
const [element, restProps] = asyncControl ? [AsyncControllableInput, { inputRef: this.handleRef }] : ["textarea", { ref: this.handleRef }]

return React.createElement(element, {
...htmlProps,
className: rootClasses,
onChange: this.handleChange,
style,
...restProps
})
}

private handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
Expand Down

1 comment on commit 93f9f0c

@adidahiya
Copy link
Contributor

Choose a reason for hiding this comment

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

async control TextArea component

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Please sign in to comment.