From 03f38b9599891f9d8283b72e57904b876eae6e09 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 29 Apr 2021 13:17:31 +0200 Subject: [PATCH] fix(keyboard): maintain cursor position on controlled React input (#665) * test: maintain cursor position on controlled React input * fix: maintain cursor position on controlled React input * fix: remove obsolete call of setSelectionRange --- src/__tests__/react/keyboard.tsx | 21 +++++++++++++++++++++ src/keyboard/shared/fireInputEvent.ts | 26 ++++++++++++-------------- 2 files changed, 33 insertions(+), 14 deletions(-) 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) +}) diff --git a/src/keyboard/shared/fireInputEvent.ts b/src/keyboard/shared/fireInputEvent.ts index 4379bedb..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( @@ -38,7 +39,7 @@ export function fireInputEvent( ...eventOverrides, }) - setSelectionRangeAfterInputHandler(element, newValue) + setSelectionRangeAfterInputHandler(element, newValue, newSelectionStart) } function setSelectionRangeAfterInput( @@ -51,24 +52,21 @@ 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 - // 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 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 && 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) + } } }