Skip to content

Commit 8890bd6

Browse files
authored
feat(event): support beforeinput (#851)
1 parent ca4482a commit 8890bd6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+804
-934
lines changed

src/clipboard/cut.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {Config, Instance} from '../setup'
22
import {
33
copySelection,
4+
input,
45
isEditable,
5-
prepareInput,
66
writeDataTransferToClipboard,
77
} from '../utils'
88

@@ -21,7 +21,7 @@ export async function cut(this: Instance) {
2121
})
2222

2323
if (isEditable(target)) {
24-
prepareInput(this[Config], '', target, 'deleteByCut')?.commit()
24+
input(this[Config], target, '', 'deleteByCut')
2525
}
2626

2727
if (this[Config].writeToClipboard) {

src/clipboard/paste.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {Config, Instance} from '../setup'
22
import {
33
createDataTransfer,
4-
getSpaceUntilMaxLength,
5-
prepareInput,
64
isEditable,
75
readDataTransferFromClipboard,
6+
input,
87
} from '../utils'
98

109
export async function paste(
@@ -29,13 +28,7 @@ export async function paste(
2928
})
3029

3130
if (isEditable(target)) {
32-
const textData = dataTransfer
33-
.getData('text')
34-
.substr(0, getSpaceUntilMaxLength(target))
35-
36-
if (textData) {
37-
prepareInput(this[Config], textData, target, 'insertFromPaste')?.commit()
38-
}
31+
input(this[Config], target, dataTransfer.getData('text'), 'insertFromPaste')
3932
}
4033
}
4134

src/event/behavior/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import './click'
2+
import './keydown'
3+
import './keypress'
4+
import './keyup'
25

36
export {behavior} from './registry'
47
export type {BehaviorPlugin} from './registry'

src/event/behavior/keydown.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
3+
import {setUISelection} from '../../document'
4+
import {
5+
focus,
6+
getTabDestination,
7+
getValue,
8+
hasOwnSelection,
9+
input,
10+
isContentEditable,
11+
isEditable,
12+
isElementType,
13+
moveSelection,
14+
selectAll,
15+
setSelectionRange,
16+
} from '../../utils'
17+
import {BehaviorPlugin} from '.'
18+
import {behavior} from './registry'
19+
20+
behavior.keydown = (event, target, config) => {
21+
return (
22+
keydownBehavior[event.key]?.(event, target, config) ??
23+
combinationBehavior(event, target, config)
24+
)
25+
}
26+
27+
const keydownBehavior: {
28+
[key: string]: BehaviorPlugin<'keydown'> | undefined
29+
} = {
30+
ArrowLeft: (event, target) => () => moveSelection(target, -1),
31+
ArrowRight: (event, target) => () => moveSelection(target, 1),
32+
Backspace: (event, target, config) => {
33+
if (isEditable(target)) {
34+
return () => {
35+
input(config, target, '', 'deleteContentBackward')
36+
}
37+
}
38+
},
39+
Delete: (event, target, config) => {
40+
if (isEditable(target)) {
41+
return () => {
42+
input(config, target, '', 'deleteContentForward')
43+
}
44+
}
45+
},
46+
End: (event, target) => {
47+
if (
48+
isElementType(target, ['input', 'textarea']) ||
49+
isContentEditable(target)
50+
) {
51+
return () => {
52+
const newPos = getValue(target)?.length ?? /* istanbul ignore next */ 0
53+
setSelectionRange(target, newPos, newPos)
54+
}
55+
}
56+
},
57+
Home: (event, target) => {
58+
if (
59+
isElementType(target, ['input', 'textarea']) ||
60+
isContentEditable(target)
61+
) {
62+
return () => {
63+
setSelectionRange(target, 0, 0)
64+
}
65+
}
66+
},
67+
PageDown: (event, target) => {
68+
if (isElementType(target, ['input'])) {
69+
return () => {
70+
const newPos = getValue(target).length
71+
setSelectionRange(target, newPos, newPos)
72+
}
73+
}
74+
},
75+
PageUp: (event, target) => {
76+
if (isElementType(target, ['input'])) {
77+
return () => {
78+
setSelectionRange(target, 0, 0)
79+
}
80+
}
81+
},
82+
Tab: (event, target, {keyboardState}) => {
83+
return () => {
84+
const dest = getTabDestination(target, keyboardState.modifiers.Shift)
85+
focus(dest)
86+
if (hasOwnSelection(dest)) {
87+
setUISelection(dest, {
88+
anchorOffset: 0,
89+
focusOffset: dest.value.length,
90+
})
91+
}
92+
}
93+
},
94+
}
95+
96+
const combinationBehavior: BehaviorPlugin<'keydown'> = (
97+
event,
98+
target,
99+
config,
100+
) => {
101+
if (event.code === 'KeyA' && config.keyboardState.modifiers.Control) {
102+
return () => selectAll(target)
103+
}
104+
}

