Skip to content

Commit 9600abb

Browse files
authored
fix(keyboard): dispatch change event on blur (#703)
1 parent e5e78af commit 9600abb

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {setup} from '__tests__/helpers/utils'
2+
import userEvent from '../../../'
3+
4+
it('dispatch change event on blur', () => {
5+
const {element, getEvents} = setup('<input/>')
6+
7+
;(element as HTMLInputElement).focus()
8+
userEvent.keyboard('foo')
9+
;(element as HTMLInputElement).blur()
10+
11+
expect(getEvents('change')).toHaveLength(1)
12+
})
13+
14+
it('do not dispatch change event if value did not change', () => {
15+
const {element, getEvents} = setup('<input/>')
16+
17+
;(element as HTMLInputElement).focus()
18+
userEvent.keyboard('foo')
19+
userEvent.keyboard('{backspace}{backspace}{backspace}')
20+
;(element as HTMLInputElement).blur()
21+
22+
expect(getEvents('change')).toHaveLength(0)
23+
})

src/keyboard/shared/fireInputEvent.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ function setSelectionRangeAfterInputHandler(
7070
}
7171
}
7272

73+
const initial = Symbol('initial input value/textContent')
74+
const onBlur = Symbol('onBlur')
75+
declare global {
76+
interface Element {
77+
[initial]?: string
78+
[onBlur]?: EventListener
79+
}
80+
}
81+
7382
/**
7483
* React tracks the changes on element properties.
7584
* This workaround tries to alter the DOM element without React noticing,
@@ -92,8 +101,39 @@ function applyNative<T extends Element, P extends keyof T>(
92101
Object.defineProperty(element, propName, nativeDescriptor)
93102
}
94103

104+
// Keep track of the initial value to determine if a change event should be dispatched.
105+
// CONSTRAINT: We can not determine what happened between focus event and our first API call.
106+
if (element[initial] === undefined) {
107+
element[initial] = String(element[propName])
108+
}
109+
95110
element[propName] = propValue
96111

112+
// Add an event listener for the blur event to the capture phase on the window.
113+
// CONSTRAINT: Currently there is no cross-platform solution to unshift the event handler stack.
114+
// Our change event might occur after other event handlers on the blur event have been processed.
115+
if (!element[onBlur]) {
116+
element.ownerDocument.defaultView?.addEventListener(
117+
'blur',
118+
(element[onBlur] = () => {
119+
const initV = element[initial]
120+
121+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
122+
delete element[onBlur]
123+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
124+
delete element[initial]
125+
126+
if (String(element[propName]) !== initV) {
127+
fireEvent.change(element)
128+
}
129+
}),
130+
{
131+
capture: true,
132+
once: true,
133+
},
134+
)
135+
}
136+
97137
if (descriptor) {
98138
Object.defineProperty(element, propName, descriptor)
99139
}

0 commit comments

Comments
 (0)