Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/PlaywrightNative/Chromium/CRPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
29 changes: 28 additions & 1 deletion src/PlaywrightNative/Chromium/CRRawKeyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,16 +30,19 @@ internal class CRRawKeyboard : IRawKeyboard
{
private readonly CRSession _session;
private readonly CRDragManager _dragManager;
private readonly bool _isMac;

/// <summary>
/// Initializes a new instance of the <see cref="CRRawKeyboard"/> class.
/// </summary>
/// <param name="session">The CDP session to send commands on.</param>
/// <param name="dragManager">Chromium drag interceptor.</param>
public CRRawKeyboard(CRSession session, CRDragManager dragManager)
/// <param name="isMac">Whether the browser runs on macOS and needs editing commands.</param>
public CRRawKeyboard(CRSession session, CRDragManager dragManager, bool isMac)
{
_session = session;
_dragManager = dragManager;
_isMac = isMac;
}

/// <summary>
Expand All @@ -52,13 +57,15 @@ public async Task KeyDownAsync(IReadOnlyCollection<Input.KeyboardModifier> modif
}

string type = string.IsNullOrEmpty(key.Text) ? "rawKeyDown" : "keyDown";
string[] commands = CommandsForCode(key.Code, modifiers);

await _session.SendAsync("Input.dispatchKeyEvent", new
{
type,
modifiers = modifiers.ToCdpMask(),
windowsVirtualKeyCode = key.KeyCodeWithoutLocation == 0 ? key.KeyCode : key.KeyCodeWithoutLocation,
code = key.Code,
commands,
key = key.Key,
text = key.Text,
unmodifiedText = key.Text,
Expand Down Expand Up @@ -92,5 +99,25 @@ public Task InsertTextAsync(string text)
{
return _session.SendAsync("Input.insertText", new { text });
}

/// <summary>
/// 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 <c>crInput.ts</c> <c>_commandsForCode</c>.
/// </summary>
private string[] CommandsForCode(string code, IReadOnlyCollection<Input.KeyboardModifier> modifiers)
{
if (!_isMac)
{
return Array.Empty<string>();
}

// 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@
* limitations under the License.
*/
using System.Collections.Generic;
using System.Linq;

namespace PlaywrightNative.WebKit
namespace PlaywrightNative.Input
{
/// <summary>
/// Maps a keyboard shortcut (modifiers + physical <c>code</c>, joined with <c>+</c>) to the
/// macOS editing command(s) WebKit should execute for that key. WebKit applies these named
/// commands (e.g. <c>insertNewline:</c> 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. <c>insertNewline:</c> for Enter, <c>selectAll:</c>
/// for Meta+A) because the native key bindings that would run them are not used for
/// synthetic key events.
/// Ported from upstream Playwright's <c>macEditingCommands.ts</c>.
/// </summary>
internal static class MacEditingCommands
Expand Down Expand Up @@ -145,6 +147,44 @@ internal static class MacEditingCommands
["Shift+Meta+KeyZ"] = ["redo:"],
};

/// <summary>
/// Builds the shortcut string for a key press: held modifiers (in
/// <c>Shift+Control+Alt+Meta</c> order) followed by the physical key code, joined
/// with <c>+</c>. Mirrors upstream <c>wkInput.ts</c> and <c>crInput.ts</c>.
/// </summary>
/// <param name="modifiers">The held modifiers.</param>
/// <param name="code">The physical <c>event.code</c> of the key.</param>
/// <returns>The shortcut string used to look up macOS editing commands.</returns>
internal static string BuildShortcut(IReadOnlyCollection<KeyboardModifier> modifiers, string code)
{
List<string> 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);
}

/// <summary>
/// Resolves the macOS editing command(s) for a shortcut, or an empty array when the
/// shortcut has no associated command.
Expand Down
40 changes: 1 addition & 39 deletions src/PlaywrightNative/WebKit/WKRawKeyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,48 +68,10 @@ public static int ToWebKitModifiersMask(IReadOnlyCollection<Input.KeyboardModifi
return mask;
}

/// <summary>
/// Builds the macEditingCommands shortcut string for a key press: held modifiers
/// (in <c>Shift+Control+Alt+Meta</c> order) followed by the physical key code,
/// joined with <c>+</c>. Mirrors upstream <c>wkInput.ts</c>.
/// </summary>
/// <param name="modifiers">The held modifiers.</param>
/// <param name="code">The physical <c>event.code</c> of the key.</param>
/// <returns>The shortcut string used to look up macOS editing commands.</returns>
public static string BuildShortcut(IReadOnlyCollection<Input.KeyboardModifier> modifiers, string code)
{
List<string> 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);
}

/// <inheritdoc/>
public Task KeyDownAsync(IReadOnlyCollection<Input.KeyboardModifier> 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.
Expand Down
Loading