Fix keyboard and space handling in the rich text editor suggestions popup

What does this MR do and why?

In the rich text editor, typing a colon after a space (standard French typography, and common in English too) opens the emoji popup and leaves it armed for the rest of the sentence. While it is open, Enter does nothing instead of starting a new paragraph, Tab does nothing, and Enter inserts the highlighted match over the colon. In the create work item modal, Escape closes the whole modal and discards the draft.

Root cause, in one line each: the dropdown returned "handled" for Enter and Tab even with nothing highlighted; ProseMirror calls preventDefault but not stopPropagation, so a consumed Escape still bubbled to the modal's own handler; and every trigger was configured with allowSpaces: true, so the : query kept growing past the space and matched every emoji whose description contains one.

Three changes:

  1. Enter and Tab fall through to the editor when no item is highlighted, and the popup hides on fall-through.
  2. Escape stops propagating once the popup consumes it, so it dismisses the popup and not the surrounding dialog. A second Escape reaches the form as before.
  3. A space typed immediately after a trigger character dismisses the popup, for every trigger. No autocompleted name starts with a space, so that space means prose rather than a query. Spaces later in the query still search, so :smiling narrows the smiling faces and #login page still finds an issue by title.

Unchanged: multi-word search for every trigger (@john doe, ~"UX Paper Cuts", %Next release), arrow-key navigation, :smile plus Enter, and the rule that a colon inside a URL or a time never opens a popup.

The plain text editor already behaves this way: at.js hides its list and lets Enter or Tab through when nothing is highlighted, and the modal's cancel handler already skips itself while at.js is open. This MR gives the rich text popup the equivalent.

One difference is deliberate. at.js dismisses at any space, because GfmAutoComplete.defaultMatcher reads only the last whitespace-delimited token, which is why the plain text editor cannot search a reference by multi-word title. The rich text editor can, so it dismisses on a leading space only and keeps that capability.

Resolves #602797 (closed)

Related to #458075

Evidence

  • Jest: 58 of 223 examples across the three touched specs fail at the merge base and pass here (Vue 2). Whole content_editor tree green: Vue 2 1340 examples across 126 suites, Vue 3 1322 passed with 18 skipped. The new plugin-level spec is the skipped suite under Vue 3 (VueRenderer.updateProps cannot mutate props under @vue/compat), the gap spec/frontend/vue3migration/tiptap_spec.js documents.
  • Feature examples: four added (three to the shared autocomplete group, one to the work item create modal spec). All four fail on origin/master with the two source files swapped, each at the assertion naming the defect. Predictive RSpec may not map this JavaScript change to them, so they first run at tier-2 or with pipeline:run-all-rspec.
  • Screenshots and recordings below: 13 matrix rows and 2 GIF stories, real headless Chrome, origin/master 26225e7e against this branch.
Trigger configuration after this MR (every trigger behaves the same way)
Trigger Node Spaces inside the query Space right after the trigger
@ reference (user) searched (@john doe) dismisses
#, [issue:, [work_item: reference (issue, work item) searched (title) dismisses
! reference (merge request) searched (title) dismisses
&, [epic: reference (epic) searched (title) dismisses
$ reference (snippet) searched (title) dismisses
[vulnerability: reference (vulnerability) searched (title) dismisses
~ referenceLabel searched (~"UX Paper Cuts") dismisses
% reference (milestone) searched (title) dismisses
*iteration: reference (iteration) searched (title) dismisses
" reference (status, type) searched ("In progress") dismisses
: emoji searched (:smiling ) dismisses
[[ link (wiki) searched (page titles) dismisses
/ reference (command), startOfLine searched (title) dismisses

Screenshots or screen recordings

Scenario Before After
Fix: colon typed after a space, then Enter, then a line of text 01_fix_enter_after_colon 01_fix_enter_after_colon
Fix: colon, then a space (the popup and its highlighted first match) 02_fix_popup_after_space 02_fix_popup_after_space
Fix: colon, space, Enter, then a line of text 03_fix_enter_after_space 03_fix_enter_after_space
Fix: create work item modal, colon after a space, then Escape 04_fix_escape_in_create_modal 04_fix_escape_in_create_modal
Edge: colon at the end of a list item, then Enter, then text 05_edge_list_item_colon_enter 05_edge_list_item_colon_enter
Edge (unchanged): :smile then Enter inserts the highlighted emoji 06_edge_query_enter_inserts_emoji 06_edge_query_enter_inserts_emoji
Edge (unchanged): a colon inside a URL never opens the popup 07_edge_url_with_colon 07_edge_url_with_colon
Edge (unchanged): a colon inside a time never opens the popup 08_edge_time_range 08_edge_time_range
Unchanged: Escape in a plain description field, then more typing 09_same_escape_in_plain_field 09_same_escape_in_plain_field
Unchanged: @ plus a partial username, then Enter 10_same_mention_enter 10_same_mention_enter
Unchanged: ~ plus a partial label containing a space, then Enter 11_same_label_with_space 11_same_label_with_space
Unchanged: % plus a partial milestone containing a space, then Enter 12_same_milestone_with_space 12_same_milestone_with_space
Unchanged: /lab then Enter inserts the quick action 13_same_quick_action_enter 13_same_quick_action_enter

In rows 06 to 13 content, popup state and serialized markdown match on both refs; the only differing pixels are the project avatar tile (its colour derives from the project id) and stray edge pixels.

Recordings

Story Before After
A colon after a space, then a space, then Enter and a next line (the popup shows A button (blood type) highlighted on master and Enter inserts it in place of the colon and space; on this branch the popup closes at the space and Enter starts a new paragraph) sentence_before sentence_after
The create work item modal: a colon after a space, Escape, Escape again (on master the first Escape closes the modal and the draft is gone; on this branch the first Escape closes only the popup and the second reaches the form's cancel flow, which asks to confirm discarding) modal_before modal_after

Three beats per side; the first beat is the same state on both refs and the stories diverge at the second beat.

How to set up and validate locally

  1. On any new issue page, switch the description to rich text.
  2. Type Steps to reproduce : then Enter, then Open the page. Before: the text lands on the same line. After: a new paragraph.
  3. Type Statut : then a space. Before: the popup stays open with A button (blood type) highlighted, and Enter inserts it. After: the popup closes at the space.
  4. Type Bonjour ! then a space. Before: the merge request list stays open for the rest of the sentence. After: it closes at the space.
  5. From the sidebar + menu choose New work item, switch to rich text, type Draft description : and press Escape. Before: the modal closes and the draft is gone. After: only the popup closes; a second Escape runs the cancel flow as before.
  6. Regressions: :smile, @, ~ with a space, % with a space and /lab all still complete on Enter, and multi-word search still works for every trigger; a colon in https://gitlab.com or 10:30-11:00 opens no popup.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Paul W

Merge request reports

Loading
Loading