Repository: tinymce/tinymce
Pull request #10253: TINY-11986: Adding simulating typing into input elements to agar
Author: login: spocke
Created: 2025-03-26T14:13:32Z | Merged: 2025-03-28T12:39:00Z

Related Ticket: TINY-11986

Description of Changes:
* Added new `UiControls.pType` that lets you simulate typing into textarea/input fields that support it

Pre-checks:
* [x] Changelog entry added
* [x] Tests have been added (if applicable)
* [x] Branch prefixed with `feature/`, `hotfix/` or `spike/`

Review:
* [x] Milestone set
* [x] Docs ticket created (if applicable)

GitHub issues (if applicable):


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Enhanced text input simulation now accurately mimics natural keyboard interactions in input and textarea fields.
	- New functionality added to UI controls for simulating typing text into input elements at specified speeds.
	- Introduced new methods for handling keyboard interactions, improving user experience with input fields.
	- Comprehensive mapping of keyboard keys and their respective key codes has been added for better event handling.

- **Tests**
	- Added a new test suite to validate keyboard interactions within input elements, ensuring reliability and correctness of the new features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/modules/agar/src/main/ts/ephox/agar/api/Keyboard.ts b/modules/agar/src/main/ts/ephox/agar/api/Keyboard.ts
index be996764149..0fbdb8353df 100644
--- a/modules/agar/src/main/ts/ephox/agar/api/Keyboard.ts
+++ b/modules/agar/src/main/ts/ephox/agar/api/Keyboard.ts
@@ -2,6 +2,7 @@ import { Arr } from '@ephox/katamari';
 import { Focus, SugarElement, Traverse } from '@ephox/sugar';
 
 import { keyevent, MixedKeyModifiers } from '../keyboard/FakeKeys';
+import * as TypeInInput from '../keyboard/TypeInInput';
 import { Step } from './Step';
 
 export type KeyModifiers = MixedKeyModifiers;
@@ -49,6 +50,8 @@ const sKeyup = sFakeKey(keyupTypes);
 const sKeypress = sFakeKey(keypressTypes);
 const sKeystroke = sFakeKey(keystrokeTypes);
 
