diff --git a/packages/core/src/components/forms/inputGroup.tsx b/packages/core/src/components/forms/inputGroup.tsx index 89a28c0607..0e0071c138 100644 --- a/packages/core/src/components/forms/inputGroup.tsx +++ b/packages/core/src/components/forms/inputGroup.tsx @@ -40,6 +40,16 @@ export interface InputGroupProps /** Whether this input should use large styles. */ large?: boolean; + /** + * Callback invoked when the input value changes, typically via keyboard interactions. + * + * Using this prop instead of `onChange` can help avoid common bugs in React 16 related to Event Pooling + * where developers forget to save the text value from a change event or call `event.persist()`. + * + * @see https://legacy.reactjs.org/docs/legacy-event-pooling.html + */ + onValueChange?(value: string, targetElement: HTMLInputElement | null): void; + /** Whether this input should use small styles. */ small?: boolean; @@ -66,6 +76,8 @@ export interface InputGroupState { rightElementWidth?: number; } +const NON_HTML_PROPS: Array = ["onValueChange"]; + /** * Input group component. * @@ -120,8 +132,9 @@ export class InputGroup extends AbstractPureComponent) => { + const value = event.target.value; + this.props.onChange?.(event); + this.props.onValueChange?.(value, event.target); + }; + private maybeRenderLeftElement() { const { leftElement, leftIcon } = this.props; diff --git a/packages/core/src/components/forms/numericInput.tsx b/packages/core/src/components/forms/numericInput.tsx index 35dc777050..74ca09343b 100644 --- a/packages/core/src/components/forms/numericInput.tsx +++ b/packages/core/src/components/forms/numericInput.tsx @@ -456,12 +456,12 @@ export class NumericInput extends AbstractPureComponent { - const { value } = e.target as HTMLInputElement; + private handleInputChange = (value: string) => { let nextValue = value; if (this.props.allowNumericCharactersOnly && this.didPasteEventJustOccur) { this.didPasteEventJustOccur = false; diff --git a/packages/core/test/controls/inputGroupTests.tsx b/packages/core/test/controls/inputGroupTests.tsx index 7676501c5c..439e5724df 100644 --- a/packages/core/test/controls/inputGroupTests.tsx +++ b/packages/core/test/controls/inputGroupTests.tsx @@ -99,4 +99,18 @@ describe("", () => { // value should not change because our change handler prevents it from being longer than characters assert.strictEqual(input.prop("value"), "abc"); }); + + it("supports the onValueChange callback", () => { + const initialValue = "value"; + const newValue = "new-value"; + const handleValueChange = spy(); + const inputGroup = mount(); + assert.strictEqual(inputGroup.find("input").prop("value"), initialValue); + + inputGroup + .find("input") + .simulate("change", { currentTarget: { value: newValue }, target: { value: newValue } }); + assert.isTrue(handleValueChange.calledOnce, "onValueChange not called"); + assert.isTrue(handleValueChange.calledWithMatch(newValue), `onValueChange not called with '${newValue}'`); + }); }); diff --git a/packages/docs-app/src/components/icons.tsx b/packages/docs-app/src/components/icons.tsx index c633b0d1fe..bf40f4d60d 100644 --- a/packages/docs-app/src/components/icons.tsx +++ b/packages/docs-app/src/components/icons.tsx @@ -58,8 +58,8 @@ export class Icons extends React.PureComponent { className={Classes.FILL} large={true} leftIcon="search" + onValueChange={this.handleFilterChange} placeholder="Search for icons..." - onChange={this.handleFilterChange} type="search" value={this.state.filter} /> @@ -100,10 +100,7 @@ export class Icons extends React.PureComponent { return icons.filter(icon => iconFilter(this.state.filter, icon)); } - private handleFilterChange = (e: React.SyntheticEvent) => { - const filter = (e.target as HTMLInputElement).value; - this.setState({ filter }); - }; + private handleFilterChange = (filter: string) => this.setState({ filter }); } function isIconFiltered(query: string, icon: Icon) { diff --git a/packages/docs-app/src/examples/core-examples/breadcrumbsExample.tsx b/packages/docs-app/src/examples/core-examples/breadcrumbsExample.tsx index 2cd541038e..ccb8d347fc 100644 --- a/packages/docs-app/src/examples/core-examples/breadcrumbsExample.tsx +++ b/packages/docs-app/src/examples/core-examples/breadcrumbsExample.tsx @@ -142,8 +142,5 @@ export class BreadcrumbsExample extends React.PureComponent = props => { const [text, setText] = React.useState(props.defaultValue ?? ""); - const handleChange = React.useCallback((event: React.FormEvent) => { - setText((event.target as HTMLInputElement).value); - }, []); - return ; + return ; }; diff --git a/packages/docs-app/src/examples/core-examples/fileInputExample.tsx b/packages/docs-app/src/examples/core-examples/fileInputExample.tsx index e51e37d747..6ab66d57c2 100644 --- a/packages/docs-app/src/examples/core-examples/fileInputExample.tsx +++ b/packages/docs-app/src/examples/core-examples/fileInputExample.tsx @@ -48,10 +48,10 @@ export class FileInputExample extends React.PureComponent
Props
- + - + @@ -59,13 +59,9 @@ export class FileInputExample extends React.PureComponent) => { - this.setState({ text: e.target.value }); - }; + private handleTextChange = (text: string) => this.setState({ text }); - private handleButtonTextChange = (e: React.ChangeEvent) => { - this.setState({ buttonText: e.target.value }); - }; + private handleButtonTextChange = (buttonText: string) => this.setState({ buttonText }); private handleSmallChange = handleBooleanChange(small => this.setState({ small, ...(small && { large: false }) })); diff --git a/packages/docs-app/src/examples/core-examples/tabsExample.tsx b/packages/docs-app/src/examples/core-examples/tabsExample.tsx index 7ed8fd17c2..01db2a24bd 100644 --- a/packages/docs-app/src/examples/core-examples/tabsExample.tsx +++ b/packages/docs-app/src/examples/core-examples/tabsExample.tsx @@ -145,7 +145,7 @@ export class TabsExample extends React.PureComponent} panelClassName="ember-panel" /> } /> - + );