From 480fc0356971deeda93cbbfe9efdae6be179225f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Tue, 6 Oct 2026 12:53:28 -0300 Subject: [PATCH] Send macOS editing commands for Chromium keyboard shortcuts Chromium does not run native key bindings for synthetic key events on macOS, so Meta+A, copy/paste, undo/redo and similar shortcuts did nothing. Upstream crInput sends the matching macEditingCommands in Input.dispatchKeyEvent when the browser runs on a Mac. Do the same, and share the existing WebKit command table and shortcut builder. Co-Authored-By: Claude Opus 5.5 --- src/PlaywrightNative/Chromium/CRPage.cs | 2 +- .../Chromium/CRRawKeyboard.cs | 29 ++++++++++- .../{WebKit => Input}/MacEditingCommands.cs | 48 +++++++++++++++++-- src/PlaywrightNative/WebKit/WKRawKeyboard.cs | 40 +--------------- 4 files changed, 74 insertions(+), 45 deletions(-) rename src/PlaywrightNative/{WebKit => Input}/MacEditingCommands.cs (81%) diff --git a/src/PlaywrightNative/Chromium/CRPage.cs b/src/PlaywrightNative/Chromium/CRPage.cs index a3064605..b9f3eccf 100644 --- a/src/PlaywrightNative/Chromium/CRPage.cs +++ b/src/PlaywrightNative/Chromium/CRPage.cs @@ -137,7 +137,7 @@ public CRPage(CRSession client, string targetId, CRBrowser browser, ILoggerFacto // DragManager mirrors upstream crDragDrop: intercept HTML5 drag so custom // DataTransfer types are preserved without chromium/x-drag-id. CRDragManager dragManager = new CRDragManager(this); - _keyboard = new Input.Keyboard(new CRRawKeyboard(_client, dragManager)); + _keyboard = new Input.Keyboard(new CRRawKeyboard(_client, dragManager, browser.UserAgent.Contains("Macintosh", StringComparison.Ordinal))); _mouse = new Input.Mouse(new CRRawMouse(_client, dragManager), _keyboard); _touchscreen = new Input.Touchscreen(new CRRawTouchscreen(_client), _keyboard); diff --git a/src/PlaywrightNative/Chromium/CRRawKeyboard.cs b/src/PlaywrightNative/Chromium/CRRawKeyboard.cs index e622d84a..7a343b64 100644 --- a/src/PlaywrightNative/Chromium/CRRawKeyboard.cs +++ b/src/PlaywrightNative/Chromium/CRRawKeyboard.cs @@ -14,7 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using PlaywrightNative.Input; @@ -28,16 +30,19 @@ internal class CRRawKeyboard : IRawKeyboard { private readonly CRSession _session; private readonly CRDragManager _dragManager; + private readonly bool _isMac; /// /// Initializes a new instance of the class. /// /// The CDP session to send commands on. /// Chromium drag interceptor. - public CRRawKeyboard(CRSession session, CRDragManager dragManager) + /// Whether the browser runs on macOS and needs editing commands. + public CRRawKeyboard(CRSession session, CRDragManager dragManager, bool isMac) { _session = session; _dragManager = dragManager; + _isMac = isMac; } /// @@ -52,6 +57,7 @@ public async Task KeyDownAsync(IReadOnlyCollection modif } string type = string.IsNullOrEmpty(key.Text) ? "rawKeyDown" : "keyDown"; + string[] commands = CommandsForCode(key.Code, modifiers); await _session.SendAsync("Input.dispatchKeyEvent", new { @@ -59,6 +65,7 @@ public async Task KeyDownAsync(IReadOnlyCollection modif modifiers = modifiers.ToCdpMask(), windowsVirtualKeyCode = key.KeyCodeWithoutLocation == 0 ? key.KeyCode : key.KeyCodeWithoutLocation, code = key.Code, + commands, key = key.Key, text = key.Text, unmodifiedText = key.Text, @@ -92,5 +99,25 @@ public Task InsertTextAsync(string text) { return _session.SendAsync("Input.insertText", new { text }); } + + /// + /// Returns the macOS editing commands Chromium should run for a key press. Chromium + /// skips native key bindings for synthetic events, so shortcuts like Meta+A need them. + /// Mirrors upstream crInput.ts _commandsForCode. + /// + private string[] CommandsForCode(string code, IReadOnlyCollection modifiers) + { + if (!_isMac) + { + return Array.Empty(); + } + + // Commands that insert text are not supported. Drop the trailing ':' to match the + // Chromium command names. + return MacEditingCommands.Resolve(MacEditingCommands.BuildShortcut(modifiers, code)) + .Where(command => !command.StartsWith("insert", StringComparison.Ordinal)) + .Select(command => command[..^1]) + .ToArray(); + } } } diff --git a/src/PlaywrightNative/WebKit/MacEditingCommands.cs b/src/PlaywrightNative/Input/MacEditingCommands.cs similarity index 81% rename from src/PlaywrightNative/WebKit/MacEditingCommands.cs rename to src/PlaywrightNative/Input/MacEditingCommands.cs index 12a787b5..39a1291d 100644 --- a/src/PlaywrightNative/WebKit/MacEditingCommands.cs +++ b/src/PlaywrightNative/Input/MacEditingCommands.cs @@ -15,14 +15,16 @@ * limitations under the License. */ using System.Collections.Generic; +using System.Linq; -namespace PlaywrightNative.WebKit +namespace PlaywrightNative.Input { /// /// Maps a keyboard shortcut (modifiers + physical code, joined with +) to the - /// macOS editing command(s) WebKit should execute for that key. WebKit applies these named - /// commands (e.g. insertNewline: for Enter) instead of inserting the raw key text, - /// so an Enter in a textarea produces a real newline rather than a carriage return. + /// macOS editing command(s) the browser should execute for that key. WebKit and Chromium on + /// macOS apply these named commands (e.g. insertNewline: for Enter, selectAll: + /// for Meta+A) because the native key bindings that would run them are not used for + /// synthetic key events. /// Ported from upstream Playwright's macEditingCommands.ts. /// internal static class MacEditingCommands @@ -145,6 +147,44 @@ internal static class MacEditingCommands ["Shift+Meta+KeyZ"] = ["redo:"], }; + /// + /// Builds the shortcut string for a key press: held modifiers (in + /// Shift+Control+Alt+Meta order) followed by the physical key code, joined + /// with +. Mirrors upstream wkInput.ts and crInput.ts. + /// + /// The held modifiers. + /// The physical event.code of the key. + /// The shortcut string used to look up macOS editing commands. + internal static string BuildShortcut(IReadOnlyCollection modifiers, string code) + { + List parts = new(); + if (modifiers != null) + { + if (modifiers.Contains(KeyboardModifier.Shift)) + { + parts.Add("Shift"); + } + + if (modifiers.Contains(KeyboardModifier.Control)) + { + parts.Add("Control"); + } + + if (modifiers.Contains(KeyboardModifier.Alt)) + { + parts.Add("Alt"); + } + + if (modifiers.Contains(KeyboardModifier.Meta)) + { + parts.Add("Meta"); + } + } + + parts.Add(code); + return string.Join("+", parts); + } + /// /// Resolves the macOS editing command(s) for a shortcut, or an empty array when the /// shortcut has no associated command. diff --git a/src/PlaywrightNative/WebKit/WKRawKeyboard.cs b/src/PlaywrightNative/WebKit/WKRawKeyboard.cs index e274e726..973ed08f 100644 --- a/src/PlaywrightNative/WebKit/WKRawKeyboard.cs +++ b/src/PlaywrightNative/WebKit/WKRawKeyboard.cs @@ -68,48 +68,10 @@ public static int ToWebKitModifiersMask(IReadOnlyCollection - /// Builds the macEditingCommands shortcut string for a key press: held modifiers - /// (in Shift+Control+Alt+Meta order) followed by the physical key code, - /// joined with +. Mirrors upstream wkInput.ts. - /// - /// The held modifiers. - /// The physical event.code of the key. - /// The shortcut string used to look up macOS editing commands. - public static string BuildShortcut(IReadOnlyCollection modifiers, string code) - { - List parts = new(); - if (modifiers != null) - { - if (modifiers.Contains(Input.KeyboardModifier.Shift)) - { - parts.Add("Shift"); - } - - if (modifiers.Contains(Input.KeyboardModifier.Control)) - { - parts.Add("Control"); - } - - if (modifiers.Contains(Input.KeyboardModifier.Alt)) - { - parts.Add("Alt"); - } - - if (modifiers.Contains(Input.KeyboardModifier.Meta)) - { - parts.Add("Meta"); - } - } - - parts.Add(code); - return string.Join("+", parts); - } - /// public Task KeyDownAsync(IReadOnlyCollection modifiers, Input.KeyDefinition key, bool autoRepeat) { - string shortcut = BuildShortcut(modifiers, key.Code); + string shortcut = MacEditingCommands.BuildShortcut(modifiers, key.Code); // Linux/Windows WebKit uses native Ctrl+A select-all. The macOS table maps // Control+KeyA to moveToBeginningOfParagraph, which would steal selectAll.