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}`);