diff --git a/package.json b/package.json index 86b04912..bdf6fd23 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,16 @@ "devDependencies": { "@testing-library/dom": "^7.28.1", "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.5", "@types/estree": "0.0.45", "@types/jest-in-case": "^1.0.3", + "@types/react": "^17.0.3", "is-ci": "^2.0.0", "jest-in-case": "^1.0.2", "jest-serializer-ansi": "^1.0.3", "kcd-scripts": "^7.5.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", "typescript": "^4.1.2" }, "peerDependencies": { diff --git a/src/__tests__/react/tsconfig.json b/src/__tests__/react/tsconfig.json new file mode 100644 index 00000000..014cc675 --- /dev/null +++ b/src/__tests__/react/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "react" + } +} diff --git a/src/__tests__/react/type.tsx b/src/__tests__/react/type.tsx new file mode 100644 index 00000000..917079e1 --- /dev/null +++ b/src/__tests__/react/type.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import {render, screen} from '@testing-library/react' +import userEvent from 'index' + +test('trigger onChange SyntheticEvent on input', () => { + const inputHandler = jest.fn() + const changeHandler = jest.fn() + + render() + + userEvent.type(screen.getByRole('textbox'), 'abcdef') + + expect(inputHandler).toHaveBeenCalledTimes(6) + expect(changeHandler).toHaveBeenCalledTimes(6) +}) diff --git a/src/keyboard/shared/fireInputEvent.ts b/src/keyboard/shared/fireInputEvent.ts index 97b34ac6..4379bedb 100644 --- a/src/keyboard/shared/fireInputEvent.ts +++ b/src/keyboard/shared/fireInputEvent.ts @@ -23,9 +23,11 @@ export function fireInputEvent( ) { // apply the changes before firing the input event, so that input handlers can access the altered dom and selection if (isContentEditable(element)) { - element.textContent = newValue - } else /* istanbul ignore else */ if (isElementType(element, ['input', 'textarea'])) { - element.value = newValue + applyNative(element, 'textContent', newValue) + } /* istanbul ignore else */ else if ( + isElementType(element, ['input', 'textarea']) + ) { + applyNative(element, 'value', newValue) } else { // TODO: properly type guard throw new Error('Invalid Element') @@ -48,7 +50,7 @@ function setSelectionRangeAfterInput( function setSelectionRangeAfterInputHandler( element: Element, - newValue: string + newValue: string, ) { // if we *can* change the selection start, then we will if the new value // is the same as the current value (so it wasn't programatically changed @@ -60,11 +62,41 @@ function setSelectionRangeAfterInputHandler( // don't apply this workaround on elements that don't necessarily report the visible value - e.g. number // TODO: this could probably be only applied when there is keyboardState.carryValue - const expectedValue = value === newValue || (value === '' && hasUnreliableEmptyValue(element)) - if(!expectedValue) { + const expectedValue = + value === newValue || (value === '' && hasUnreliableEmptyValue(element)) + if (!expectedValue) { // If the currentValue is different than the expected newValue and we *can* // change the selection range, than we should set it to the length of the // currentValue to ensure that the browser behavior is mimicked. setSelectionRange(element, value.length, value.length) } } + +/** + * React tracks the changes on element properties. + * This workaround tries to alter the DOM element without React noticing, + * so that it later picks up the change. + * + * @see https://github.com/facebook/react/blob/148f8e497c7d37a3c7ab99f01dec2692427272b1/packages/react-dom/src/client/inputValueTracking.js#L51-L104 + */ +function applyNative( + element: T, + propName: P, + propValue: T[P], +) { + const descriptor = Object.getOwnPropertyDescriptor(element, propName) + const nativeDescriptor = Object.getOwnPropertyDescriptor( + element.constructor.prototype, + propName, + ) + + if (descriptor && nativeDescriptor) { + Object.defineProperty(element, propName, nativeDescriptor) + } + + element[propName] = propValue + + if (descriptor) { + Object.defineProperty(element, propName, descriptor) + } +} diff --git a/tsconfig.json b/tsconfig.json index e02acdcd..93f39daa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "./node_modules/kcd-scripts/shared-tsconfig.json", "compilerOptions": { - "allowJs": true + "allowJs": true, + "esModuleInterop": true }, "include": ["./src", "./typings"] }