src/event/behavior/keypress.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
3+
import {dispatchUIEvent} from '..'
4+
import {input, isContentEditable, isEditable, isElementType} from '../../utils'
5+
import {behavior} from './registry'
6+
7+
behavior.keypress = (event, target, config) => {
8+
if (event.key === 'Enter') {
9+
if (
10+
isElementType(target, 'button') ||
11+
(isElementType(target, 'input') &&
12+
ClickInputOnEnter.includes(target.type)) ||
13+
(isElementType(target, 'a') && Boolean(target.href))
14+
) {
15+
return () => {
16+
dispatchUIEvent(config, target, 'click')
17+
}
18+
} else if (isElementType(target, 'input')) {
19+
const form = target.form
20+
const submit = form?.querySelector(
21+
'input[type="submit"], button:not([type]), button[type="submit"]',
22+
)
23+
if (submit) {
24+
return () => dispatchUIEvent(config, submit, 'click')
25+
} else if (
26+
form &&
27+
SubmitSingleInputOnEnter.includes(target.type) &&
28+
form.querySelectorAll('input').length === 1
29+
) {
30+
return () => dispatchUIEvent(config, form, 'submit')
31+
} else {
32+
return
33+
}
34+
}
35+
}
36+
37+
if (isEditable(target)) {
38+
const inputType =
39+
event.key === 'Enter'
40+
? isContentEditable(target) && !config.keyboardState.modifiers.Shift
41+
? 'insertParagraph'
42+
: 'insertLineBreak'
43+
: 'insertText'
44+
const inputData = event.key === 'Enter' ? '\n' : event.key
45+
46+
return () => input(config, target, inputData, inputType)
47+
}
48+
}
49+
50+
const ClickInputOnEnter = [
51+
'button',
52+
'color',
53+
'file',
54+
'image',
55+
'reset',
56+
'submit',
57+
]
58+
59+
const SubmitSingleInputOnEnter = [
60+
'email',
61+
'month',
62+
'password',
63+
'search',
64+
'tel',
65+
'text',
66+
'url',
67+
'week',
68+
]

src/event/behavior/keyup.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
3+
import {isClickableInput} from '../../utils'
4+
import {dispatchUIEvent} from '..'
5+
import {BehaviorPlugin} from '.'
6+
import {behavior} from './registry'
7+
8+
behavior.keyup = (event, target, config) => {
9+
return keyupBehavior[event.key]?.(event, target, config)
10+
}
11+
12+
const keyupBehavior: {
13+
[key: string]: BehaviorPlugin<'keyup'> | undefined
14+
} = {
15+
' ': (event, target, config) => {
16+
if (isClickableInput(target)) {
17+
return () => dispatchUIEvent(config, target, 'click')
18+
}
19+
},
20+
}

src/event/createEvent.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {createEvent as createEventBase} from '@testing-library/dom'
2-
import {eventMap} from '@testing-library/dom/dist/event-map.js'
2+
import {eventMap} from './eventMap'
33
import {isMouseEvent} from './eventTypes'
44
import {EventType, PointerCoords} from './types'
55

@@ -32,9 +32,14 @@ export function createEvent<K extends EventType>(
3232
) {
3333
const eventKey = Object.keys(eventMap).find(
3434
k => k.toLowerCase() === type,
35-
) as keyof typeof createEventBase
35+
) as keyof typeof eventMap
3636

