Skip to content

Commit df57d7b

Browse files
authored
feat(createEvent): allow creating generic events (#650)
1 parent 167b4ac commit df57d7b

File tree

3 files changed

+184
-152
lines changed

3 files changed

+184
-152
lines changed

src/__tests__/events.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {eventMap, eventAliasMap} from '../event-map'
2-
import {fireEvent} from '..'
2+
import {fireEvent, createEvent} from '..'
33

44
const eventTypes = [
55
{
@@ -390,3 +390,14 @@ test('fires events on Document', () => {
390390
expect(keyDownSpy).toHaveBeenCalledTimes(1)
391391
document.removeEventListener('keydown', keyDownSpy)
392392
})
393+
394+
test('can create generic events', () => {
395+
const el = document.createElement('div')
396+
const eventName = 'my-custom-event'
397+
const handler = jest.fn()
398+
el.addEventListener(eventName, handler)
399+
const event = createEvent(eventName, el)
400+
fireEvent(el, event)
401+
expect(handler).toHaveBeenCalledTimes(1)
402+
expect(handler).toHaveBeenCalledWith(event)
403+
})

src/events.js

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -18,72 +18,78 @@ function fireEvent(element, event) {
1818
})
1919
}
2020

21-
const createEvent = {}
21+
function createEvent(
22+
eventName,
23+
node,
24+
init,
25+
{EventType = 'Event', defaultInit = {}} = {},
26+
) {
27+
if (!node) {
28+
throw new Error(
29+
`Unable to fire a "${eventName}" event - please provide a DOM element.`,
30+
)
31+
}
32+
const eventInit = {...defaultInit, ...init}
33+
const {target: {value, files, ...targetProperties} = {}} = eventInit
34+
if (value !== undefined) {
35+
setNativeValue(node, value)
36+
}
37+
if (files !== undefined) {
38+
// input.files is a read-only property so this is not allowed:
39+
// input.files = [file]
40+
// so we have to use this workaround to set the property
41+
Object.defineProperty(node, 'files', {
42+
configurable: true,
43+
enumerable: true,
44+
writable: true,
45+
value: files,
46+
})
47+
}
48+
Object.assign(node, targetProperties)
49+
const window = getWindowFromNode(node)
50+
const EventConstructor = window[EventType] || window.Event
51+
let event
52+
/* istanbul ignore else */
53+
if (typeof EventConstructor === 'function') {
54+
event = new EventConstructor(eventName, eventInit)
55+
} else {
56+
// IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
57+
event = window.document.createEvent(EventType)
58+
const {bubbles, cancelable, detail, ...otherInit} = eventInit
59+
event.initEvent(eventName, bubbles, cancelable, detail)
60+
Object.keys(otherInit).forEach(eventKey => {
61+
event[eventKey] = otherInit[eventKey]
62+
})
63+
}
64+
65+
// DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568
66+
const dataTransferProperties = ['dataTransfer', 'clipboardData']
67+
dataTransferProperties.forEach(dataTransferKey => {
68+
const dataTransferValue = eventInit[dataTransferKey]
69+
70+
if (typeof dataTransferValue === 'object') {
71+
/* istanbul ignore if */
72+
if (typeof window.DataTransfer === 'function') {
73+
Object.defineProperty(event, dataTransferKey, {
74+
value: Object.assign(new window.DataTransfer(), dataTransferValue),
75+
})
76+
} else {
77+
Object.defineProperty(event, dataTransferKey, {
78+
value: dataTransferValue,
79+
})
80+
}
81+
}
82+
})
83+
84+
return event
85+
}
2286

2387
Object.keys(eventMap).forEach(key => {
2488
const {EventType, defaultInit} = eventMap[key]
2589
const eventName = key.toLowerCase()
2690

27-
createEvent[key] = (node, init) => {
28-
if (!node) {
29-
throw new Error(
30-
`Unable to fire a "${key}" event - please provide a DOM element.`,
31-
)
32-
}
33-
const eventInit = {...defaultInit, ...init}
34-
const {target: {value, files, ...targetProperties} = {}} = eventInit
35-
if (value !== undefined) {
36-
setNativeValue(node, value)
37-
}
38-
if (files !== undefined) {
39-
// input.files is a read-only property so this is not allowed:
40-
// input.files = [file]
41-
// so we have to use this workaround to set the property
42-
Object.defineProperty(node, 'files', {
43-
configurable: true,
44-
enumerable: true,
45-
writable: true,
46-
value: files,
47-
})
48-
}
49-
Object.assign(node, targetProperties)
50-
const window = getWindowFromNode(node)
51-
const EventConstructor = window[EventType] || window.Event
52-
let event
53-
/* istanbul ignore else */
54-
if (typeof EventConstructor === 'function') {
55-
event = new EventConstructor(eventName, eventInit)
56-
} else {
57-
// IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
58-
event = window.document.createEvent(EventType)
59-
const {bubbles, cancelable, detail, ...otherInit} = eventInit
60-
event.initEvent(eventName, bubbles, cancelable, detail)
61-
Object.keys(otherInit).forEach(eventKey => {
62-
event[eventKey] = otherInit[eventKey]
63-
})
64-
}
65-
66-
// DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568
67-
['dataTransfer', 'clipboardData'].forEach(dataTransferKey => {
68-
const dataTransferValue = eventInit[dataTransferKey];
69-
70-
if (typeof dataTransferValue === 'object') {
71-
/* istanbul ignore if */
72-
if (typeof window.DataTransfer === 'function') {
73-
Object.defineProperty(event, dataTransferKey, {
74-
value: Object.assign(new window.DataTransfer(), dataTransferValue)
75-
})
76-
} else {
77-
Object.defineProperty(event, dataTransferKey, {
78-
value: dataTransferValue
79-
})
80-
}
81-
}
82-
})
83-
84-
return event
85-
}
86-
91+
createEvent[key] = (node, init) =>
92+
createEvent(eventName, node, init, {EventType, defaultInit})
8793
fireEvent[key] = (node, init) => fireEvent(node, createEvent[key](node, init))
8894
})
8995

types/events.d.ts

Lines changed: 105 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,110 @@
11
export type EventType =
2-
| 'copy'
3-
| 'cut'
4-
| 'paste'
5-
| 'compositionEnd'
6-
| 'compositionStart'
7-
| 'compositionUpdate'
8-
| 'keyDown'
9-
| 'keyPress'
10-
| 'keyUp'
11-
| 'focus'
12-
| 'blur'
13-
| 'focusIn'
14-
| 'focusOut'
15-
| 'change'
16-
| 'input'
17-
| 'invalid'
18-
| 'submit'
19-
| 'reset'
20-
| 'click'
21-
| 'contextMenu'
22-
| 'dblClick'
23-
| 'drag'
24-
| 'dragEnd'
25-
| 'dragEnter'
26-
| 'dragExit'
27-
| 'dragLeave'
28-
| 'dragOver'
29-
| 'dragStart'
30-
| 'drop'
31-
| 'mouseDown'
32-
| 'mouseEnter'
33-
| 'mouseLeave'
34-
| 'mouseMove'
35-
| 'mouseOut'
36-
| 'mouseOver'
37-
| 'mouseUp'
38-
| 'popState'
39-
| 'select'
40-
| 'touchCancel'
41-
| 'touchEnd'
42-
| 'touchMove'
43-
| 'touchStart'
44-
| 'scroll'
45-
| 'wheel'
46-
| 'abort'
47-
| 'canPlay'
48-
| 'canPlayThrough'
49-
| 'durationChange'
50-
| 'emptied'
51-
| 'encrypted'
52-
| 'ended'
53-
| 'loadedData'
54-
| 'loadedMetadata'
55-
| 'loadStart'
56-
| 'pause'
57-
| 'play'
58-
| 'playing'
59-
| 'progress'
60-
| 'rateChange'
61-
| 'seeked'
62-
| 'seeking'
63-
| 'stalled'
64-
| 'suspend'
65-
| 'timeUpdate'
66-
| 'volumeChange'
67-
| 'waiting'
68-
| 'load'
69-
| 'error'
70-
| 'animationStart'
71-
| 'animationEnd'
72-
| 'animationIteration'
73-
| 'transitionEnd'
74-
| 'doubleClick'
75-
| 'pointerOver'
76-
| 'pointerEnter'
77-
| 'pointerDown'
78-
| 'pointerMove'
79-
| 'pointerUp'
80-
| 'pointerCancel'
81-
| 'pointerOut'
82-
| 'pointerLeave'
83-
| 'gotPointerCapture'
84-
| 'lostPointerCapture';
2+
| 'copy'
3+
| 'cut'
4+
| 'paste'
5+
| 'compositionEnd'
6+
| 'compositionStart'
7+
| 'compositionUpdate'
8+
| 'keyDown'
9+
| 'keyPress'
10+
| 'keyUp'
11+
| 'focus'
12+
| 'blur'
13+
| 'focusIn'
14+
| 'focusOut'
15+
| 'change'
16+
| 'input'
17+
| 'invalid'
18+
| 'submit'
19+
| 'reset'
20+
| 'click'
21+
| 'contextMenu'
22+
| 'dblClick'
23+
| 'drag'
24+
| 'dragEnd'
25+
| 'dragEnter'
26+
| 'dragExit'
27+
| 'dragLeave'
28+
| 'dragOver'
29+
| 'dragStart'
30+
| 'drop'
31+
| 'mouseDown'
32+
| 'mouseEnter'
33+
| 'mouseLeave'
34+
| 'mouseMove'
35+
| 'mouseOut'
36+
| 'mouseOver'
37+
| 'mouseUp'
38+
| 'popState'
39+
| 'select'
40+
| 'touchCancel'
41+
| 'touchEnd'
42+
| 'touchMove'
43+
| 'touchStart'
44+
| 'scroll'
45+
| 'wheel'
46+
| 'abort'
47+
| 'canPlay'
48+
| 'canPlayThrough'
49+
| 'durationChange'
50+
| 'emptied'
51+
| 'encrypted'
52+
| 'ended'
53+
| 'loadedData'
54+
| 'loadedMetadata'
55+
| 'loadStart'
56+
| 'pause'
57+
| 'play'
58+
| 'playing'
59+
| 'progress'
60+
| 'rateChange'
61+
| 'seeked'
62+
| 'seeking'
63+
| 'stalled'
64+
| 'suspend'
65+
| 'timeUpdate'
66+
| 'volumeChange'
67+
| 'waiting'
68+
| 'load'
69+
| 'error'
70+
| 'animationStart'
71+
| 'animationEnd'
72+
| 'animationIteration'
73+
| 'transitionEnd'
74+
| 'doubleClick'
75+
| 'pointerOver'
76+
| 'pointerEnter'
77+
| 'pointerDown'
78+
| 'pointerMove'
79+
| 'pointerUp'
80+
| 'pointerCancel'
81+
| 'pointerOut'
82+
| 'pointerLeave'
83+
| 'gotPointerCapture'
84+
| 'lostPointerCapture'
8585

86-
export type FireFunction = (element: Document | Element | Window | Node, event: Event) => boolean;
86+
export type FireFunction = (
87+
element: Document | Element | Window | Node,
88+
event: Event,
89+
) => boolean
8790
export type FireObject = {
88-
[K in EventType]: (element: Document | Element | Window | Node, options?: {}) => boolean;
89-
};
91+
[K in EventType]: (
92+
element: Document | Element | Window | Node,
93+
options?: {},
94+
) => boolean
95+
}
96+
export type CreateFunction = (
97+
eventName: EventType,
98+
node: Document | Element | Window | Node,
99+
init?: {},
100+
options?: {EventType?: string; defaultInit?: {}},
101+
) => Event
90102
export type CreateObject = {
91-
[K in EventType]: (element: Document | Element | Window | Node, options?: {}) => Event;
92-
};
103+
[K in EventType]: (
104+
element: Document | Element | Window | Node,
105+
options?: {},
106+
) => Event
107+
}
93108

94-
export const createEvent: CreateObject;
95-
export const fireEvent: FireFunction & FireObject;
109+
export const createEvent: CreateObject
110+
export const fireEvent: FireFunction & FireObject

0 commit comments

Comments
 (0)