From 99fab5ad2578e5e4901fc12db93606ddf3928344 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Tue, 27 Apr 2021 14:16:32 +0200 Subject: [PATCH 1/3] test: maintain cursor position on controlled React input --- src/__tests__/react/keyboard.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/__tests__/react/keyboard.tsx diff --git a/src/__tests__/react/keyboard.tsx b/src/__tests__/react/keyboard.tsx new file mode 100644 index 00000000..29f8df22 --- /dev/null +++ b/src/__tests__/react/keyboard.tsx @@ -0,0 +1,21 @@ +import React, { useState } from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from 'index' + +test('maintain cursor position on controlled input', () => { + function Input({initialValue}: {initialValue: string}) { + const [val, setVal] = useState(initialValue) + + return setVal(e.target.value)}/> + } + + render() + + ;screen.getByRole('textbox').focus() + ;(screen.getByRole('textbox') as HTMLInputElement).setSelectionRange(1,1) + userEvent.keyboard('b') + + expect(screen.getByRole('textbox')).toHaveValue('abcd') + expect(screen.getByRole('textbox')).toHaveProperty('selectionStart', 2) + expect(screen.getByRole('textbox')).toHaveProperty('selectionEnd', 2) +}) From 8003c10abe0450800893d1d94e743fc4a96534c9 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Tue, 27 Apr 2021 14:33:58 +0200 Subject: [PATCH 2/3] fix: maintain cursor position on controlled React input --- src/keyboard/shared/fireInputEvent.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/keyboard/shared/fireInputEvent.ts b/src/keyboard/shared/fireInputEvent.ts index 4379bedb..01c0daac 100644 --- a/src/keyboard/shared/fireInputEvent.ts +++ b/src/keyboard/shared/fireInputEvent.ts @@ -38,7 +38,7 @@ export function fireInputEvent( ...eventOverrides, }) - setSelectionRangeAfterInputHandler(element, newValue) + setSelectionRangeAfterInputHandler(element, newValue, newSelectionStart) } function setSelectionRangeAfterInput( @@ -51,6 +51,7 @@ function setSelectionRangeAfterInput( function setSelectionRangeAfterInputHandler( element: Element, newValue: string, + newSelectionStart: number, ) { // 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 @@ -62,13 +63,16 @@ 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) { - // 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) + const isUnreliableValue = value === '' && hasUnreliableEmptyValue(element) + + if (!isUnreliableValue) { + const isExpectedValue = value === newValue + + // If the value was changed in the input handler, + // mimic the browser behavior and move the cursor to the end + const newCursor = isExpectedValue ? newSelectionStart : value.length + + setSelectionRange(element, newCursor, newCursor) } } From 847863ab3ae4f81140c9d2ff176991402f5137db Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Tue, 27 Apr 2021 15:28:41 +0200 Subject: [PATCH 3/3] fix: remove obsolete call of setSelectionRange --- src/keyboard/shared/fireInputEvent.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/keyboard/shared/fireInputEvent.ts b/src/keyboard/shared/fireInputEvent.ts index 01c0daac..c1a7e801 100644 --- a/src/keyboard/shared/fireInputEvent.ts +++ b/src/keyboard/shared/fireInputEvent.ts @@ -5,6 +5,7 @@ import { hasUnreliableEmptyValue, isContentEditable, setSelectionRange, + getSelectionRange, } from '../../utils' export function fireInputEvent( @@ -53,26 +54,19 @@ function setSelectionRangeAfterInputHandler( newValue: string, newSelectionStart: number, ) { - // 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 - // when the fireEvent.input was triggered). - // The reason we have to do this at all is because it actually *is* - // programmatically changed by fireEvent.input, so we have to simulate the - // browser's default behavior const value = getValue(element) as string // 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 isUnreliableValue = value === '' && hasUnreliableEmptyValue(element) - if (!isUnreliableValue) { - const isExpectedValue = value === newValue - - // If the value was changed in the input handler, - // mimic the browser behavior and move the cursor to the end - const newCursor = isExpectedValue ? newSelectionStart : value.length - - setSelectionRange(element, newCursor, newCursor) + if (!isUnreliableValue && value === newValue) { + const {selectionStart} = getSelectionRange(element) + if (selectionStart === value.length) { + // The value was changed as expected, but the cursor was moved to the end + // TODO: this could probably be only applied when we work around a framework setter on the element in applyNative + setSelectionRange(element, newSelectionStart, newSelectionStart) + } } }