37-
const event = createEventBase[eventKey](target, init) as DocumentEventMap[K]
37+
const event = createEventBase(
38+
type,
39+
target,
40+
init,
41+
eventMap[eventKey],
42+
) as DocumentEventMap[K]
3843

3944
// Can not use instanceof, as MouseEvent might be polyfilled.
4045
if (isMouseEvent(type) && init) {

src/event/dispatchEvent.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import {Config} from '../setup'
22
import {EventType} from './types'
33
import {behavior, BehaviorPlugin} from './behavior'
4+
import {wrapEvent} from './wrapEvent'
45

5-
export function dispatchEvent(config: Config, target: Element, event: Event) {
6+
export function dispatchEvent(
7+
config: Config,
8+
target: Element,
9+
event: Event,
10+
preventDefault: boolean = false,
11+
) {
612
const type = event.type as EventType
7-
const behaviorImplementation = (
8-
behavior[type] as BehaviorPlugin<EventType> | undefined
9-
)?.(event, target, config)
13+
const behaviorImplementation = preventDefault
14+
? () => {}
15+
: (behavior[type] as BehaviorPlugin<EventType> | undefined)?.(
16+
event,
17+
target,
18+
config,
19+
)
1020

1121
if (behaviorImplementation) {
1222
event.preventDefault()
@@ -20,7 +30,7 @@ export function dispatchEvent(config: Config, target: Element, event: Event) {
2030
},
2131
})
2232

23-
target.dispatchEvent(event)
33+
wrapEvent(() => target.dispatchEvent(event), target)
2434

2535
if (!defaultPrevented as boolean) {
2636
behaviorImplementation()
@@ -29,5 +39,5 @@ export function dispatchEvent(config: Config, target: Element, event: Event) {
2939
return !defaultPrevented
3040
}
3141

32-
return target.dispatchEvent(event)
42+
return wrapEvent(() => target.dispatchEvent(event), target)
3343
}

src/event/eventMap.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {eventMap as baseEventMap} from '@testing-library/dom/dist/event-map.js'
2+
3+
export const eventMap = {
4+
...baseEventMap,
5+
6+
beforeInput: {
7+
EventType: 'InputEvent',
8+
defaultInit: {bubbles: true, cancelable: true, composed: true},
9+
},
10+
}

src/event/eventTypes.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import {eventMap} from '@testing-library/dom/dist/event-map.js'
33
const eventKeys = Object.fromEntries(
44
Object.keys(eventMap).map(k => [k.toLowerCase(), k]),
55
) as {
6-
[k in keyof DocumentEventMap]: keyof typeof eventMap
6+
[k in keyof DocumentEventMap]?: keyof typeof eventMap
77
}
88

99
function getEventClass(type: keyof DocumentEventMap) {
10-
return eventMap[eventKeys[type]].EventType
10+
return type in eventKeys
11+
? eventMap[eventKeys[type] as keyof typeof eventMap].EventType
12+
: 'Event'
1113
}
1214

1315
const mouseEvents = ['MouseEvent', 'PointerEvent']

src/event/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {createEvent, EventTypeInit} from './createEvent'
44
import {dispatchEvent} from './dispatchEvent'
55
import {isKeyboardEvent, isMouseEvent} from './eventTypes'
66
import {EventType, PointerCoords} from './types'
7-
import {wrapEvent} from './wrapEvent'
87

98
export type {EventType, PointerCoords}
109

@@ -13,6 +12,7 @@ export function dispatchUIEvent<K extends EventType>(
1312
target: Element,
1413
type: K,
1514
init?: EventTypeInit<K>,
15+
preventDefault: boolean = false,
1616
) {
1717
if (isMouseEvent(type) || isKeyboardEvent(type)) {
1818
init = {
@@ -23,7 +23,7 @@ export function dispatchUIEvent<K extends EventType>(
2323

2424
const event = createEvent(type, target, init)
2525

26-
return wrapEvent(() => dispatchEvent(config, target, event), target)
26+
return dispatchEvent(config, target, event, preventDefault)
2727
}
2828

2929
export function bindDispatchUIEvent(config: Config) {

0 commit comments

Comments
 (0)