diff --git a/src/create-dom-event.ts b/src/create-dom-event.ts index b57eb26a4..5616021dc 100644 --- a/src/create-dom-event.ts +++ b/src/create-dom-event.ts @@ -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, @@ -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( diff --git a/tests/trigger.spec.ts b/tests/trigger.spec.ts index 894357ded..9fb3f9a26 100644 --- a/tests/trigger.spec.ts +++ b/tests/trigger.spec.ts @@ -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: '
', + 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: '
', + 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: '
', + 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() { @@ -105,7 +147,7 @@ describe('trigger', () => { template: '', methods: { keydownHandler } } - const wrapper = mount(Component, {}) + const wrapper = mount(Component) // is not called when key is not 'enter' await wrapper.trigger('keydown', { key: 'Backspace' }) @@ -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) @@ -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: '', + 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: '', + 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: '', @@ -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 = {