Skip to content

Commit

Permalink
fix(keyboard): maintain cursor position on controlled React input (#665)
Browse files Browse the repository at this point in the history
* test: maintain cursor position on controlled React input

* fix: maintain cursor position on controlled React input

* fix: remove obsolete call of setSelectionRange
  • Loading branch information
ph-fritsche committed Apr 29, 2021
1 parent 180a3d3 commit 03f38b9
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 14 deletions.
21 changes: 21 additions & 0 deletions 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 <input value={val} onChange={e => setVal(e.target.value)}/>
}

render(<Input initialValue="acd"/>)

;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)
})
26 changes: 12 additions & 14 deletions src/keyboard/shared/fireInputEvent.ts
Expand Up @@ -5,6 +5,7 @@ import {
hasUnreliableEmptyValue,
isContentEditable,
setSelectionRange,
getSelectionRange,
} from '../../utils'

export function fireInputEvent(
Expand Down Expand Up @@ -38,7 +39,7 @@ export function fireInputEvent(
...eventOverrides,
})

setSelectionRangeAfterInputHandler(element, newValue)
setSelectionRangeAfterInputHandler(element, newValue, newSelectionStart)
}

function setSelectionRangeAfterInput(
Expand All @@ -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)
}
}
}

Expand Down

0 comments on commit 03f38b9

Please sign in to comment.