Skip to content

Commit

Permalink
fix(keyboard): fix fireInputEventIfNeeded (#583)
Browse files Browse the repository at this point in the history
When a text input with a single character value is selected, typing text
which starts with the same character results in the first character of
the typed text not to be typed.

This occurs as fireInputEventIfNeeded does not fire an input event due
to the new value matching the previous value of the input. The next
character is then typed and the selected text is then overwritten.

This fix changes the fireInputEventIfNeeded condition from no event
being fired when the previous and new values match to an event being
fired in this scenario when the element value has a selection.
  • Loading branch information
fergusmcdonald committed Mar 22, 2021
1 parent 74d191c commit 02037d4
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 72 deletions.
185 changes: 114 additions & 71 deletions src/__tests__/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,49 @@ test('should replace selected text', () => {
expect(element).toHaveValue('hello friend')
})

// https://github.com/testing-library/user-event/issues/583
test('should replace selected text when selected text equals first value of text to be typed', () => {
const {element, getEventSnapshot} = setup('<input type="text" value="a" />')
element.select()
userEvent.type(element, 'abc')

expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="abc"]
input[value="a"] - select
input[value="a"] - pointerover
input[value="a"] - pointerenter
input[value="a"] - mouseover: Left (0)
input[value="a"] - mouseenter: Left (0)
input[value="a"] - pointermove
input[value="a"] - mousemove: Left (0)
input[value="a"] - pointerdown
input[value="a"] - mousedown: Left (0)
input[value="a"] - focus
input[value="a"] - focusin
input[value="a"] - pointerup
input[value="a"] - mouseup: Left (0)
input[value="a"] - click: Left (0)
input[value="a"] - keydown: a (97)
input[value="a"] - keypress: a (97)
input[value="a"] - input
input[value="a"] - select
input[value="a"] - keyup: a (97)
input[value="a"] - keydown: b (98)
input[value="a"] - keypress: b (98)
input[value="ab"] - input
"a{CURSOR}" -> "ab{CURSOR}"
input[value="ab"] - keyup: b (98)
input[value="ab"] - keydown: c (99)
input[value="ab"] - keypress: c (99)
input[value="abc"] - input
"ab{CURSOR}" -> "abc{CURSOR}"
input[value="abc"] - keyup: c (99)
`)

expect(element).toHaveValue('abc')
})

test('does not continue firing events when disabled during typing', () => {
const {element} = setup('<input />', {
eventHandlers: {input: e => (e.target.disabled = true)},
Expand Down Expand Up @@ -1015,84 +1058,84 @@ test('can type into an input with type `time`', () => {
const {element, getEventSnapshot} = setup('<input type="time" />')
userEvent.type(element, '01:05')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="01:05"]
input[value=""] - pointerover
input[value=""] - pointerenter
input[value=""] - mouseover: Left (0)
input[value=""] - mouseenter: Left (0)
input[value=""] - pointermove
input[value=""] - mousemove: Left (0)
input[value=""] - pointerdown
input[value=""] - mousedown: Left (0)
input[value=""] - focus
input[value=""] - focusin
input[value=""] - pointerup
input[value=""] - mouseup: Left (0)
input[value=""] - click: Left (0)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value=""] - keyup: 0 (48)
input[value=""] - keydown: 1 (49)
input[value=""] - keypress: 1 (49)
input[value=""] - keyup: 1 (49)
input[value=""] - keydown: : (58)
input[value=""] - keypress: : (58)
input[value=""] - keyup: : (58)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value="01:00"] - input
"{CURSOR}" -> "{CURSOR}01:00"
input[value="01:00"] - change
input[value="01:00"] - keyup: 0 (48)
input[value="01:00"] - keydown: 5 (53)
input[value="01:00"] - keypress: 5 (53)
input[value="01:05"] - input
"{CURSOR}01:00" -> "{CURSOR}01:05"
input[value="01:05"] - change
input[value="01:05"] - keyup: 5 (53)
`)
Events fired on: input[value="01:05"]
input[value=""] - pointerover
input[value=""] - pointerenter
input[value=""] - mouseover: Left (0)
input[value=""] - mouseenter: Left (0)
input[value=""] - pointermove
input[value=""] - mousemove: Left (0)
input[value=""] - pointerdown
input[value=""] - mousedown: Left (0)
input[value=""] - focus
input[value=""] - focusin
input[value=""] - pointerup
input[value=""] - mouseup: Left (0)
input[value=""] - click: Left (0)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value=""] - keyup: 0 (48)
input[value=""] - keydown: 1 (49)
input[value=""] - keypress: 1 (49)
input[value=""] - keyup: 1 (49)
input[value=""] - keydown: : (58)
input[value=""] - keypress: : (58)
input[value=""] - keyup: : (58)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value="01:00"] - input
"{CURSOR}" -> "{CURSOR}01:00"
input[value="01:00"] - change
input[value="01:00"] - keyup: 0 (48)
input[value="01:00"] - keydown: 5 (53)
input[value="01:00"] - keypress: 5 (53)
input[value="01:05"] - input
"{CURSOR}01:00" -> "{CURSOR}01:05"
input[value="01:05"] - change
input[value="01:05"] - keyup: 5 (53)
`)
expect(element).toHaveValue('01:05')
})

test('can type into an input with type `time` without ":"', () => {
const {element, getEventSnapshot} = setup('<input type="time" />')
userEvent.type(element, '0105')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="01:05"]
input[value=""] - pointerover
input[value=""] - pointerenter
input[value=""] - mouseover: Left (0)
input[value=""] - mouseenter: Left (0)
input[value=""] - pointermove
input[value=""] - mousemove: Left (0)
input[value=""] - pointerdown
input[value=""] - mousedown: Left (0)
input[value=""] - focus
input[value=""] - focusin
input[value=""] - pointerup
input[value=""] - mouseup: Left (0)
input[value=""] - click: Left (0)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value=""] - keyup: 0 (48)
input[value=""] - keydown: 1 (49)
input[value=""] - keypress: 1 (49)
input[value=""] - keyup: 1 (49)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value="01:00"] - input
"{CURSOR}" -> "{CURSOR}01:00"
input[value="01:00"] - change
input[value="01:00"] - keyup: 0 (48)
input[value="01:00"] - keydown: 5 (53)
input[value="01:00"] - keypress: 5 (53)
input[value="01:05"] - input
"{CURSOR}01:00" -> "{CURSOR}01:05"
input[value="01:05"] - change
input[value="01:05"] - keyup: 5 (53)
`)
Events fired on: input[value="01:05"]
input[value=""] - pointerover
input[value=""] - pointerenter
input[value=""] - mouseover: Left (0)
input[value=""] - mouseenter: Left (0)
input[value=""] - pointermove
input[value=""] - mousemove: Left (0)
input[value=""] - pointerdown
input[value=""] - mousedown: Left (0)
input[value=""] - focus
input[value=""] - focusin
input[value=""] - pointerup
input[value=""] - mouseup: Left (0)
input[value=""] - click: Left (0)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value=""] - keyup: 0 (48)
input[value=""] - keydown: 1 (49)
input[value=""] - keypress: 1 (49)
input[value=""] - keyup: 1 (49)
input[value=""] - keydown: 0 (48)
input[value=""] - keypress: 0 (48)
input[value="01:00"] - input
"{CURSOR}" -> "{CURSOR}01:00"
input[value="01:00"] - change
input[value="01:00"] - keyup: 0 (48)
input[value="01:00"] - keydown: 5 (53)
input[value="01:00"] - keypress: 5 (53)
input[value="01:05"] - input
"{CURSOR}01:00" -> "{CURSOR}01:05"
input[value="01:05"] - change
input[value="01:05"] - keyup: 5 (53)
`)
expect(element).toHaveValue('01:05')
})

Expand Down
3 changes: 2 additions & 1 deletion src/keyboard/shared/fireInputEventIfNeeded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isClickableInput,
getValue,
isContentEditable,
hasSelection,
} from '../../utils'
import {setSelectionRange} from './setSelectionRange'

Expand All @@ -28,7 +29,7 @@ export function fireInputEventIfNeeded({
el &&
!isReadonly(el) &&
!isClickableInput(el) &&
newValue !== prevValue
(newValue !== prevValue || hasSelection(el))
) {
if (isContentEditable(el)) {
fireEvent.input(el, {
Expand Down
6 changes: 6 additions & 0 deletions src/utils/edit/hasSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {getSelectionRange} from 'utils'

export function hasSelection(element: Element): boolean {
const {selectionStart, selectionEnd} = getSelectionRange(element)
return selectionStart !== selectionEnd
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './edit/buildTimeValue'
export * from './edit/calculateNewValue'
export * from './edit/getSelectionRange'
export * from './edit/getValue'
export * from './edit/hasSelection'
export * from './edit/isContentEditable'
export * from './edit/isValidDateValue'
export * from './edit/isValidInputTimeValue'
Expand Down

0 comments on commit 02037d4

Please sign in to comment.