Skip to content

Commit

Permalink
feat(event): support beforeinput
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Feb 9, 2022
1 parent ca4482a commit 3820621
Show file tree
Hide file tree
Showing 53 changed files with 767 additions and 934 deletions.
4 changes: 2 additions & 2 deletions src/clipboard/cut.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Config, Instance} from '../setup'
import {
copySelection,
input,
isEditable,
prepareInput,
writeDataTransferToClipboard,
} from '../utils'

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

if (isEditable(target)) {
prepareInput(this[Config], '', target, 'deleteByCut')?.commit()
input(this[Config], target, '', 'deleteByCut')
}

if (this[Config].writeToClipboard) {
Expand Down
11 changes: 2 additions & 9 deletions src/clipboard/paste.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {Config, Instance} from '../setup'
import {
createDataTransfer,
getSpaceUntilMaxLength,
prepareInput,
isEditable,
readDataTransferFromClipboard,
input,
} from '../utils'

export async function paste(
Expand All @@ -29,13 +28,7 @@ export async function paste(
})

if (isEditable(target)) {
const textData = dataTransfer
.getData('text')
.substr(0, getSpaceUntilMaxLength(target))

if (textData) {
prepareInput(this[Config], textData, target, 'insertFromPaste')?.commit()
}
input(this[Config], target, dataTransfer.getData('text'), 'insertFromPaste')
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/event/behavior/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import './click'
import './keydown'
import './keypress'
import './keyup'

export {behavior} from './registry'
export type {BehaviorPlugin} from './registry'
104 changes: 104 additions & 0 deletions src/event/behavior/keydown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/no-use-before-define */

import {setUISelection} from '../../document'
import {
focus,
getTabDestination,
getValue,
hasOwnSelection,
input,
isContentEditable,
isEditable,
isElementType,
moveSelection,
selectAll,
setSelectionRange,
} from '../../utils'
import {BehaviorPlugin} from '.'
import {behavior} from './registry'

behavior.keydown = (event, target, config) => {
return (
keydownBehavior[event.key]?.(event, target, config) ??
combinationBehavior(event, target, config)
)
}

const keydownBehavior: {
[key: string]: BehaviorPlugin<'keydown'> | undefined
} = {
ArrowLeft: (event, target) => () => moveSelection(target, -1),
ArrowRight: (event, target) => () => moveSelection(target, 1),
Backspace: (event, target, config) => {
if (isEditable(target)) {
return () => {
input(config, target, '', 'deleteContentBackward')
}
}
},
Delete: (event, target, config) => {
if (isEditable(target)) {
return () => {
input(config, target, '', 'deleteContentForward')
}
}
},
End: (event, target) => {
if (
isElementType(target, ['input', 'textarea']) ||
isContentEditable(target)
) {
return () => {
const newPos = getValue(target)?.length ?? /* istanbul ignore next */ 0
setSelectionRange(target, newPos, newPos)
}
}
},
Home: (event, target) => {
if (
isElementType(target, ['input', 'textarea']) ||
isContentEditable(target)
) {
return () => {
setSelectionRange(target, 0, 0)
}
}
},
PageDown: (event, target) => {
if (isElementType(target, ['input'])) {
return () => {
const newPos = getValue(target).length
setSelectionRange(target, newPos, newPos)
}
}
},
PageUp: (event, target) => {
if (isElementType(target, ['input'])) {
return () => {
setSelectionRange(target, 0, 0)
}
}
},
Tab: (event, target, {keyboardState}) => {
return () => {
const dest = getTabDestination(target, keyboardState.modifiers.Shift)
focus(dest)
if (hasOwnSelection(dest)) {
setUISelection(dest, {
anchorOffset: 0,
focusOffset: dest.value.length,
})
}
}
},
}

const combinationBehavior: BehaviorPlugin<'keydown'> = (
event,
target,
config,
) => {
if (event.code === 'KeyA' && config.keyboardState.modifiers.Control) {
return () => selectAll(target)
}
}
68 changes: 68 additions & 0 deletions src/event/behavior/keypress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/no-use-before-define */

import {dispatchUIEvent} from '..'
import {input, isContentEditable, isEditable, isElementType} from '../../utils'
import {behavior} from './registry'

behavior.keypress = (event, target, config) => {
if (event.key === 'Enter') {
if (
isElementType(target, 'button') ||
(isElementType(target, 'input') &&
ClickInputOnEnter.includes(target.type)) ||
(isElementType(target, 'a') && Boolean(target.href))
) {
return () => {
dispatchUIEvent(config, target, 'click')
}
} else if (isElementType(target, 'input')) {
const form = target.form
const submit = form?.querySelector(
'input[type="submit"], button:not([type]), button[type="submit"]',
)
if (submit) {
return () => dispatchUIEvent(config, submit, 'click')
} else if (
form &&
SubmitSingleInputOnEnter.includes(target.type) &&
form.querySelectorAll('input').length === 1
) {
return () => dispatchUIEvent(config, form, 'submit')
} else {
return
}
}
}

if (isEditable(target)) {
const inputType =
event.key === 'Enter'
? isContentEditable(target) && !config.keyboardState.modifiers.Shift
? 'insertParagraph'
: 'insertLineBreak'
: 'insertText'
const inputData = event.key === 'Enter' ? '\n' : event.key

return () => input(config, target, inputData, inputType)
}
}

const ClickInputOnEnter = [
'button',
'color',
'file',
'image',
'reset',
'submit',
]

const SubmitSingleInputOnEnter = [
'email',
'month',
'password',
'search',
'tel',
'text',
'url',
'week',
]
20 changes: 20 additions & 0 deletions src/event/behavior/keyup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-use-before-define */

import {isClickableInput} from '../../utils'
import {dispatchUIEvent} from '..'
import {BehaviorPlugin} from '.'
import {behavior} from './registry'

behavior.keyup = (event, target, config) => {
return keyupBehavior[event.key]?.(event, target, config)
}

const keyupBehavior: {
[key: string]: BehaviorPlugin<'keyup'> | undefined
} = {
' ': (event, target, config) => {
if (isClickableInput(target)) {
return () => dispatchUIEvent(config, target, 'click')
}
},
}
11 changes: 8 additions & 3 deletions src/event/createEvent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createEvent as createEventBase} from '@testing-library/dom'
import {eventMap} from '@testing-library/dom/dist/event-map.js'
import {eventMap} from './eventMap'
import {isMouseEvent} from './eventTypes'
import {EventType, PointerCoords} from './types'

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

const event = createEventBase[eventKey](target, init) as DocumentEventMap[K]
const event = createEventBase(
type,
target,
init,
eventMap[eventKey],
) as DocumentEventMap[K]

// Can not use instanceof, as MouseEvent might be polyfilled.
if (isMouseEvent(type) && init) {
Expand Down
22 changes: 16 additions & 6 deletions src/event/dispatchEvent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import {Config} from '../setup'
import {EventType} from './types'
import {behavior, BehaviorPlugin} from './behavior'
import {wrapEvent} from './wrapEvent'

export function dispatchEvent(config: Config, target: Element, event: Event) {
export function dispatchEvent(
config: Config,
target: Element,
event: Event,
preventDefault: boolean = false,
) {
const type = event.type as EventType
const behaviorImplementation = (
behavior[type] as BehaviorPlugin<EventType> | undefined
)?.(event, target, config)
const behaviorImplementation = preventDefault
? () => {}
: (behavior[type] as BehaviorPlugin<EventType> | undefined)?.(
event,
target,
config,
)

if (behaviorImplementation) {
event.preventDefault()
Expand All @@ -20,7 +30,7 @@ export function dispatchEvent(config: Config, target: Element, event: Event) {
},
})

target.dispatchEvent(event)
wrapEvent(() => target.dispatchEvent(event), target)

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

return target.dispatchEvent(event)
return wrapEvent(() => target.dispatchEvent(event), target)
}
10 changes: 10 additions & 0 deletions src/event/eventMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {eventMap as baseEventMap} from '@testing-library/dom/dist/event-map.js'

export const eventMap = {
...baseEventMap,

beforeInput: {
EventType: 'InputEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
}
6 changes: 4 additions & 2 deletions src/event/eventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import {eventMap} from '@testing-library/dom/dist/event-map.js'
const eventKeys = Object.fromEntries(
Object.keys(eventMap).map(k => [k.toLowerCase(), k]),
) as {
[k in keyof DocumentEventMap]: keyof typeof eventMap
[k in keyof DocumentEventMap]?: keyof typeof eventMap
}

function getEventClass(type: keyof DocumentEventMap) {
return eventMap[eventKeys[type]].EventType
return type in eventKeys
? eventMap[eventKeys[type] as keyof typeof eventMap].EventType
: 'Event'
}

const mouseEvents = ['MouseEvent', 'PointerEvent']
Expand Down
4 changes: 2 additions & 2 deletions src/event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {createEvent, EventTypeInit} from './createEvent'
import {dispatchEvent} from './dispatchEvent'
import {isKeyboardEvent, isMouseEvent} from './eventTypes'
import {EventType, PointerCoords} from './types'
import {wrapEvent} from './wrapEvent'

export type {EventType, PointerCoords}

Expand All @@ -13,6 +12,7 @@ export function dispatchUIEvent<K extends EventType>(
target: Element,
type: K,
init?: EventTypeInit<K>,
preventDefault: boolean = false,
) {
if (isMouseEvent(type) || isKeyboardEvent(type)) {
init = {
Expand All @@ -23,7 +23,7 @@ export function dispatchUIEvent<K extends EventType>(

const event = createEvent(type, target, init)

return wrapEvent(() => dispatchEvent(config, target, event), target)
return dispatchEvent(config, target, event, preventDefault)
}

export function bindDispatchUIEvent(config: Config) {
Expand Down
Loading

0 comments on commit 3820621

Please sign in to comment.