+const pTypeTextInInput = TypeInInput.pTypeTextInInput;
+
 export {
   keydown,
   keyup,
@@ -63,5 +66,6 @@ export {
   sKeydown,
   sKeyup,
   sKeypress,
-  sKeystroke
+  sKeystroke,
+  pTypeTextInInput
 };
diff --git a/modules/agar/src/main/ts/ephox/agar/api/UiControls.ts b/modules/agar/src/main/ts/ephox/agar/api/UiControls.ts
index f4874e59a2f..1bb6e56d998 100644
--- a/modules/agar/src/main/ts/ephox/agar/api/UiControls.ts
+++ b/modules/agar/src/main/ts/ephox/agar/api/UiControls.ts
@@ -2,6 +2,7 @@ import { Type } from '@ephox/katamari';
 import { SugarElement, Value } from '@ephox/sugar';
 
 import { Chain } from './Chain';
+import * as Keyboard from './Keyboard';
 import { Step } from './Step';
 import * as UiFinder from './UiFinder';
 
@@ -43,6 +44,13 @@ const sSetValue = <T>(element: SugarElement<TogglableElement>, newValue: string)
 const sSetValueOn = <T>(container: SugarElement<Node>, selector: string, newValue: string): Step<T, T> =>
   Step.sync(() => setValueOn(container, selector, newValue));
 
+const pType = Keyboard.pTypeTextInInput;
+
+const pTypeOn = async (container: SugarElement<Node>, selector: string, text: string, speed: number = 0): Promise<void> => {
+  const input = UiFinder.findIn<HTMLTextAreaElement | HTMLInputElement>(container, selector).getOrDie();
+  await Keyboard.pTypeTextInInput(input, text, speed);
+};
+
 export {
   setValue,
   setValueOn,
@@ -52,5 +60,8 @@ export {
   sSetValue,
 
   cSetValue,
-  cGetValue
+  cGetValue,
+
+  pType,
+  pTypeOn
 };
diff --git a/modules/agar/src/main/ts/ephox/agar/keyboard/Keycodes.ts b/modules/agar/src/main/ts/ephox/agar/keyboard/Keycodes.ts
new file mode 100644
index 00000000000..d1ff17a17ed
--- /dev/null
+++ b/modules/agar/src/main/ts/ephox/agar/keyboard/Keycodes.ts
@@ -0,0 +1,178 @@
+import { Arr, Optional } from '@ephox/katamari';
+
+const keys = [
+  { keyCode: 8, code: 'Backspace', key: 'Backspace' },
+  { keyCode: 9, code: 'Tab', key: 'Tab' },
+  { keyCode: 13, code: 'Enter', key: 'Enter' },
+  { keyCode: 16, code: 'ShiftLeft', key: 'Shift', shiftKey: true },
+  { keyCode: 16, code: 'ShiftRight', key: 'Shift', shiftKey: true },
+  { keyCode: 17, code: 'ControlLeft', key: 'Control' },
+  { keyCode: 17, code: 'ControlRight', key: 'Control' },
+  { keyCode: 18, code: 'AltLeft', key: 'Alt' },
+  { keyCode: 18, code: 'AltRight', key: 'Alt' },
+  { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },
+  { keyCode: 27, code: 'Escape', key: 'Escape' },
+  { keyCode: 32, code: 'Space', key: ' ', data: ' ' },
+  { keyCode: 33, code: 'PageUp', key: 'PageUp' },
+  { keyCode: 34, code: 'PageDown', key: 'PageDown' },
+  { keyCode: 35, code: 'End', key: 'End' },
+  { keyCode: 36, code: 'Home', key: 'Home' },
+  { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
+  { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
+  { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
+  { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
+  { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },
+  { keyCode: 45, code: 'Insert', key: 'Insert' },
+  { keyCode: 46, code: 'Delete', key: 'Delete' },
+  { keyCode: 48, code: 'Digit0', key: '0', data: '0' },
+  { keyCode: 48, code: 'Digit0', key: ')', data: ')', shiftKey: true },
+  { keyCode: 49, code: 'Digit1', key: '1', data: '1' },
+  { keyCode: 49, code: 'Digit1', key: '!', data: '!', shiftKey: true },
+  { keyCode: 50, code: 'Digit2', key: '2', data: '2' },
+  { keyCode: 50, code: 'Digit2', key: '@', data: '@', shiftKey: true },
+  { keyCode: 51, code: 'Digit3', key: '3', data: '3' },
+  { keyCode: 51, code: 'Digit3', key: '#', data: '#', shiftKey: true },
+  { keyCode: 52, code: 'Digit4', key: '4', data: '4' },
+  { keyCode: 52, code: 'Digit4', key: '$', data: '$', shiftKey: true },
+  { keyCode: 53, code: 'Digit5', key: '5', data: '5' },
+  { keyCode: 53, code: 'Digit5', key: '%', data: '%', shiftKey: true },
+  { keyCode: 54, code: 'Digit6', key: '6', data: '6' },
+  { keyCode: 54, code: 'Digit6', key: '^', data: '^', shiftKey: true },
+  { keyCode: 55, code: 'Digit7', key: '7', data: '7' },
+  { keyCode: 55, code: 'Digit7', key: '&', data: '&', shiftKey: true },
+  { keyCode: 56, code: 'Digit8', key: '8', data: '8' },
+  { keyCode: 56, code: 'Digit8', key: '*', data: '*', shiftKey: true },
+  { keyCode: 57, code: 'Digit9', key: '9', data: '9' },
+  { keyCode: 57, code: 'Digit9', key: '(', data: '(', shiftKey: true },
+  { keyCode: 65, code: 'KeyA', key: 'a', data: 'a' },
+  { keyCode: 65, code: 'KeyA', key: 'A', data: 'A', shiftKey: true },
+  { keyCode: 66, code: 'KeyB', key: 'b', data: 'b' },
+  { keyCode: 66, code: 'KeyB', key: 'B', data: 'B', shiftKey: true },
+  { keyCode: 67, code: 'KeyC', key: 'c', data: 'c' },
+  { keyCode: 67, code: 'KeyC', key: 'C', data: 'C', shiftKey: true },
+  { keyCode: 68, code: 'KeyD', key: 'd', data: 'd' },
+  { keyCode: 68, code: 'KeyD', key: 'D', data: 'D', shiftKey: true },
+  { keyCode: 69, code: 'KeyE', key: 'e', data: 'e' },
+  { keyCode: 69, code: 'KeyE', key: 'E', data: 'E', shiftKey: true },
+  { keyCode: 70, code: 'KeyF', key: 'f', data: 'f' },
+  { keyCode: 70, code: 'KeyF', key: 'F', data: 'F', shiftKey: true },
+  { keyCode: 71, code: 'KeyG', key: 'g', data: 'g' },
+  { keyCode: 71, code: 'KeyG', key: 'G', data: 'G', shiftKey: true },
+  { keyCode: 72, code: 'KeyH', key: 'h', data: 'h' },
+  { keyCode: 72, code: 'KeyH', key: 'H', data: 'H', shiftKey: true },
+  { keyCode: 73, code: 'KeyI', key: 'i', data: 'i' },
+  { keyCode: 73, code: 'KeyI', key: 'I', data: 'I', shiftKey: true },
+  { keyCode: 74, code: 'KeyJ', key: 'j', data: 'j' },
+  { keyCode: 74, code: 'KeyJ', key: 'J', data: 'J', shiftKey: true },
+  { keyCode: 75, code: 'KeyK', key: 'k', data: 'k' },
+  { keyCode: 75, code: 'KeyK', key: 'K', data: 'K', shiftKey: true },
+  { keyCode: 76, code: 'KeyL', key: 'l', data: 'l' },
+  { keyCode: 76, code: 'KeyL', key: 'L', data: 'L', shiftKey: true },
+  { keyCode: 77, code: 'KeyM', key: 'm', data: 'm' },
+  { keyCode: 77, code: 'KeyM', key: 'M', data: 'M', shiftKey: true },
+  { keyCode: 78, code: 'KeyN', key: 'n', data: 'n' },
+  { keyCode: 78, code: 'KeyN', key: 'N', data: 'N', shiftKey: true },
+  { keyCode: 79, code: 'KeyO', key: 'o', data: 'o' },
+  { keyCode: 79, code: 'KeyO', key: 'O', data: 'O', shiftKey: true },
+  { keyCode: 80, code: 'KeyP', key: 'p', data: 'p' },
+  { keyCode: 80, code: 'KeyP', key: 'P', data: 'P', shiftKey: true },
+  { keyCode: 81, code: 'KeyQ', key: 'q', data: 'q' },
+  { keyCode: 81, code: 'KeyQ', key: 'Q', data: 'Q', shiftKey: true },
+  { keyCode: 82, code: 'KeyR', key: 'r', data: 'r' },
+  { keyCode: 82, code: 'KeyR', key: 'R', data: 'R', shiftKey: true },
+  { keyCode: 83, code: 'KeyS', key: 's', data: 's' },
+  { keyCode: 83, code: 'KeyS', key: 'S', data: 'S', shiftKey: true },
+  { keyCode: 84, code: 'KeyT', key: 't', data: 't' },
+  { keyCode: 84, code: 'KeyT', key: 'T', data: 'T', shiftKey: true },
+  { keyCode: 85, code: 'KeyU', key: 'u', data: 'u' },
+  { keyCode: 85, code: 'KeyU', key: 'U', data: 'U', shiftKey: true },
+  { keyCode: 86, code: 'KeyV', key: 'v', data: 'v' },
+  { keyCode: 86, code: 'KeyV', key: 'V', data: 'V', shiftKey: true },
+  { keyCode: 87, code: 'KeyW', key: 'w', data: 'w' },
+  { keyCode: 87, code: 'KeyW', key: 'W', data: 'W', shiftKey: true },
+  { keyCode: 88, code: 'KeyX', key: 'x', data: 'x' },
+  { keyCode: 88, code: 'KeyX', key: 'X', data: 'X', shiftKey: true },
+  { keyCode: 89, code: 'KeyY', key: 'y', data: 'y' },
+  { keyCode: 89, code: 'KeyY', key: 'Y', data: 'Y', shiftKey: true },
+  { keyCode: 90, code: 'KeyZ', key: 'z', data: 'z' },
+  { keyCode: 90, code: 'KeyZ', key: 'Z', data: 'Z', shiftKey: true },
+  { keyCode: 91, code: 'MetaLeft', key: 'Meta' },
+  { keyCode: 92, code: 'MetaRight', key: 'Meta' },
+  { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },
+  { keyCode: 96, code: 'Numpad0', key: '0', data: '0' },
+  { keyCode: 97, code: 'Numpad1', key: '1', data: '1' },
+  { keyCode: 98, code: 'Numpad2', key: '2', data: '2' },
+  { keyCode: 99, code: 'Numpad3', key: '3', data: '3' },
+  { keyCode: 100, code: 'Numpad4', key: '4', data: '4' },
+  { keyCode: 101, code: 'Numpad5', key: '5', data: '5' },
+  { keyCode: 102, code: 'Numpad6', key: '6', data: '6' },
+  { keyCode: 103, code: 'Numpad7', key: '7', data: '7' },
+  { keyCode: 104, code: 'Numpad8', key: '8', data: '8' },
+  { keyCode: 105, code: 'Numpad9', key: '9', data: '9' },
+  { keyCode: 106, code: 'NumpadMultiply', key: '*', data: '*' },
+  { keyCode: 107, code: 'NumpadAdd', key: '+', data: '+' },
+  { keyCode: 109, code: 'NumpadSubtract', key: '-', data: '-' },
+  { keyCode: 110, code: 'NumpadDecimal', key: '.', data: '.' },
+  { keyCode: 111, code: 'NumpadDivide', key: '/', data: '/' },
+  { keyCode: 112, code: 'F1', key: 'F1' },
+  { keyCode: 113, code: 'F2', key: 'F2' },
+  { keyCode: 114, code: 'F3', key: 'F3' },
+  { keyCode: 115, code: 'F4', key: 'F4' },
+  { keyCode: 116, code: 'F5', key: 'F5' },
+  { keyCode: 117, code: 'F6', key: 'F6' },
+  { keyCode: 118, code: 'F7', key: 'F7' },
+  { keyCode: 119, code: 'F8', key: 'F8' },
+  { keyCode: 120, code: 'F9', key: 'F9' },
+  { keyCode: 121, code: 'F10', key: 'F10' },
+  { keyCode: 122, code: 'F11', key: 'F11' },
+  { keyCode: 123, code: 'F12', key: 'F12' },
+  { keyCode: 144, code: 'NumLock', key: 'NumLock' },
+  { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },
+  { keyCode: 186, code: 'Semicolon', key: ';', data: ';' },
+  { keyCode: 187, code: 'Equal', key: '=', data: '=' },
+  { keyCode: 188, code: 'Comma', key: ',', data: ',' },
+  { keyCode: 189, code: 'Minus', key: '-', data: '-' },
+  { keyCode: 189, code: 'Minus', key: '_', data: '_', shiftKey: true },
+  { keyCode: 190, code: 'Period', key: '.', data: '.' },
+  { keyCode: 190, code: 'Period', key: '>', data: '>', shiftKey: true },
+  { keyCode: 191, code: 'Slash', key: '/', data: '/' },
+  { keyCode: 191, code: 'Slash', key: '?', data: '?', shiftKey: true },
+  { keyCode: 192, code: 'Backquote', key: '`', data: '`' },
+  { keyCode: 192, code: 'Backquote', key: '~', data: '~', shiftKey: true },
+  { keyCode: 219, code: 'BracketLeft', key: '[', data: '[' },
+  { keyCode: 219, code: 'BracketLeft', key: '{', data: '{', shiftKey: true },
+  { keyCode: 220, code: 'Backslash', key: '\\', data: '\\' },
+  { keyCode: 220, code: 'Backslash', key: '|', data: '|', shiftKey: true },
+  { keyCode: 221, code: 'BracketRight', key: ']', data: ']' },
+  { keyCode: 221, code: 'BracketRight', key: '}', data: '}', shiftKey: true },
+  { keyCode: 222, code: 'Quote', key: '\'', data: '\'' },
+  { keyCode: 222, code: 'Quote', key: '"', data: '"', shiftKey: true }
+];
+
+export const getKeyEventFromData = (view: typeof globalThis, event: 'keypress' | 'keydown' | 'keyup', data: string): Optional<KeyboardEvent> =>
+  Arr.find(keys, (key) => key.data === data).map((key) => {
+    if (event === 'keypress') {
+      const charCode = data.charCodeAt(0);
+
+      return new view.KeyboardEvent(event, {
+        key: key.key,
+        code: key.code,
+        keyCode: charCode,
+        which: charCode,
+        charCode,
+        shiftKey: key.shiftKey === true,
+        cancelable: true,
+        bubbles: true
+      });
+    } else {
+      return new view.KeyboardEvent(event, {
+        key: key.key,
+        code: key.code,
+        keyCode: key.keyCode,
+        which: key.keyCode,
+        shiftKey: key.shiftKey === true,
+        cancelable: true,
+        bubbles: true
+      });
+    }
+  });
diff --git a/modules/agar/src/main/ts/ephox/agar/keyboard/TypeInInput.ts b/modules/agar/src/main/ts/ephox/agar/keyboard/TypeInInput.ts
new file mode 100644
index 00000000000..fcf0a8bf9ea
--- /dev/null
+++ b/modules/agar/src/main/ts/ephox/agar/keyboard/TypeInInput.ts
@@ -0,0 +1,50 @@
+import { Type } from '@ephox/katamari';
+import { SugarElement, Value } from '@ephox/sugar';
+
+import * as Waiter from '../api/Waiter';
+import { getKeyEventFromData } from './Keycodes';
+
+const typeCharInInput = (input: SugarElement<HTMLInputElement | HTMLTextAreaElement>, chr: string) => {
+  const rawInput = input.dom;
+  const { selectionStart, selectionEnd } = rawInput;
+
+  if (Type.isNullable(selectionStart) || Type.isNullable(selectionEnd)) {
+    throw new Error('Element does not have text selection properties');
+  }
+
+  const value = Value.get(input);
+  const before = value.substring(0, selectionStart);
+  const after = value.substring(selectionEnd);
+  const view = rawInput.ownerDocument.defaultView;
+
+  const keydownEvent = getKeyEventFromData(view, 'keydown', chr).getOrDie(`Could not find keydown event for char: ${chr}`);
+  if (!rawInput.dispatchEvent(keydownEvent)) {
+    return;
+  }
+
+  const keypressEvent = getKeyEventFromData(view, 'keypress', chr).getOrDie(`Could not find keypress event for char: ${chr}`);
+  if (!rawInput.dispatchEvent(keypressEvent)) {
+    return;
+  }
+
+  if (!rawInput.dispatchEvent(new view.InputEvent('beforeinput', { inputType: 'insertText', data: chr, cancelable: true, bubbles: true }))) {
+    return;
+  }
+
+  Value.set(input, before + chr + after);
+  rawInput.selectionStart = selectionStart + 1;
+  rawInput.selectionEnd = selectionStart + 1;
+
+  rawInput.dispatchEvent(new view.InputEvent('input', { inputType: 'insertText', data: chr, cancelable: false, bubbles: true }));
+
+  const keyupEvent = getKeyEventFromData(view, 'keyup', chr).getOrDie(`Could not find keyup event for char: ${chr}`);
+  rawInput.dispatchEvent(keyupEvent);
+};
+
+export const pTypeTextInInput = async (input: SugarElement<HTMLInputElement | HTMLTextAreaElement>, text: string, speed: number = 0): Promise<void> => {
+  for (let i = 0; i < text.length; i++) {
+    typeCharInInput(input, text[i]);
+    await (speed === 0 ? Promise.resolve() : Waiter.pWait(speed));
+  }
+};
+
diff --git a/modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts b/modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts
new file mode 100644
index 00000000000..bf5ddd9b32d
--- /dev/null
+++ b/modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts
@@ -0,0 +1,140 @@
+import { Assert, describe, it } from '@ephox/bedrock-client';
+import { DomEvent, Insert, Remove, SugarBody, SugarElement } from '@ephox/sugar';
+
+import * as TestStore from 'ephox/agar/api/TestStore';
+import * as UiControls from 'ephox/agar/api/UiControls';
+
+interface InputStoreDataItem {
+  readonly type: 'beforeinput' | 'input';
+  readonly inputType: string;
+  readonly data: string;
+}
+
+interface KeyboardStoreDataItem {
+  readonly type: 'keydown' | 'keypress' | 'keyup';
+  readonly which: number;
+  readonly key: string;
+  readonly code: string;
+  readonly keyCode: number;
+  readonly charCode: number;
+  readonly shiftKey: boolean;
+}
+
+type StoreDataItem = InputStoreDataItem | KeyboardStoreDataItem;
+
+describe('browser.agar.keyboard.TypeInInputTest', () => {
+  const pTestOnTag = async (tagHtml: string) => {
+    const store = TestStore.TestStore<StoreDataItem>();
+
+    const input = SugarElement.fromHtml<HTMLInputElement | HTMLTextAreaElement>(tagHtml);
+    Insert.append(SugarBody.body(), input);
+
+    DomEvent.bind(input, 'keydown', ({ raw, kill }) => {
+      if (raw.code === 'KeyD') {
+        kill();
+      }
+
+      store.add({
+        type: 'keydown',
+        which: raw.which,
+        key: raw.key,
+        code: raw.code,
+        keyCode: raw.keyCode,
+        charCode: raw.charCode,
+        shiftKey: raw.shiftKey
+      });
+    });
+    DomEvent.bind(input, 'keypress', ({ raw, kill }) => {
+      if (raw.code === 'KeyE') {
+        kill();
+      }
+
+      store.add({
+        type: 'keypress',
+        which: raw.which,
+        key: raw.key,
+        code: raw.code,
+        keyCode: raw.keyCode,
+        charCode: raw.charCode,
+        shiftKey: raw.shiftKey
+      });
+    });
+    DomEvent.bind(input, 'beforeinput', ({ raw, kill }) => {
+      if (raw.data === 'f') {
+        kill();
+      }
+
+      store.add({ type: 'beforeinput', inputType: raw.inputType, data: raw.data });
+    });
+    DomEvent.bind<InputEvent>(input, 'input', ({ raw }) => store.add({ type: 'input', inputType: raw.inputType, data: raw.data }));
+    DomEvent.bind(input, 'keyup', ({ raw }) => {
+      store.add({
+        type: 'keyup',
+        which: raw.which,
+        key: raw.key,
+        code: raw.code,
+        keyCode: raw.keyCode,
+        charCode: raw.charCode,
+        shiftKey: raw.shiftKey
+      });
+    });
+
+    await UiControls.pType(input, 'aBcdef');
+    Assert.eq('Should be expected input value d, e, f is prevented', 'aBc', UiControls.getValue(input));
+
+    store.assertEq('Should have correct events', [
+      { type: 'keydown', which: 65, key: 'a', code: 'KeyA', keyCode: 65, charCode: 0, shiftKey: false },
+      { type: 'keypress', which: 97, key: 'a', code: 'KeyA', keyCode: 97, charCode: 97, shiftKey: false },
+      { type: 'beforeinput', inputType: 'insertText', data: 'a' },
+      { type: 'input', inputType: 'insertText', data: 'a' },
+      { type: 'keyup', which: 65, key: 'a', code: 'KeyA', keyCode: 65, charCode: 0, shiftKey: false },
+      { type: 'keydown', which: 66, key: 'B', code: 'KeyB', keyCode: 66, charCode: 0, shiftKey: true },
+      { type: 'keypress', which: 66, key: 'B', code: 'KeyB', keyCode: 66, charCode: 66, shiftKey: true },
+      { type: 'beforeinput', inputType: 'insertText', data: 'B' },
+      { type: 'input', inputType: 'insertText', data: 'B' },
+      { type: 'keyup', which: 66, key: 'B', code: 'KeyB', keyCode: 66, charCode: 0, shiftKey: true },
+      { type: 'keydown', which: 67, key: 'c', code: 'KeyC', keyCode: 67, charCode: 0, shiftKey: false },
+      { type: 'keypress', which: 99, key: 'c', code: 'KeyC', keyCode: 99, charCode: 99, shiftKey: false },
+      { type: 'beforeinput', inputType: 'insertText', data: 'c' },
+      { type: 'input', inputType: 'insertText', data: 'c' },
+      { type: 'keyup', which: 67, key: 'c', code: 'KeyC', keyCode: 67, charCode: 0, shiftKey: false },
+      { type: 'keydown', which: 68, key: 'd', code: 'KeyD', keyCode: 68, charCode: 0, shiftKey: false },
+      { type: 'keydown', which: 69, key: 'e', code: 'KeyE', keyCode: 69, charCode: 0, shiftKey: false },
+      { type: 'keypress', which: 101, key: 'e', code: 'KeyE', keyCode: 101, charCode: 101, shiftKey: false },
+      { type: 'keydown', which: 70, key: 'f', code: 'KeyF', keyCode: 70, charCode: 0, shiftKey: false },
+      { type: 'keypress', which: 102, key: 'f', code: 'KeyF', keyCode: 102, charCode: 102, shiftKey: false },
+      { type: 'beforeinput', inputType: 'insertText', data: 'f' }
+    ]);
+
+    Remove.remove(input);
+  };
+
+  const pTestOnTagFail = async (tagHtml: string) => {
+    const input = SugarElement.fromHtml<HTMLInputElement | HTMLTextAreaElement>(tagHtml);
+
+    try {
+      await UiControls.pType(input, 'abcde');
+      Assert.fail('Should fail on input');
+    } catch (e) {
+      Assert.eq('Should be expected error', 'Element does not have text selection properties', e.message);
+    }
+
+    Remove.remove(input);
+  };
+
+  it('TINY-11986: Typing in an input element', () => pTestOnTag('<input type="text">'));
+  it('TINY-11986: Typing in a textarea element', () => pTestOnTag('<textarea></textarea>'));
+  it('TINY-11986: Typing in a non text element should fail', () => pTestOnTagFail('<input type="button"></button>'));
+  it('TINY-11986: Typing on element using selector', async () => {
+    const input = SugarElement.fromHtml<HTMLInputElement>('<input type="text">');
+    const container = SugarElement.fromTag('div');
+    Insert.append(container, input);
+    Insert.append(SugarBody.body(), container);
+
+    await UiControls.pTypeOn(container, 'input', 'abc');
+    Assert.eq('Should be expected input value', 'abc', UiControls.getValue(input));
+
+    Remove.remove(container);
+  });
+});
+


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts (1)</summary><blockquote>

`74-74`: **Fix attribute in input element HTML**

There's an incorrect attribute in the input element HTML.


```diff
-it('TINY-11986: Typing in an input element', () => pTestOnTag('<input text="text">'));
+it('TINY-11986: Typing in an input element', () => pTestOnTag('<input type="text">'));
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 80a106d01a2c8f282574765a03ddf7be43059855 and 7252158e541807dca035381b18428c6c07660c8f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `modules/agar/src/main/ts/ephox/agar/api/UiControls.ts` (3 hunks)
* `modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>modules/agar/src/main/ts/ephox/agar/api/UiControls.ts (4)</summary>

`5-5`: **Good addition of Keyboard import**

This import nicely integrates the new keyboard simulation functionality with the existing UI controls.

---

`47-48`: **Well-organized API design**

Exposing `pTypeTextInInput` through a well-named constant `pType` provides a clean and intuitive API for test authors.

---

`49-52`: **Good implementation of pTypeOn function**

The function combines element selection with typing simulation, which provides a convenient API for common testing scenarios. The default speed parameter of 0 is appropriate for most testing contexts where immediate input is desired.

---

`65-66`: **Proper exports of new functionality**

The new functions are correctly exported, making them available to consumers of this module.

</details>
<details>
<summary>modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts (4)</summary>

`1-12`: **Good test structure and type definitions**

The imports and type definition for `StoreDataItem` are well-organized. The interface provides a clear structure for event data captured during tests.

---

`14-59`: **Well-implemented test function with thorough event validation**

The `pTestOnTag` function is thorough in its testing approach:
1. Creates appropriate test elements
2. Sets up event listeners to capture and validate the event flow
3. Tests event prevention logic for both keydown and beforeinput events
4. Validates the final input value and exact event sequence

This ensures that the typing simulation correctly mimics user interaction with proper event ordering.

---

`61-72`: **Good error case testing**

The `pTestOnTagFail` function properly tests the error handling when typing is attempted on unsupported elements. The error message assertion ensures that appropriate feedback is provided to developers.

---

`77-87`: **Good selector-based typing test**

This test case effectively validates the `pTypeOn` function, demonstrating how to use the selector-based API to simulate typing. The test properly sets up, executes, and cleans up the test environment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T14:37:32Z
user: login: TheSpyder
body: I'm surprised we have features that depend on real simulated typing, I would've thought just setting the value and dispatching a `change` or `input` event meets most needs 🤷‍♂️ 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T22:04:10Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>modules/agar/src/main/ts/ephox/agar/keyboard/Keycodes.ts (1)</summary><blockquote>

`152-152`: **Consider adding JSDoc for better API documentation.**

Adding JSDoc comments for the exported function would improve developer experience when using this API, especially explaining the purpose of the `view` parameter and the return type being Optional.


```diff
+/**
+ * Creates a KeyboardEvent from the given data string.
+ * @param view The window/global object to use for creating the event
+ * @param event The type of keyboard event to create
+ * @param data The character data to simulate
+ * @returns An Optional containing the KeyboardEvent if the data is supported, None otherwise
+ */
export const getKeyEventFromData = (view: typeof globalThis, event: 'keypress' | 'keydown' | 'keyup', data: string): Optional<KeyboardEvent> =>
```

</blockquote></details>
<details>
<summary>modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts (2)</summary><blockquote>

`26-110`: **Comprehensive test function with room for refactoring.**

The `pTestOnTag` function thoroughly tests typing simulation, including event prevention and validation. The test is well-structured with proper setup, execution, and cleanup phases.

Consider refactoring the duplicate event handler code for keydown, keypress, and keyup events to reduce repetition and improve maintainability.



```diff
-    DomEvent.bind(input, 'keydown', ({ raw, kill }) => {
-      if (raw.keyCode === 68) {
-        kill();
-      }
-
-      store.add({
-        type: 'keydown',
-        which: raw.which,
-        key: raw.key,
-        code: raw.code,
-        keyCode: raw.keyCode,
-        charCode: raw.charCode,
-        shiftKey: raw.shiftKey
-      });
-    });
-    DomEvent.bind(input, 'keypress', ({ raw, kill }) => {
-      if (raw.charCode === 'e'.charCodeAt(0)) {
-        kill();
-      }
-
-      store.add({
-        type: 'keypress',
-        which: raw.which,
-        key: raw.key,
-        code: raw.code,
-        keyCode: raw.keyCode,
-        charCode: raw.charCode,
-        shiftKey: raw.shiftKey
-      });
-    });
+    const bindKeyboardEvent = (eventType: 'keydown' | 'keypress' | 'keyup', killCondition?: (raw: KeyboardEvent) => boolean) => {
+      DomEvent.bind(input, eventType, ({ raw, kill }) => {
+        if (killCondition && killCondition(raw)) {
+          kill();
+        }
+
+        store.add({
+          type: eventType,
+          which: raw.which,
+          key: raw.key,
+          code: raw.code,
+          keyCode: raw.keyCode,
+          charCode: raw.charCode,
+          shiftKey: raw.shiftKey
+        });
+      });
+    };
+
+    bindKeyboardEvent('keydown', (raw) => raw.keyCode === 68);
+    bindKeyboardEvent('keypress', (raw) => raw.charCode === 'e'.charCodeAt(0));
+    // ... other keyboard events
```

---

`112-123`: **Good error case testing.**

The `pTestOnTagFail` function correctly tests that typing simulation fails for non-text input elements with proper error handling and assertion.

Consider making the type cast more specific to match what's being tested:



```diff
-    const input = SugarElement.fromHtml<HTMLInputElement | HTMLTextAreaElement>(tagHtml);
+    const input = SugarElement.fromHtml<HTMLElement>(tagHtml);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4e77b6e15b7c4221e6b71d57a746f9130dc21b77 and 88b52eaed7ade311d58652dc4136bad63f02358e.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `modules/agar/src/main/ts/ephox/agar/keyboard/Keycodes.ts` (1 hunks)
* `modules/agar/src/main/ts/ephox/agar/keyboard/TypeInInput.ts` (1 hunks)
* `modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* modules/agar/src/main/ts/ephox/agar/keyboard/TypeInInput.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>modules/agar/src/main/ts/ephox/agar/keyboard/Keycodes.ts (2)</summary>

`3-150`: **Comprehensive key mapping implementation looks good.**

The `keys` array provides a thorough mapping of keyboard keys with their respective keyCode, code, key, and data properties, which will enable accurate keyboard event simulation for the typing feature.

---

`152-176`: **Consider handling empty string input and potential character mismatches.**

The function implementation is solid but has a potential issue when handling empty strings. If an empty string is passed as the `data` parameter, `data.charCodeAt(0)` at line 155 would throw an error.

Also be aware that characters not defined in the `keys` array will return None from Optional.


Consider adding a check for empty strings and documenting the behavior for unsupported characters:

```diff
export const getKeyEventFromData = (view: typeof globalThis, event: 'keypress' | 'keydown' | 'keyup', data: string): Optional<KeyboardEvent> =>
+  // Return None for empty strings
+  data.length === 0 ? Optional.none() :
  Arr.find(keys, (key) => key.data === data).map((key) => {
    if (event === 'keypress') {
      const charCode = data.charCodeAt(0);
```

</details>
<details>
<summary>modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts (5)</summary>

`1-23`: **Well-structured type definitions.**

The interfaces for keyboard and input events are clearly defined, making the test code more maintainable and type-safe. The union type `StoreDataItem` is a good approach for handling different event types in the test store.

---

`125-139`: **Comprehensive test coverage with good organization.**

These test cases cover various scenarios effectively:
1. Standard input elements
2. Textarea elements
3. Error handling for non-text elements
4. Selector-based typing with nested DOM structures

The consistent use of the ticket number (TINY-11986) in each test description is a good practice for traceability.

---

`33-65`: **Well-implemented event prevention test strategy.**

The code systematically tests keyboard event prevention at different stages:
- Line 33: Preventing a keydown event for the 'd' key
- Line 48: Preventing a keypress event for the 'e' key
- Line 63: Preventing a beforeinput event for the 'f' character

This provides thorough testing of the event handling chain and ensures that character prevention works as expected at each stage of input processing.

---

`82-107`: **Thorough event sequence validation.**

The test effectively validates the complete chain of events for each character typed, including verification that:
1. Prevented characters ('d', 'e', 'f') don't appear in the final input value
2. Events for prevented characters are still fired correctly up to the point of prevention
3. Case sensitivity is properly handled (note the uppercase 'B' vs lowercase characters)

This attention to detail ensures the typing simulation behaves exactly as expected under various conditions.

---

`128-138`: **Good testing of selector-based typing.**

This test case provides valuable validation of the `pTypeOn` function, which allows typing on elements found via selectors. This is an important use case for automated testing scenarios where elements need to be located within a DOM structure.

The test properly constructs a nested DOM, applies the typing operation, and cleans up afterward, demonstrating good test hygiene.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T08:56:59Z
user: login: hamza0867
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T10:08:15Z
user: login: spocke
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T11:02:10Z

Review comments:
diff_hunk: @@ -0,0 +1,140 @@
+import { Assert, describe, it } from '@ephox/bedrock-client';
+import { DomEvent, Insert, Remove, SugarBody, SugarElement } from '@ephox/sugar';
+
+import * as TestStore from 'ephox/agar/api/TestStore';
+import * as UiControls from 'ephox/agar/api/UiControls';
+
+interface InputStoreDataItem {
+  readonly type: 'beforeinput' | 'input';
+  readonly inputType: string;
+  readonly data: string;
+}
+
+interface KeyboardStoreDataItem {
+  readonly type: 'keydown' | 'keypress' | 'keyup';
+  readonly which: number;
+  readonly key: string;
+  readonly code: string;
+  readonly keyCode: number;
+  readonly charCode: number;
+  readonly shiftKey: boolean;
+}
+
+type StoreDataItem = InputStoreDataItem | KeyboardStoreDataItem;
+
+describe('browser.agar.keyboard.TypeInInputTest', () => {
+  const pTestOnTag = async (tagHtml: string) => {
+    const store = TestStore.TestStore<StoreDataItem>();
+
+    const input = SugarElement.fromHtml<HTMLInputElement | HTMLTextAreaElement>(tagHtml);
+    Insert.append(SugarBody.body(), input);
+
+    DomEvent.bind(input, 'keydown', ({ raw, kill }) => {
+      if (raw.keyCode === 68) {
path: modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts
user: login: hamza0867
body: I don't think we should use `keyCode` in new code since it's deprecated and less readable. I think it's better that we use the `code` property. What do you think ?
created_at: 2025-03-28T10:05:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import { Assert, describe, it } from '@ephox/bedrock-client';
+import { DomEvent, Insert, Remove, SugarBody, SugarElement } from '@ephox/sugar';
+
+import * as TestStore from 'ephox/agar/api/TestStore';
+import * as UiControls from 'ephox/agar/api/UiControls';
+
+interface InputStoreDataItem {
+  readonly type: 'beforeinput' | 'input';
+  readonly inputType: string;
+  readonly data: string;
+}
+
+interface KeyboardStoreDataItem {
+  readonly type: 'keydown' | 'keypress' | 'keyup';
+  readonly which: number;
+  readonly key: string;
+  readonly code: string;
+  readonly keyCode: number;
+  readonly charCode: number;
+  readonly shiftKey: boolean;
+}
+
+type StoreDataItem = InputStoreDataItem | KeyboardStoreDataItem;
+
+describe('browser.agar.keyboard.TypeInInputTest', () => {
+  const pTestOnTag = async (tagHtml: string) => {
+    const store = TestStore.TestStore<StoreDataItem>();
+
+    const input = SugarElement.fromHtml<HTMLInputElement | HTMLTextAreaElement>(tagHtml);
+    Insert.append(SugarBody.body(), input);
+
+    DomEvent.bind(input, 'keydown', ({ raw, kill }) => {
+      if (raw.keyCode === 68) {
+        kill();
+      }
+
+      store.add({
+        type: 'keydown',
+        which: raw.which,
+        key: raw.key,
+        code: raw.code,
+        keyCode: raw.keyCode,
+        charCode: raw.charCode,
+        shiftKey: raw.shiftKey
+      });
+    });
+    DomEvent.bind(input, 'keypress', ({ raw, kill }) => {
+      if (raw.charCode === 'e'.charCodeAt(0)) {
path: modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts
user: login: hamza0867
body: Same here.
created_at: 2025-03-28T10:06:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import { Assert, describe, it } from '@ephox/bedrock-client';
+import { DomEvent, Insert, Remove, SugarBody, SugarElement } from '@ephox/sugar';
+
+import * as TestStore from 'ephox/agar/api/TestStore';
+import * as UiControls from 'ephox/agar/api/UiControls';
+
+interface InputStoreDataItem {
+  readonly type: 'beforeinput' | 'input';
+  readonly inputType: string;
+  readonly data: string;
+}
+
+interface KeyboardStoreDataItem {
+  readonly type: 'keydown' | 'keypress' | 'keyup';
+  readonly which: number;
+  readonly key: string;
+  readonly code: string;
+  readonly keyCode: number;
+  readonly charCode: number;
+  readonly shiftKey: boolean;
+}
+
+type StoreDataItem = InputStoreDataItem | KeyboardStoreDataItem;
+
+describe('browser.agar.keyboard.TypeInInputTest', () => {
+  const pTestOnTag = async (tagHtml: string) => {
+    const store = TestStore.TestStore<StoreDataItem>();
+
+    const input = SugarElement.fromHtml<HTMLInputElement | HTMLTextAreaElement>(tagHtml);
+    Insert.append(SugarBody.body(), input);
+
+    DomEvent.bind(input, 'keydown', ({ raw, kill }) => {
+      if (raw.keyCode === 68) {
path: modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts
user: login: spocke
body: Yes, that makes sense since at some point we might want to add some lint rule to get rid of these and keeping it around would just add work to that. I will see if I can stick in a follow up ticket to also use this more modern key handling for the `Keyboard.keydown` etc should be pretty simple to introduce it there then we can simulate modern keyboard events with agar that would be nice.
created_at: 2025-03-28T11:02:10Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-26T14:13:42Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces simulated typing functionality within agar. A new file is added to handle text input simulation in HTML elements, including error handling and event dispatching. The API files have been updated to expose the new functionality via additional export constants, and a corresponding test suite validates both successful inputs and error scenarios.

## Changes

| File(s)                                                       | Change Summary                                                                                                                                                                                   |
|---------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `.../api/Keyboard.ts`<br>`.../api/UiControls.ts`              | Added new export constants (`pTypeTextInInput` and `pType`) to expose simulated typing functionality; minor syntax adjustment via a trailing comma in exports.                           |
| `.../keyboard/TypeInInput.ts`                                 | New file implementing functions to simulate text input in HTML input and textarea elements, including character-by-character insertion and error handling.                                  |
| `.../test/ts/browser/keyboard/TypeInInputTest.ts`             | New test suite validating the simulated keyboard input functionality, covering both proper event dispatch and error conditions for non-text elements.                                            |

## Assessment against linked issues

| Objective                                     | Addressed | Explanation |
|-----------------------------------------------|:---------:|-------------|
| Simulate typing in textarea (TINY-11986)       | ✅        |             |


## Poem
> I'm a tiny rabbit coder, hopping through the keys,  
> Injecting new functions with effortless expertise.  
> Each character typed, a little dance of code,  
> In inputs and textareas, on a well-worn road.  
> Hooray for our changes—may our code forever explode!  
> 🐇🎉

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c9e98b9182713ea7414fa5b76f823d994841728d and dd6d4577a8a291fa764ddd41997782b03296384d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* modules/agar/src/test/ts/browser/keyboard/TypeInInputTest.ts

</details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARNAAkgByAJpgAIxpAJwAHABsXACCtLTwGESQiPDMPtSl5biy3HUoGAQt3HiQJFFsrchtaEQuMZAA7mjIiNgCzOo09HLhsJ7YiJQV3PgMANaemPSo8Eqt8ABm8NGLuMuQGE4C62kADABMAKwAzBowN7xVLvJTltVvxTktULYUP18C1cBRFNgGHtbiRRpBTiRqNgAgAaSAAAwAqvAAML4Vrw7yIDTcaCNEj4vGjBAMWBdDAqKL9G6VareWrk0HhRrNUptGgAD1wLkx6AwBwwHTC526tG51Aq2G4mwoYWuqFO2AwYng5LQ3nUsg07hurMwpGQzDQSha4OQkJl/m61Gi4pu4h2JDCCRS6SyeW+Hhw13wfFgE3ZUwC9GuGrWUiofl4JDArNE22Q8akkAeZAUzG4UXmeNKDG82BKZSWe2K6lNWHwYLQjHjZW6+HK7FccvoASiEkwetC1MgADFsddKMxYyQ8YuS1RjWyJogtvAffRRuo2fr3XZUGhtfC/j7vPJs+cJXQa/L4Axak31MgOhRNmt0Mg3YYliAToK24jktaBjxFgNAUMwyCdl6EgXGiATOqU0iIGuNyzFyBDkOisbNtYdjxjIJBlmsuB4vs6CQLQwK9NKEGwW+uxhORJaUVgDDofMKBdtqFrvgIUSRjckKMdItz4GEzHtnK8g7nuB6QAA4uoAAS0xQlM0hQbOxHLqBSjSvAVI4Z4kLvlgDzoAwSKIGs9AauuQK+PgR5NoSNgADJcAA2rAuC4NwiAcAA9JFRDHtMGhMMwkXiBgsjMEiyWlGlGUdL4kXPO8HwALoABQhWFEXRbF1zxYlmWpelJD1dlTW5d4+WvJ8ACUUEWJAZKsOwyCOM4rhGDBJGJcxkyiKxQrrrYVmQKs6yIJsgZdBK2bOdESZ/P+qZ6ssoGesBuDYjJY6yM0groX4vI1AJDRNGUeJTEQDopfU8ZhLylbyNRX0kRO9Z7PKDFINw1Css03b4nafb4vwfD4qUSpIyQUitGM+A+PQUynOcSJEXwy6IL9SIchQpozlpXmY5QtEYHJx3LWsfCbGK0S40dbnHXsoFraIZxvowEzSG9iLboBeCEIllZBpQyBAnwAACCnksgpTltNTL8xUVSPc0z2isg5BOYgAISYcFHvitzb/mdF1m5RKYwvg3BlnRASnAEiBske1xdGgrKQLssjk/Cux62+bKoMzYQhwgmOcp41zwtgRBskIqy/UGQPriDIQjhDa3QwgTYM30UFRiUiAMKslSCggWcWlnuDcis4gWg0QoPfyBcik2KvhCQUoygmdFo3g2E8C4AY1BQd6uvXZAuDTYz63C8AfZQzRVx3/BYJiofh5H+C7FCXQ7Sc5rfEZfClOTmBIi+k3i0KACy1CwJ/flrkPco3ZKhlCiJAKscExawWprvPgQsGAiwYNxIspoKDqkTqBEyngpqCm1itR+rRlAmk1hJbBLBppelOFEMQ9EmAYDWPQkEgp1xVB1NKY0ngkKnnRJic6oER5kF7Agps+DNqe2pmQYm2skS6m0LxckkoO54hIBWci8AABexsfqbwsp4EoAQaFF08IDZoTp5CjFjNsV0TpUpizWLPRcJ1BaewQUTdERpiEch7hcdBXpgjwBOpALBtxsAv2iJeXg+Ak7hBhNPX6hsB7tigvoYw4AoBkHoEhNAstiBkGUAJKa7AuC8H4MIWakgZKLCYEoKgqh1BaB0CkkwUA4A2zlNGQgpByBUAKeQop/g0BoimKNeQVTFDKDqZobQugwCGFSaYAwy5aA+GkJFQYLhIqIAoAwSKTpSjJUQJFEg3BYD4AlGsoYFA1lNEigAaRILIAQUSKC0A0B3DgBgYhfIMH1Ao8RcndLUsMp0w4kIIwdDabBvYHSwnhMs4m3ZyBolYbGX6LEVHsBJgSOknsYIwXRuieEzASL4g0BoSK4cnkuFoJFHFJA8WKjwEjJZKzvgFBRGiOhz9sb4lpPSaAY9cAMoJagJQ5xyC0BjqHH2lBJEyXxHS4VeAaR0oFVKJVuBSU/FQFy9hYRUCLiwM6JQbsSJjzYRUdFzFaKeW8uUdQMTuIOQtvAMSnCZ5HDTrhBE4kfieDHkgIG5rUWWp9MxSAXFSxYCWSLX0sTjT1hdLy1VgqNWMh0WA9c2YUK40QMvYNupoj4kQPciOcIL4Mi9Hs+hy0tz2miNrdcBawgWnJjXG4+AMzmnunCREfDOFgnXCysBARq1a1aCHGiYxjwkT+CC+QMaiYCjsiQLRLYShzS4TcJFCh6F6pLl+FodZViKUbTcZt1IjDmEsAUbwcFl3QhIkoOsLgH1CmbdzDm0wRLsgiD4owUBEiol3dyxOxQ6BcHhprMIfLPZqqFRgfFTLXT4mHas9ZVytk7OrQco5JyzkXI2ZeeAdyHlUpeW8xA+IDBQAAKJbVRdEXVk5lrcFoAeSDsGSDwbTWBE1jqm0Md1OAwNKG0OHIw5s7Zuy5G4eOac85kniOkcec815HdqNfJiABhZ4nCOYekzhjueGFP6euSR4kZIKT4CpJRj5Wmfk3v+V0/Ju0nDzqFOC6QkKIaE1hQiJy9Ed0ouE8/Gg4aR7rnxKW8jtBmU+tXCgCsqLYa+BQIfC9bKOUgf3cm+kSNRUkHFc+HRocLzOR3hK4G5pi5IWi2RtTKr+WpsQ4yzVbLwLtm7bIWi2WJiyC3PCZmIJDTGjmnlz2AB5DABXALgclaVk8aBdiAV3eZbpvW1jUIIBQXrijLXU1eiXTA/BuCsXNBsV2c8qBsDgtbZWHi5oBG3gzQCWA4ldB6Jik9TYiTwFnKUGp6bDVi18MgerqnqVNbgy1pDmrHX9x9MKF631PCRJQvx/bYoYQsLa59jF2M+bGJcbG/Gns6CkLESGtYnir5anY09eNx6XRPKDhNytdF2fTfTcyXRZqhNhDqwwdSQYABqNXK2oEXRcU1tZE3IjhNoC0TYppoDxAITozN3Fja634LzgFMHOjTrEw+3gBxvm+JNrtvglr6/ZEIr1ng0MlnkFzIgPTmijc8eaS0hKWAkpi2p+LyywF0WNc0Hd74oZqG8TJQOpzOjmjgvvCUgbTFBlOWqZGzYAlJYVsxB9vUb13vye2R965n38g95rd9AvP1eDEqLdgbZvM0cgEBzl0GWPGogwSLjBWsCoYSxJy5UnsOyeM/JgjSmbmWYUZSakGm28d6CRnxQfHe9c4wCVOh63KBcEcJc2jX3WjAESOMvQb1uizVjFwSOdQ1yCrv9vI7Qte93GYA8PgABeSATwupcBWBEpIAkDAASD4BHB6AD4Eh6aSZYYyb7KT74aKaj7KZz7Wa2ZL5QDi7UypxegchsC0CQbC5i4S7QFD4h7oaj7wFGaHJT4oFEaz6kjz42aL5UaQAlQ94phUAWTNCq5dSfLfI0a6bD5mY0ET50HIFmaUpqa0r0gap2aCHaa/LOZ5I9JuYjKebQqt7so7rnBgIKryGtZKiUZIyRo8T+bwola86hy75yIPY65mg9yAjESI4FyCrtCdDaxaTQB/yeGJzgyKITz47TSU5MD8KPbth4j4jPQkAkjxgUBpprjLYyRwyH4uDH4E7w4BDbTN5NgnYfYqjeCuTgzdh2hUBiCMwRr7DK6o4tDsybqDq2gJGTrrBE6MDYgBDYxbblKCicytDfDxBhB5g7DKzETrhoATgWQqC8G9xIQ9G06RLiLiCpGBGwDwijBvZdAUDwiPxNEPLoCgQJw8B+zsCU5e5PZBgSJSCdwdE7GYpGLzQ3BxJvSViHoOrY7XaHwPAqxgyuSnCQLrgLGsRMz0B1xQy4B5jIAyHUoHp44HyPpuGO7I6wyeJsoAnrBPyUAFzNGvqVG7bpasYM4yRAnX607PzCZ/htiCghzhGNhEB3hQRFAbq653hLTs48bGHIYXGnrIAfoLbdpeSe7jGALtLN6Cwv72qtA47PHwkn6aDaoOG07qCuaIQZjBy2EtH4muhZrwgY67SSl4jvi+DNAxH0jxEuC8YCLRLlGtF8B0Qezna+DmLaBAwjzAIk4qhk5XYPC4CjAWFxIzhRhv70BQw3aKx8BirZJ3qPrqKUD4A2pm52pYpVCEH7g0D+GCSyS/Seky7tqeCsLynLpHry58k7HES9i0C1FYrAmKRLHYk+IlxkBJgknaITFXgey4EZkImHF6KQzlxxpBJVCiyiIfa0nYi3hWjXqQC3r3qsTl43CV6vrzm15sL14dCN5ILN4rGIAAaQCfxr6uTzaQbNo5Y8AprqpclhC/79bGgcFxIH6ZwZHynAA+F/xw6ZHhoAA+kAb5fk8GBQ6En57Al+o8Uoz+h2RArxrsXAH+X+kAv+/+gBwBawYBEBtAUBro+hngFBKyI+Gy4hiBkhpmkmMJLychuKV5phShOmYARgsB1B0mNA5MuGAgGx7MFKDW1KFF9KV5Aqba7yNFKhAKrm+M7mAIWh9au5BguhwGzFyofOhhlFcO/FmgGm1RFEZYYocKiIg5IMRwSOZFCo96niyA8e2sH21+FC2s3YbFXk7Ma2gq5xfOeAvBcZtx+Iqs9BkUDwtA8IOwuYFo7ASMnOXlUh70LgSMFobF68MkkWoQsM4MAAIpNp/EEpgPALlMuoMWEGKphP0BYrCJQKcCHNIJBnDvYDtiQEldQGgEMSokjCPFHnwqKG1mAGOGpAfAxLVSXBDrFpVSuDVdKPVcwI1cREZV0FjLlbVVKtuKBIlGoFVp8d2EaIpLEQSANQEENXVeFqSj5tWtrt7s4VfJIpeFMAPA2rBIVbeayENrmodfOZxqpdNtAIMCFeDMmqEC9YMLOErlqlGJ9eTN9UQI1ZEYKNRMgFqO0pZfKX7sSn+eroDsgBONTPdRNV1a2jQN0sgCVPiOHIxKMDNtEeHNtFRtET8SuHEumqjG1umpzuHFqPiF1AJuevKaCV6HSbcT2TZaPOTJaiuN8PYLmUguHJNUNL2ScQfNEL7P7g8M0JEhbNECVCQBoEQBoLblqXBMgAAOS0Da14ja0kD60lza2nDa3M1VLdproEjbAWTeAlRdTMqHlHxmpTWlIiBiDomQJuHGyilAmSmQDa1oAABCDAYqxtDqO42JviuOSokAjx5WYis04S4MGYsaMdtosYBiecwQcq76U1ZlCYBisYJq+Zfez1GAr1RAv1FkoNjhsE04JEDwKCxEzIXsskGAYAWOcpWRdOLkOV6AoUKiZ2iJCSSOJs+R4M5qs0WxlAuxjq9k6cXkGAs1V8IOUdharkQwT8icl2riTe5ZpMWEgwKtPm2Fu6zOceM6QI2IvNwx4sviIxViE9Up9E3K7GLy/he2gqE8vWGuoUgoVl7A7NBRPd4aP2QCFQZJO2UE05s5peNefoeiogVeb6SE/J2eG5P625/6y+cloQmoKpG+RBBISg9c1MDwJU2tdlmxlAWglyGgRlGgiqfFoQxtZKGgjtYmohcBTFoQrF7FlAnFkO5FLDKloQ1FgGwGPJNJx5BIN155X1ldgwJU0oRAWkuAzA3gEFdQXD2suFXIYhfDLFxmNDHFRlPFGqqlkj7e0jYNRqcj+ICjtISjVdNd9tajGjWjOjZQejg+DFBFxjuAAj9lQjFjYjbW1jWBtjyKhCFAJVCKjjXM8TpVkAFVVV21I15BATBmOy8lITtDVy4TRh4jAlVGeDsTcECT66m+yT1TkAge1Km11VtVWT3DlB+FuTyU/DpjgjRTXFojJTkTEj0TdK6AFok8jj61zTmT4WCFaTbWMzrTczP5jTLySzw1u17TeFRjeTPThyZjYTAzNKETSoUT5TDmwhdFiyPDjF4+RFJm0+o+Fjpa1SBkglDmwlLm6hYlmhYK2h0lslaIdK9gfEmVClUQ0Rrz4ybB6a5hWl1mVhC2NhbIeV5Aq28sAQyw9CFS6V2oN0YIE1Z8LNq6fAfsLi4gxYotbzs8otBBEsx2zkqkAkDOaAlOeNDy7BLgVA8gdhT87tM9XouRJwTYKNpoIIZ8L4zOfBLBfgxLytqt6tkAwdIcBYUMr8kAtGcTXUeI0u6w8rKtateI9gCAAJeIVm5a3gOr4zJymATge8ItnLHBhrSrBQYAAAWniE8GAJkNa3RDI1gPK7OGkGAMGy8D1Jq9EoIB7fqgmg2DJPWbqI2VMKHAmBy7IGSEoNTW89TeHHTeDI6ayfIPiKy+9fQMWqa7gKWumoDm+B+HUeHNrRRC3XwG3bBDcCa2cGEKLagKTRTkYMydSV4mySROfRevRAGwSKQFWw8rRlNbOESttTziyEtitksAEGjq+rdkrJBihKiNTQfPm+W6W/dg9YpK5RaO5SSoBRQBoOKnFqvjGKambu+Bmd2KLdG7NDqTcOm1yzsWgPIIdOlZCcsLcejp6se7VVqklcchkjdO22nPSPnZirjSTX7GTTbQ8gTUTdnum4zdaw6rqhQH2m9qOEGNiDWoisBn1WpnO8FWzNbREteF2WjteA2XFUSqO7jODB+2Up7X6me4A4wn7EsO0V1TqmEvyG6iXBrqoFyHiOB/SW/ewh/SUHGYeAB46nxLwp4OjQXSWOLJklgPfk2Ky2XYWVkcWXLvG2WfPZWdWUvZnCeDcPbHVpNmdrrnTbasKXAmgBiM6lhCS2HAcayyXNmnkeUMRycGwNsbsVrGCFrk6CB80D22MUaK8nAyXtXjWkgwxCg8uWXquYxpkl+pub+i3tJVAPOPXcQyeQLmedO6WnR60AuywNtfMyVHu6MFwLEUhAyYIOaK0rPAfFwNrWh1hNrZAD+WN1h8vZN9NwzdwMbayz40QAAZAO506cAGs7QM17gJhdrOfQY1QYE/cxgHJlIaRccypjS9RZcykgYM0uyJkl2Dkt82pIUq0FwFQEMuJcOGMjUioGoFMo0rMo92kjrOoAAPpHCIBQ8BBdd0BQ8UlhAPdPfZBPBoDPC5C0BPBpBoAvAMDZCnAvDZDvAADsAALOT7kG8GgE8B8MUKcOTw8JTx8E8G8DkG8G8DMoYE9+T+8C8GkG8NkCQG8JT2kBj+T7QO+Az58NkGkAIJL5T6TwwLkAwE8DT7kE8ET2CE0hDx8Pj2HdkAIE8KcLQJkDTwIB8C8KcKz28LkGkMBHj8L+T6cO+CHBz68Lz+DxAJAJTyQOTyz7kCQMLwIOTwwCr0LyQLkOH2kLQG8OT2gFT7kKcJkGkOzzL0L+H+Tz7+jyb28C8JiHQEn0oIb/HyL7Ty8DLxLx8LH86LkB8KcK8B8CL54Pr37wwJkCQDkAIOn2T+Txn5iFT2kJTyVW8OH6n2T4z5kJkJT9kBLwL9kPQB31AMUDj5T4n0n9kAT+nyVTT5T8ULQBL7P0H2T6bzb5kI3wvyv2D771AFNND7D/D5jKhEjxkrz0AA== -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=tinymce/tinymce&utm_content=10253):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: spocke
created_at: 2025-03-27T05:01:47Z
body: > I'm surprised we have features that depend on real simulated typing, I would've thought just setting the value and dispatching a `change` or `input` event meets most needs 🤷‍♂️

For the most part yes but we do have auto completers for @mentions in comments for a example that open and refresh on each keystroke.
We have a similar typing key by key inside the editor content so I think it would be useful to have one for textareas/inputs that trigger all the events in the same way.
For the mathml case for example we just type one letter and that is likely the more common scenario and this can be used for that.
author_association: MEMBER