Prevent looping of cursor position when CTRL+ARROW_LEFT/ARROW_RIGHT is pressed.

Change-Id: I3df70a614a490779155083b99fa0460bd3eb2bd8
Release-Notes: skip
Google-Bug-Id: b/340968595
diff --git a/polygerrit-ui/app/embed/gr-textarea.ts b/polygerrit-ui/app/embed/gr-textarea.ts
index 05bef49..27948fd 100644
--- a/polygerrit-ui/app/embed/gr-textarea.ts
+++ b/polygerrit-ui/app/embed/gr-textarea.ts
@@ -173,6 +173,8 @@
 
   private focused = false;
 
+  private currentCursorPosition = -1;
+
   private readonly isPlaintextOnlySupported = supportsPlainTextEditing();
 
   static override get styles() {
@@ -488,6 +490,19 @@
       event.preventDefault();
       this.fire('saveShortcut');
     }
+    // Prevent looping of cursor position when CTRL+ARROW_LEFT/ARROW_RIGHT is
+    // pressed.
+    if (event.ctrlKey || event.metaKey || event.altKey) {
+      if (event.key === 'ArrowLeft' && this.currentCursorPosition === 0) {
+        event.preventDefault();
+      }
+      if (
+        event.key === 'ArrowRight' &&
+        this.currentCursorPosition === (this.value?.length ?? 0)
+      ) {
+        event.preventDefault();
+      }
+    }
     await this.toggleHintVisibilityIfAny();
   }
 
@@ -597,7 +612,9 @@
   }
 
   private onCursorPositionChange() {
-    this.fire('cursorPositionChange', {position: this.getCursorPosition()});
+    const cursorPosition = this.getCursorPosition();
+    this.fire('cursorPositionChange', {position: cursorPosition});
+    this.currentCursorPosition = cursorPosition;
   }
 
   private async updateValueInDom() {
diff --git a/polygerrit-ui/app/embed/gr-textarea_test.ts b/polygerrit-ui/app/embed/gr-textarea_test.ts
index b701dcb..d125d2f 100644
--- a/polygerrit-ui/app/embed/gr-textarea_test.ts
+++ b/polygerrit-ui/app/embed/gr-textarea_test.ts
@@ -232,4 +232,56 @@
 
     assert.equal(element.value, oldValue + hint);
   });
+
+  test('when cursor is at end, Mod + ArrowRight does not change cursor position', async () => {
+    const CURSOR_POSITION_CHANGE_EVENT = 'cursorPositionChange';
+    let cursorPosition = -1;
+    const value = 'Hola amigos';
+    const editableDiv = element.shadowRoot!.querySelector(
+      '.editableDiv'
+    ) as HTMLDivElement;
+    element.addEventListener(CURSOR_POSITION_CHANGE_EVENT, (event: Event) => {
+      const detail = (event as CustomEvent<CursorPositionChangeEventDetail>)
+        .detail;
+      cursorPosition = detail.position;
+    });
+    await element.updateComplete;
+    element.value = value;
+    await element.putCursorAtEnd();
+    await element.updateComplete;
+
+    editableDiv.dispatchEvent(
+      new KeyboardEvent('keydown', {key: 'ArrowRight', metaKey: true})
+    );
+    await element.updateComplete;
+    await rafPromise();
+
+    assert.equal(cursorPosition, value.length);
+  });
+
+  test('when cursor is at 0, Mod + ArrowLeft does not change cursor position', async () => {
+    const CURSOR_POSITION_CHANGE_EVENT = 'cursorPositionChange';
+    let cursorPosition = -1;
+    const value = 'Hola amigos';
+    const editableDiv = element.shadowRoot!.querySelector(
+      '.editableDiv'
+    ) as HTMLDivElement;
+    element.addEventListener(CURSOR_POSITION_CHANGE_EVENT, (event: Event) => {
+      const detail = (event as CustomEvent<CursorPositionChangeEventDetail>)
+        .detail;
+      cursorPosition = detail.position;
+    });
+    await element.updateComplete;
+    element.value = value;
+    element.setCursorPosition(0);
+    await element.updateComplete;
+
+    editableDiv.dispatchEvent(
+      new KeyboardEvent('keydown', {key: 'ArrowLeft', metaKey: true})
+    );
+    await element.updateComplete;
+    await rafPromise();
+
+    assert.equal(cursorPosition, 0);
+  });
 });