diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index c3c28aeafc0699f875d74fd2b646e2181bc41df0..07fd6dae76aeb8952af5156bf8aef4ab36734606 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -43,7 +43,7 @@ function genericSuccess(e) {
 }
 
 /**
- * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
+ * Safari < 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
  * See http://clipboardjs.com/#browser-support
  */
 function genericError(e) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 96d019f62f234cdb47d1752e9d780cda8194f339..7ff623a1adcde20ed0e858a623e1293d51d5ca06 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -282,23 +282,51 @@ export const getSelectedFragment = (restrictToNode) => {
   return documentFragment;
 };
 
+function execInsertText(text) {
+  if (text === '') return document.execCommand('delete');
+
+  return document.execCommand('insertText', false, text);
+}
+
+/**
+ * This method inserts text into a textarea/input field.
+ * Uses `execCommand` if supported
+ *
+ * @param {HTMLElement} target - textarea/input to have text inserted into
+ * @param {String | function} text - text to be inserted
+ */
 export const insertText = (target, text) => {
-  // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
   const { selectionStart, selectionEnd, value } = target;
-
   const textBefore = value.substring(0, selectionStart);
   const textAfter = value.substring(selectionEnd, value.length);
-
   const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
-  const newText = textBefore + insertedText + textAfter;
 
-  // eslint-disable-next-line no-param-reassign
-  target.value = newText;
-  // eslint-disable-next-line no-param-reassign
-  target.selectionStart = selectionStart + insertedText.length;
-
-  // eslint-disable-next-line no-param-reassign
-  target.selectionEnd = selectionStart + insertedText.length;
+  // The `execCommand` is officially deprecated.  However, for `insertText`,
+  // there is currently no alternative. We need to use it in order to trigger
+  // the browser's undo tracking when we insert text.
+  // Per https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand on 2022-04-11,
+  //   The Clipboard API can be used instead of execCommand in many cases,
+  //   but execCommand is still sometimes useful. In particular, the Clipboard
+  //   API doesn't replace the insertText command
+  // So we attempt to use it if possible. Otherwise, fall back to just replacing
+  // the value as before. In this case, Undo will be broken with inserted text.
+  // Testing on older versions of Firefox:
+  //   87 and below: does not work and falls through to just replacing value.
+  //     87 was released in Mar of 2021
+  //   89 and above: works well
+  //     89 was released in May of 2021
+  if (!execInsertText(insertedText)) {
+    const newText = textBefore + insertedText + textAfter;
+
+    // eslint-disable-next-line no-param-reassign
+    target.value = newText;
+
+    // eslint-disable-next-line no-param-reassign
+    target.selectionStart = selectionStart + insertedText.length;
+
+    // eslint-disable-next-line no-param-reassign
+    target.selectionEnd = selectionStart + insertedText.length;
+  }
 
   // Trigger autosave
   target.dispatchEvent(new Event('input'));
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index c96db09cc762fa4fc1c276ba8b1eba99cb4e9aa7..2032faa1c33621a5fe2519349f1ba829a15be95c 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -8,6 +8,9 @@ describe('CopyAsGFM', () => {
     beforeEach(() => {
       target = document.createElement('input');
       target.value = 'This is code: ';
+
+      // needed for the underlying insertText to work
+      document.execCommand = jest.fn(() => false);
     });
 
     // When GFM code is copied, we put the regular plain text
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 763a9bd30fe8040fcacf8fc7a961f30e7511a574..8e499844406971e18756a50a4d0d194732d7ba9f 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -283,6 +283,75 @@ describe('common_utils', () => {
     });
   });
 
+  describe('insertText', () => {
+    let textArea;
+
+    beforeAll(() => {
+      textArea = document.createElement('textarea');
+      document.querySelector('body').appendChild(textArea);
+      textArea.value = 'two';
+      textArea.setSelectionRange(0, 0);
+      textArea.focus();
+    });
+
+    afterAll(() => {
+      textArea.parentNode.removeChild(textArea);
+    });
+
+    describe('using execCommand', () => {
+      beforeAll(() => {
+        document.execCommand = jest.fn(() => true);
+      });
+
+      it('inserts the text', () => {
+        commonUtils.insertText(textArea, 'one');
+
+        expect(document.execCommand).toHaveBeenCalledWith('insertText', false, 'one');
+      });
+
+      it('removes selected text', () => {
+        textArea.setSelectionRange(0, textArea.value.length);
+
+        commonUtils.insertText(textArea, '');
+
+        expect(document.execCommand).toHaveBeenCalledWith('delete');
+      });
+    });
+
+    describe('using fallback', () => {
+      beforeEach(() => {
+        document.execCommand = jest.fn(() => false);
+        jest.spyOn(textArea, 'dispatchEvent');
+        textArea.value = 'two';
+        textArea.setSelectionRange(0, 0);
+      });
+
+      it('inserts the text', () => {
+        commonUtils.insertText(textArea, 'one');
+
+        expect(textArea.value).toBe('onetwo');
+        expect(textArea.dispatchEvent).toHaveBeenCalled();
+      });
+
+      it('replaces the selection', () => {
+        textArea.setSelectionRange(0, textArea.value.length);
+
+        commonUtils.insertText(textArea, 'one');
+
+        expect(textArea.value).toBe('one');
+        expect(textArea.selectionStart).toBe(textArea.value.length);
+      });
+
+      it('removes selected text', () => {
+        textArea.setSelectionRange(0, textArea.value.length);
+
+        commonUtils.insertText(textArea, '');
+
+        expect(textArea.value).toBe('');
+      });
+    });
+  });
+
   describe('normalizedHeaders', () => {
     it('should upperCase all the header keys to keep them consistent', () => {
       const apiHeaders = {
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 99204639197907c7cdc9c2dbbff431b674160194..cc7d759274d78f4e6e4aac315a1420d6b1a39e01 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -14,6 +14,9 @@ describe('init markdown', () => {
     textArea = document.createElement('textarea');
     document.querySelector('body').appendChild(textArea);
     textArea.focus();
+
+    // needed for the underlying insertText to work
+    document.execCommand = jest.fn(() => false);
   });
 
   afterAll(() => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index d1c4d777d447d466f98f042f563b32dfaf6d600b..ab74ea868a2d51eb50b421d942991f548e7824c8 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -187,6 +187,11 @@ describe('Markdown field component', () => {
     });
 
     describe('markdown buttons', () => {
+      beforeEach(() => {
+        // needed for the underlying insertText to work
+        document.execCommand = jest.fn(() => false);
+      });
+
       it('converts single words', async () => {
         const textarea = subject.find('textarea').element;
         textarea.setSelectionRange(0, 7);