Skip to content

Commit 7905fe6

Browse files
Fix: trigger not working with some dot.notation modifiers (#125)
* chore: add failing test * fix: allow using key and complex modifiers directly in trigger eventString * chore: add extra test * chore: cleanup
1 parent 656adc2 commit 7905fe6

File tree

2 files changed

+180
-22
lines changed

2 files changed

+180
-22
lines changed

src/create-dom-event.ts

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,47 @@ interface TriggerOptions {
99

1010
interface EventParams {
1111
eventType: string
12-
modifier: string
13-
meta: any
12+
modifiers: string[]
1413
options?: TriggerOptions
1514
}
1615

16+
// modifiers to keep an eye on
17+
const ignorableKeyModifiers = ['stop', 'prevent', 'self', 'exact']
18+
const systemKeyModifiers = ['ctrl', 'shift', 'alt', 'meta']
19+
const mouseKeyModifiers = ['left', 'middle', 'right']
20+
21+
/**
22+
* Groups modifiers into lists
23+
*/
24+
function generateModifiers(modifiers: string[], isOnClick: boolean) {
25+
const keyModifiers: string[] = []
26+
const systemModifiers: string[] = []
27+
28+
for (let i = 0; i < modifiers.length; i++) {
29+
const modifier = modifiers[i]
30+
31+
// addEventListener() options, e.g. .passive & .capture, that we dont need to handle
32+
if (ignorableKeyModifiers.includes(modifier)) {
33+
continue
34+
}
35+
// modifiers that require special conversion
36+
// if passed a left/right key modifier with onClick, add it here as well.
37+
if (
38+
systemKeyModifiers.includes(modifier) ||
39+
(mouseKeyModifiers.includes(modifier) && isOnClick)
40+
) {
41+
systemModifiers.push(modifier)
42+
} else {
43+
keyModifiers.push(modifier)
44+
}
45+
}
46+
47+
return {
48+
keyModifiers,
49+
systemModifiers
50+
}
51+
}
52+
1753
export const keyCodesByKeyName = {
1854
backspace: 8,
1955
tab: 9,
@@ -33,52 +69,90 @@ export const keyCodesByKeyName = {
3369
}
3470

3571
function getEventProperties(eventParams: EventParams) {
36-
const { modifier, meta, options } = eventParams
72+
let { modifiers, options = {}, eventType } = eventParams
73+
74+
let isOnClick = eventType === 'click'
75+
76+
const { keyModifiers, systemModifiers } = generateModifiers(
77+
modifiers,
78+
isOnClick
79+
)
80+
81+
if (isOnClick) {
82+
// if it's a right click, it should fire a `contextmenu` event
83+
if (systemModifiers.includes('right')) {
84+
eventType = 'contextmenu'
85+
options.button = 2
86+
// if its a middle click, fire a `mouseup` event
87+
} else if (systemModifiers.includes('middle')) {
88+
eventType = 'mouseup'
89+
options.button = 1
90+
}
91+
}
92+
93+
const meta = eventTypes[eventType] || {
94+
eventInterface: 'Event',
95+
cancelable: true,
96+
bubbles: true
97+
}
98+
99+
// convert `shift, ctrl` to `shiftKey, ctrlKey`
100+
// allows trigger('keydown.shift.ctrl.n') directly
101+
const systemModifiersMeta = systemModifiers.reduce((all, key) => {
102+
all[`${key}Key`] = true
103+
return all
104+
}, {})
105+
106+
// get the keyCode for backwards compat
37107
const keyCode =
38-
keyCodesByKeyName[modifier] ||
108+
keyCodesByKeyName[keyModifiers[0]] ||
39109
(options && (options.keyCode || options.code))
40110

41-
return {
111+
const eventProperties = {
112+
...systemModifiersMeta, // shiftKey, metaKey etc
42113
...options, // What the user passed in as the second argument to #trigger
43114
bubbles: meta.bubbles,
44115
meta: meta.cancelable,
45116
// Any derived options should go here
46117
keyCode,
47-
code: keyCode
118+
code: keyCode,
119+
// if we have a `key`, use it, otherwise dont set anything (allows user to pass custom key)
120+
...(keyModifiers[0] ? { key: keyModifiers[0] } : {})
121+
}
122+
123+
return {
124+
eventProperties,
125+
meta,
126+
eventType
48127
}
49128
}
50129

51130
function createEvent(eventParams: EventParams) {
52-
const { eventType, meta } = eventParams
131+
const { eventProperties, meta, eventType } = getEventProperties(eventParams)
132+
133+
// user defined eventInterface
53134
const metaEventInterface = window[meta.eventInterface]
54135

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

58-
const eventProperties = getEventProperties(eventParams)
59-
60-
const event = new SupportedEventInterface(
139+
return new SupportedEventInterface(
61140
eventType,
62141
// event properties can only be added when the event is instantiated
63142
// custom properties must be added after the event has been instantiated
64143
eventProperties
65144
)
66-
67-
return event
68145
}
69146

70147
function createDOMEvent(eventString: String, options?: TriggerOptions) {
71-
const [eventType, modifier] = eventString.split('.')
72-
const meta = eventTypes[eventType] || {
73-
eventInterface: 'Event',
74-
cancelable: true,
75-
bubbles: true
76-
}
148+
// split eventString like `keydown.ctrl.shift.c` into `keydown` and array of modifiers
149+
const [eventType, ...modifiers] = eventString.split('.')
77150

78-
const eventParams: EventParams = { eventType, modifier, meta, options }
151+
const eventParams: EventParams = { eventType, modifiers, options }
79152
const event: Event = createEvent(eventParams)
80153
const eventPrototype = Object.getPrototypeOf(event)
81154

155+
// attach custom options to the event, like `relatedTarget` and so on.
82156
options &&
83157
Object.keys(options).forEach((key) => {
84158
const propertyDescriptor = Object.getOwnPropertyDescriptor(

tests/trigger.spec.ts

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,48 @@ describe('trigger', () => {
5050
expect(wrapper.find('p').text()).toBe('Count: 1')
5151
})
5252

53+
it('works with right modifier', async () => {
54+
const handler = jest.fn()
55+
const Component = {
56+
template: '<div @click.right="handler"/>',
57+
methods: { handler }
58+
}
59+
const wrapper = mount(Component)
60+
await wrapper.trigger('click.right')
61+
62+
expect(handler).toHaveBeenCalledTimes(1)
63+
expect(handler.mock.calls[0][0].type).toBe('contextmenu')
64+
expect(handler.mock.calls[0][0].button).toBe(2)
65+
})
66+
67+
it('works with middle modifier', async () => {
68+
const handler = jest.fn()
69+
const Component = {
70+
template: '<div @click.middle="handler"/>',
71+
methods: { handler }
72+
}
73+
const wrapper = mount(Component)
74+
await wrapper.trigger('click.middle')
75+
76+
expect(handler).toHaveBeenCalledTimes(1)
77+
expect(handler.mock.calls[0][0].button).toBe(1)
78+
expect(handler.mock.calls[0][0].type).toBe('mouseup')
79+
})
80+
81+
it('works with meta and key modifiers', async () => {
82+
const handler = jest.fn()
83+
const Component = {
84+
template: '<div @click.meta.right="handler"/>',
85+
methods: { handler }
86+
}
87+
const wrapper = mount(Component)
88+
await wrapper.trigger('click.meta.right')
89+
90+
expect(handler).toHaveBeenCalledTimes(1)
91+
expect(handler.mock.calls[0][0].metaKey).toBe(true)
92+
expect(handler.mock.calls[0][0].button).toBe(2)
93+
})
94+
5395
it('causes DOM to update after a click handler method that changes components data is called', async () => {
5496
const Component = defineComponent({
5597
setup() {
@@ -105,7 +147,7 @@ describe('trigger', () => {
105147
template: '<input @keydown.enter="keydownHandler" />',
106148
methods: { keydownHandler }
107149
}
108-
const wrapper = mount(Component, {})
150+
const wrapper = mount(Component)
109151

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

160+
// is not called if passed keyCode instead
161+
await wrapper.trigger('keydown', { keyCode: 13 })
162+
expect(keydownHandler).not.toHaveBeenCalled()
163+
118164
// is called when key is lowercase 'enter'
119165
await wrapper.trigger('keydown', { key: 'enter' })
120166
expect(keydownHandler).toHaveBeenCalledTimes(1)
@@ -124,9 +170,47 @@ describe('trigger', () => {
124170
await wrapper.trigger('keydown', { key: 'Enter' })
125171
expect(keydownHandler).toHaveBeenCalledTimes(2)
126172
expect(keydownHandler.mock.calls[1][0].key).toBe('Enter')
173+
174+
await wrapper.trigger('keydown.enter')
175+
expect(keydownHandler).toHaveBeenCalledTimes(3)
176+
expect(keydownHandler.mock.calls[2][0].key).toBe('enter')
177+
})
178+
179+
it('overwrites key if passed as a modifier', async () => {
180+
const keydownHandler = jest.fn()
181+
const Component = {
182+
template: '<input @keydown.enter="keydownHandler" />',
183+
methods: { keydownHandler }
184+
}
185+
const wrapper = mount(Component)
186+
187+
// is called when key is lowercase 'enter'
188+
await wrapper.trigger('keydown.enter', { key: 'up' })
189+
expect(keydownHandler).toHaveBeenCalledTimes(1)
190+
expect(keydownHandler.mock.calls[0][0].key).toBe('enter')
191+
expect(keydownHandler.mock.calls[0][0].keyCode).toBe(13)
192+
})
193+
194+
it('causes keydown handler to fire with multiple modifiers', async () => {
195+
const keydownHandler = jest.fn()
196+
const Component = {
197+
template: '<input @keydown.ctrl.shift.left="keydownHandler" />',
198+
methods: { keydownHandler }
199+
}
200+
const wrapper = mount(Component)
201+
202+
await wrapper.trigger('keydown.ctrl.shift.left')
203+
204+
expect(keydownHandler).toHaveBeenCalledTimes(1)
205+
206+
let event = keydownHandler.mock.calls[0][0]
207+
expect(event.key).toBe('left')
208+
expect(event.shiftKey).toBe(true)
209+
expect(event.ctrlKey).toBe(true)
210+
expect(event.ctrlKey).toBe(true)
127211
})
128212

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

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

145229
const Component = {

0 commit comments

Comments
 (0)