Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 93 additions & 19 deletions src/create-dom-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,47 @@ interface TriggerOptions {

interface EventParams {
eventType: string
modifier: string
meta: any
modifiers: string[]
options?: TriggerOptions
}

// modifiers to keep an eye on
const ignorableKeyModifiers = ['stop', 'prevent', 'self', 'exact']
const systemKeyModifiers = ['ctrl', 'shift', 'alt', 'meta']
const mouseKeyModifiers = ['left', 'middle', 'right']

/**
* Groups modifiers into lists
*/
function generateModifiers(modifiers: string[], isOnClick: boolean) {
const keyModifiers: string[] = []
const systemModifiers: string[] = []

for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i]

// addEventListener() options, e.g. .passive & .capture, that we dont need to handle
if (ignorableKeyModifiers.includes(modifier)) {
continue
}
// modifiers that require special conversion
// if passed a left/right key modifier with onClick, add it here as well.
if (
systemKeyModifiers.includes(modifier) ||
(mouseKeyModifiers.includes(modifier) && isOnClick)
) {
systemModifiers.push(modifier)
} else {
keyModifiers.push(modifier)
}
}

return {
keyModifiers,
systemModifiers
}
}

export const keyCodesByKeyName = {
backspace: 8,
tab: 9,
Expand All @@ -33,52 +69,90 @@ export const keyCodesByKeyName = {
}

function getEventProperties(eventParams: EventParams) {
const { modifier, meta, options } = eventParams
let { modifiers, options = {}, eventType } = eventParams

let isOnClick = eventType === 'click'

const { keyModifiers, systemModifiers } = generateModifiers(
modifiers,
isOnClick
)

if (isOnClick) {
// if it's a right click, it should fire a `contextmenu` event
if (systemModifiers.includes('right')) {
eventType = 'contextmenu'
options.button = 2
// if its a middle click, fire a `mouseup` event
} else if (systemModifiers.includes('middle')) {
eventType = 'mouseup'
options.button = 1
}
}

const meta = eventTypes[eventType] || {
eventInterface: 'Event',
cancelable: true,
bubbles: true
}

// convert `shift, ctrl` to `shiftKey, ctrlKey`
// allows trigger('keydown.shift.ctrl.n') directly
const systemModifiersMeta = systemModifiers.reduce((all, key) => {
all[`${key}Key`] = true
return all
}, {})

// get the keyCode for backwards compat
const keyCode =
keyCodesByKeyName[modifier] ||
keyCodesByKeyName[keyModifiers[0]] ||
(options && (options.keyCode || options.code))

return {
const eventProperties = {
...systemModifiersMeta, // shiftKey, metaKey etc
...options, // What the user passed in as the second argument to #trigger
bubbles: meta.bubbles,
meta: meta.cancelable,
// Any derived options should go here
keyCode,
code: keyCode
code: keyCode,
// if we have a `key`, use it, otherwise dont set anything (allows user to pass custom key)
...(keyModifiers[0] ? { key: keyModifiers[0] } : {})
}

return {
eventProperties,
meta,
eventType
}
}

function createEvent(eventParams: EventParams) {
const { eventType, meta } = eventParams
const { eventProperties, meta, eventType } = getEventProperties(eventParams)

// user defined eventInterface
const metaEventInterface = window[meta.eventInterface]

const SupportedEventInterface =
typeof metaEventInterface === 'function' ? metaEventInterface : window.Event

const eventProperties = getEventProperties(eventParams)

const event = new SupportedEventInterface(
return new SupportedEventInterface(
eventType,
// event properties can only be added when the event is instantiated
// custom properties must be added after the event has been instantiated
eventProperties
)

return event
}

function createDOMEvent(eventString: String, options?: TriggerOptions) {
const [eventType, modifier] = eventString.split('.')
const meta = eventTypes[eventType] || {
eventInterface: 'Event',
cancelable: true,
bubbles: true
}
// split eventString like `keydown.ctrl.shift.c` into `keydown` and array of modifiers
const [eventType, ...modifiers] = eventString.split('.')

const eventParams: EventParams = { eventType, modifier, meta, options }
const eventParams: EventParams = { eventType, modifiers, options }
const event: Event = createEvent(eventParams)
const eventPrototype = Object.getPrototypeOf(event)

// attach custom options to the event, like `relatedTarget` and so on.
options &&
Object.keys(options).forEach((key) => {
const propertyDescriptor = Object.getOwnPropertyDescriptor(
Expand Down
90 changes: 87 additions & 3 deletions tests/trigger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,48 @@ describe('trigger', () => {
expect(wrapper.find('p').text()).toBe('Count: 1')
})

it('works with right modifier', async () => {
const handler = jest.fn()
const Component = {
template: '<div @click.right="handler"/>',
methods: { handler }
}
const wrapper = mount(Component)
await wrapper.trigger('click.right')

expect(handler).toHaveBeenCalledTimes(1)
expect(handler.mock.calls[0][0].type).toBe('contextmenu')
expect(handler.mock.calls[0][0].button).toBe(2)
})

it('works with middle modifier', async () => {
const handler = jest.fn()
const Component = {
template: '<div @click.middle="handler"/>',
methods: { handler }
}
const wrapper = mount(Component)
await wrapper.trigger('click.middle')

expect(handler).toHaveBeenCalledTimes(1)
expect(handler.mock.calls[0][0].button).toBe(1)
expect(handler.mock.calls[0][0].type).toBe('mouseup')
})

it('works with meta and key modifiers', async () => {
const handler = jest.fn()
const Component = {
template: '<div @click.meta.right="handler"/>',
methods: { handler }
}
const wrapper = mount(Component)
await wrapper.trigger('click.meta.right')

expect(handler).toHaveBeenCalledTimes(1)
expect(handler.mock.calls[0][0].metaKey).toBe(true)
expect(handler.mock.calls[0][0].button).toBe(2)
})

it('causes DOM to update after a click handler method that changes components data is called', async () => {
const Component = defineComponent({
setup() {
Expand Down Expand Up @@ -105,7 +147,7 @@ describe('trigger', () => {
template: '<input @keydown.enter="keydownHandler" />',
methods: { keydownHandler }
}
const wrapper = mount(Component, {})
const wrapper = mount(Component)

// is not called when key is not 'enter'
await wrapper.trigger('keydown', { key: 'Backspace' })
Expand All @@ -115,6 +157,10 @@ describe('trigger', () => {
await wrapper.trigger('keydown', { key: 'ENTER' })
expect(keydownHandler).not.toHaveBeenCalled()

// is not called if passed keyCode instead
await wrapper.trigger('keydown', { keyCode: 13 })
expect(keydownHandler).not.toHaveBeenCalled()

// is called when key is lowercase 'enter'
await wrapper.trigger('keydown', { key: 'enter' })
expect(keydownHandler).toHaveBeenCalledTimes(1)
Expand All @@ -124,9 +170,47 @@ describe('trigger', () => {
await wrapper.trigger('keydown', { key: 'Enter' })
expect(keydownHandler).toHaveBeenCalledTimes(2)
expect(keydownHandler.mock.calls[1][0].key).toBe('Enter')

await wrapper.trigger('keydown.enter')
expect(keydownHandler).toHaveBeenCalledTimes(3)
expect(keydownHandler.mock.calls[2][0].key).toBe('enter')
})

it('overwrites key if passed as a modifier', async () => {
const keydownHandler = jest.fn()
const Component = {
template: '<input @keydown.enter="keydownHandler" />',
methods: { keydownHandler }
}
const wrapper = mount(Component)

// is called when key is lowercase 'enter'
await wrapper.trigger('keydown.enter', { key: 'up' })
expect(keydownHandler).toHaveBeenCalledTimes(1)
expect(keydownHandler.mock.calls[0][0].key).toBe('enter')
expect(keydownHandler.mock.calls[0][0].keyCode).toBe(13)
})

it('causes keydown handler to fire with multiple modifiers', async () => {
const keydownHandler = jest.fn()
const Component = {
template: '<input @keydown.ctrl.shift.left="keydownHandler" />',
methods: { keydownHandler }
}
const wrapper = mount(Component)

await wrapper.trigger('keydown.ctrl.shift.left')

expect(keydownHandler).toHaveBeenCalledTimes(1)

let event = keydownHandler.mock.calls[0][0]
expect(event.key).toBe('left')
expect(event.shiftKey).toBe(true)
expect(event.ctrlKey).toBe(true)
expect(event.ctrlKey).toBe(true)
})

it('causes keydown handler to fire with the appropiate keyCode when wrapper.trigger("keydown", { keyCode: 65 }) is fired', async () => {
it('causes keydown handler to fire with the appropriate keyCode when wrapper.trigger("keydown", { keyCode: 65 }) is fired', async () => {
const keydownHandler = jest.fn()
const Component = {
template: '<input @keydown="keydownHandler" />',
Expand All @@ -139,7 +223,7 @@ describe('trigger', () => {
expect(keydownHandler.mock.calls[0][0].keyCode).toBe(65)
})

it('causes keydown handler to fire converting keyName in an apropiate keyCode when wrapper.trigger("keydown.${keyName}") is fired', async () => {
it('causes keydown handler to fire converting keyName in an appropriate keyCode when wrapper.trigger("keydown.${keyName}") is fired', async () => {
let keydownHandler = jest.fn()

const Component = {
Expand Down