diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue index 28f9f8109880cd78e33ad999c904493ee57a7858..f13eb399fd1442c3c89effc01907ed90cdb3c773 100644 --- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue +++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue @@ -8,6 +8,7 @@ import { inputPlaceholderConfidentialTextMap, inputPlaceholderTextMap, } from '../constants'; +import { ENTER_KEY, TAB_KEY } from '../../lib/utils/keys'; import IssueToken from './issue_token.vue'; const SPACE_FACTOR = 1; @@ -167,6 +168,13 @@ export default { onFocus() { this.isInputFocused = true; }, + onKeydown(event) { + if ([ENTER_KEY, TAB_KEY].includes(event.key)) { + const { value } = this.$refs.input; + + this.$emit('addIssuableFinishEntry', { value, event }); + } + }, setupAutoComplete() { const $input = $(this.$refs.input); @@ -231,6 +239,7 @@ export default { @input="onInput" @focus="onFocus" @blur="onBlur" + @keydown="onKeydown" @keyup.escape.exact="$emit('addIssuableFormCancel')" /> diff --git a/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue b/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue index 3d2c7be84f9be9e2b820a0df6e992b10995b0229..a5f79e0505569beb1c9969030ddb0e298fac86a1 100644 --- a/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue +++ b/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue @@ -1,4 +1,5 @@ @@ -75,6 +88,7 @@ export default { @addIssuableFormInput="onAddIssuable" @pendingIssuableRemoveRequest="removeReference" @addIssuableFormBlur="onBlur" + @addIssuableFinishEntry="onKeyFinish" /> { expect(wrapper.vm.references).toHaveLength(0); }); + describe('"finish" keystrokes (Enter or Tab)', () => { + const mockEvent = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + ctrlKey: false, + key: ENTER_KEY, + metaKey: false, + }; + + beforeEach(() => { + mockEvent.ctrlKey = false; + mockEvent.key = ENTER_KEY; + mockEvent.metaKey = false; + mockEvent.preventDefault.mockReset(); + mockEvent.stopPropagation.mockReset(); + }); + + it.each` + description | event + ${'tabs'} | ${mockEvent} + ${'enters'} | ${mockEvent} + `('prevent the default event behavior for $description', ({ event }) => { + createComponent(); + + getInput().vm.$emit('addIssuableFinishEntry', { value: 'x', event }); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(event.stopPropagation).toHaveBeenCalledTimes(1); + }); + + it('do not add empty references', () => { + createComponent(); + + getInput().vm.$emit('addIssuableFinishEntry', { value: '', event: mockEvent }); + + expect(wrapper.vm.references).toHaveLength(0); + }); + + it('add new tokens', () => { + createComponent(); + + getInput().vm.$emit('addIssuableFinishEntry', { value: '!1', event: mockEvent }); + getInput().vm.$emit('addIssuableFinishEntry', { value: '!2', event: mockEvent }); + + expect(wrapper.vm.references).toEqual(['!1', '!2']); + }); + + describe('with modifiers', () => { + it.each` + modifier | event + ${'Cmd'} | ${{ ...mockEvent, metaKey: true, key: TAB_KEY }} + ${'Ctrl'} | ${{ ...mockEvent, ctrlKey: true, key: TAB_KEY }} + `('$modifier does not affect the Tab handler', ({ event }) => { + createComponent(); + + getInput().vm.$emit('addIssuableFinishEntry', { value: '!1', event }); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(event.stopPropagation).toHaveBeenCalledTimes(1); + expect(wrapper.vm.references).toEqual(['!1']); + }); + + it.each` + modifier | event + ${'Cmd'} | ${{ ...mockEvent, metaKey: true }} + ${'Ctrl'} | ${{ ...mockEvent, ctrlKey: true }} + `('$modifier skips the special handler for Enter', ({ event }) => { + createComponent(); + + getInput().vm.$emit('addIssuableFinishEntry', { value: '!1', event }); + + expect(event.preventDefault).toHaveBeenCalledTimes(0); + expect(event.stopPropagation).toHaveBeenCalledTimes(0); + expect(wrapper.vm.references).toEqual([]); + }); + }); + }); + describe('hidden inputs', () => { const createHiddenInputExpectation = (selector) => (bool) => { expect(wrapper.find(selector).element.value).toBe(`${bool}`);