Skip to content

Commit

Permalink
chore(input): add keyboard layouts
Browse files Browse the repository at this point in the history
Logic for dead keys handling added.

See #24249 (comment)
See #24249 (comment)
  • Loading branch information
ruifigueira committed Jul 26, 2023
1 parent 9cb34ae commit 1c8bbbb
Show file tree
Hide file tree
Showing 27 changed files with 1,592 additions and 218 deletions.
20 changes: 14 additions & 6 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1741,9 +1741,17 @@ Keyboard layout code. Currently, the following values are supported:

| Values | Name |
| :- | :- |
| `us`, `en-US` | [US keyboard](https://learn.microsoft.com/en-us/globalization/keyboards/kbdus_7) <!-- 00000409 --> |
| `es`, `es-ES` | [Spanish keyboard](https://learn.microsoft.com/en-us/globalization/keyboards/kbdsp) <!-- 0000040A --> |
| `br`, `pt-BR` | [Portuguese (Brazil ABNT) keyboard](https://learn.microsoft.com/en-us/globalization/keyboards/kbdbr_1) <!-- 00000416 --> |
| `latam`, `es-MX` | [Latin American keyboard](https://learn.microsoft.com/en-us/globalization/keyboards/kbdla) <!-- 0000080A --> |
| `pt`, `pt-PT` | [Portuguese keyboard](https://learn.microsoft.com/en-us/globalization/keyboards/kbdpo) <!-- 00000816 --> |
| `el`, `el-GR` | [Greek keyboard](https://learn.microsoft.com/en-us/globalization/keyboards/kbdhe) <!-- 00000408 --> |
| `us`, `en-US` | [US English](https://learn.microsoft.com/en-us/globalization/keyboards/kbdus_7) <!-- 00000409 --> |
| `gb`, `en-GB` | [British](https://learn.microsoft.com/en-us/globalization/keyboards/kbduk) <!-- 00000809 --> |
| `dk`, `da-DK` | [Danish](https://learn.microsoft.com/en-us/globalization/keyboards/kbdda) <!-- 00000406 --> |
| `fr`, `fr-FR` | [French](https://learn.microsoft.com/en-us/globalization/keyboards/kbdfr) <!-- 0000040C --> |
| `de`, `de-DE` | [German](https://learn.microsoft.com/en-us/globalization/keyboards/kbdgr) <!-- 00000407 --> |
| `it`, `it-IT` | [Italian](https://learn.microsoft.com/en-us/globalization/keyboards/kbdit) <!-- 00000410 --> |
| `pt`, `pt-PT` | [Portuguese (Portugal)](https://learn.microsoft.com/en-us/globalization/keyboards/kbdpo) <!-- 00000816 --> |
| `br`, `pt-BR` | [Portuguese (Brazil)](https://learn.microsoft.com/en-us/globalization/keyboards/kbdpo) <!-- 00000416 --> |
| `ru`, `ru-RU` | [Russian](https://learn.microsoft.com/en-us/globalization/keyboards/kbdru) <!-- 00000419 --> |
| `ua`, `uk-UA` | [Ukrainian](https://learn.microsoft.com/en-us/globalization/keyboards/kbdur1) <!-- 00020422 --> |
| `es`, `es-ES`, `ca-ES` | [Spanish & Catalan (Spain)](https://learn.microsoft.com/en-us/globalization/keyboards/kbdsp) <!-- 0000040A --> |
| `latam` | [Spanish (Latin America)](https://learn.microsoft.com/en-us/globalization/keyboards/kbdla) <!-- 0000080A --> |
| `ch`, `de-CH` | [German (Switzerland)](https://learn.microsoft.com/en-us/globalization/keyboards/kbdsg) <!-- 00000807 --> |
| `fr-CH`, `it-CH` | [French and Italian (Switzerland)](https://learn.microsoft.com/en-us/globalization/keyboards/kbdsf_2) <!-- 0000100C --> |
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/chromium/crInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export class RawKeyboardImpl implements input.RawKeyboard {
if (code === 'Escape' && await this._dragManger.cancelDrag())
return;
const commands = this._commandsForCode(code, modifiers);
if (key === 'Dead')
text = '';
await this._client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: toModifiersMask(modifiers),
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/firefox/ffInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export class RawKeyboardImpl implements input.RawKeyboard {
// Firefox will figure out Enter by itself
if (text === '\r')
text = '';
if (key === 'Dead')
text = '';
await this._client.send('Page.dispatchKeyEvent', {
type: 'keydown',
keyCode: keyCodeWithoutLocation,
Expand Down
69 changes: 55 additions & 14 deletions packages/playwright-core/src/server/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ type KeyDescription = {
code: string,
location: number,
shifted?: KeyDescription;
deadKeyMappings?: Map<string, string>;
};

// either the key description or a sequence of keys for accented keys
// e.g. in portuguese, 'à' will be ['Shift+BracketRight', 'a']
type KeyboardLayoutClosure = Map<string, KeyDescription | string[]>;

const kModifiers: types.KeyboardModifier[] = ['Alt', 'Control', 'Meta', 'Shift'];

export interface RawKeyboard {
Expand All @@ -45,7 +50,8 @@ export class Keyboard {
private _pressedKeys = new Set<string>();
private _raw: RawKeyboard;
private _page: Page;
private _keyboardLayout: Map<string, KeyDescription>;
private _keyboardLayout: Map<string, KeyDescription | string[]>;
private _deadKeyMappings?: Map<string, string>;

constructor(raw: RawKeyboard, page: Page) {
this._raw = raw;
Expand All @@ -59,32 +65,43 @@ export class Keyboard {

async down(key: string) {
const description = this._keyDescriptionForString(key);
this._deadKeyMappings = undefined;
const autoRepeat = this._pressedKeys.has(description.code);
this._pressedKeys.add(description.code);
if (kModifiers.includes(description.key as types.KeyboardModifier))
this._pressedModifiers.add(description.key as types.KeyboardModifier);
const text = description.text;
await this._raw.keydown(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, description.key, description.location, autoRepeat, text);
const descKey = description.deadKeyMappings ? 'Dead' : description.key;
await this._raw.keydown(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, descKey, description.location, autoRepeat, description.text);
}

private _keyDescriptionForString(keyString: string): KeyDescription {
let description = this._keyboardLayout.get(keyString);
assert(description, `Unknown key: "${keyString}"`);
assert(!Array.isArray(description), `Accented key not supported: "${keyString}"`);

const shift = this._pressedModifiers.has('Shift');
description = shift && description.shifted ? description.shifted : description;

// if any modifiers besides shift are pressed, no text should be sent
if (this._pressedModifiers.size > 1 || (!this._pressedModifiers.has('Shift') && this._pressedModifiers.size === 1))
return { ...description, text: '' };
return description;

if (!this._deadKeyMappings) return description;

// handle deadkeys / accented keys
const deadKeyText = this._deadKeyMappings.get(description.text) ?? this._deadKeyMappings.get('');
assert(deadKeyText, `Unknown key: "${description.text}"`);
return { ...description, text: deadKeyText, key: deadKeyText };
}

async up(key: string) {
const description = this._keyDescriptionForString(key);
if (kModifiers.includes(description.key as types.KeyboardModifier))
this._pressedModifiers.delete(description.key as types.KeyboardModifier);
this._pressedKeys.delete(description.code);
await this._raw.keyup(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, description.key, description.location);
const descKey = description.deadKeyMappings ? 'Dead' : description.key;
await this._raw.keyup(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, descKey, description.location);
if (description.key !== 'Shift') this._deadKeyMappings = description.deadKeyMappings;
}

async insertText(text: string) {
Expand All @@ -94,8 +111,14 @@ export class Keyboard {
async type(text: string, options?: { delay?: number }) {
const delay = (options && options.delay) || undefined;
for (const char of text) {
if (this._keyboardLayout.has(char)) {
await this.press(char, { delay });
const descOrKeySequence = this._keyboardLayout.get(char);
if (descOrKeySequence) {
if (Array.isArray(descOrKeySequence)) {
for (const key of descOrKeySequence)
await this.press(key, { delay });
} else {
await this.press(char, { delay });
}
} else {
if (delay)
await new Promise(f => setTimeout(f, delay));
Expand Down Expand Up @@ -249,17 +272,17 @@ const aliases = new Map<string, string[]>([
]);

const defaultKeyboard = _buildLayoutClosure(defaultKeyboardLayout);
const cache = new Map<string, Map<string, KeyDescription>>(
const cache = new Map<string, KeyboardLayoutClosure>(
// initialized with the default keyboard layout
[[defaultKlid, defaultKeyboard]]
);

function getByLocale(locale?: string): Map<string, KeyDescription> {
function getByLocale(locale?: string): KeyboardLayoutClosure {
if (!locale) return defaultKeyboard;

const normalizedLocale = normalizeLocale(locale);
const klid = localeMapping.get(normalizedLocale);
if (!klid) throw new Error(`Keyboard layout name '${klid}' not found`);
if (!klid) throw new Error(`Keyboard layout name "${locale}" not found`);

const cached = cache.get(klid);
if (cached) return cached;
Expand All @@ -277,8 +300,8 @@ function normalizeLocale(locale: string) {
return locale.replace(/-/g, '_').toLowerCase();
}

function _buildLayoutClosure(layout: KeyboardLayout): Map<string, KeyDescription> {
const result = new Map<string, KeyDescription>();
function _buildLayoutClosure(layout: KeyboardLayout): KeyboardLayoutClosure {
const result = new Map<string, KeyDescription | string[]>();
for (const code in layout) {
const definition = layout[code];
const description: KeyDescription = {
Expand All @@ -288,6 +311,7 @@ function _buildLayoutClosure(layout: KeyboardLayout): Map<string, KeyDescription
code,
text: definition.text || '',
location: definition.location || 0,
deadKeyMappings: definition.key && definition.deadKeyMappings ? new Map([...Object.entries(definition.deadKeyMappings), ['', definition.key]]) : undefined,
};
if (definition.key?.length === 1)
description.text = description.key;
Expand All @@ -301,6 +325,8 @@ function _buildLayoutClosure(layout: KeyboardLayout): Map<string, KeyDescription
shiftedDescription.text = definition.shiftKey;
if (definition.shiftKeyCode)
shiftedDescription.keyCode = definition.shiftKeyCode;
if (definition.shiftDeadKeyMappings)
shiftedDescription.deadKeyMappings = new Map([...Object.entries(definition.shiftDeadKeyMappings), ['', definition.shiftKey]]);
}

// Map from code: Digit3 -> { ... descrption, shifted }
Expand All @@ -320,9 +346,24 @@ function _buildLayoutClosure(layout: KeyboardLayout): Map<string, KeyDescription
if (description.key.length === 1)
result.set(description.key, description);

// Map from shiftKey, no shifted
if (shiftedDescription)
// Map from accented keys
if (definition.deadKeyMappings) {
for (const [k, v] of Object.entries(definition.deadKeyMappings))
// if there's a dedicated accented key, we don't want to replace them
if (!result.has(v)) result.set(v, [code, k]);
}

if (shiftedDescription) {
// Map from shiftKey, no shifted
result.set(shiftedDescription.key, { ...shiftedDescription, shifted: undefined });

// Map from shifted accented keys
if (definition.shiftDeadKeyMappings) {
for (const [k, v] of Object.entries(definition.shiftDeadKeyMappings))
// if there's a dedicated accented key, we don't want to replace them
if (!result.has(v)) result.set(v, [`Shift+${code}`, k]);
}
}
}
return result;
}
Expand Down
40 changes: 28 additions & 12 deletions packages/playwright-core/src/server/keyboards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,34 @@ export const defaultKlid = '00000409';
export const defaultKeyboardLayout: KeyboardLayout = defaultKeyboardLayoutObject;

export const localeMapping = new Map<string, string>([
['us', '00000409'], // US keyboard
['en_us', '00000409'], // US keyboard
['es', '0000040A'], // Spanish keyboard
['es_es', '0000040A'], // Spanish keyboard
['br', '00000416'], // Portuguese (Brazil ABNT) keyboard
['pt_br', '00000416'], // Portuguese (Brazil ABNT) keyboard
['latam', '0000080A'], // Latin American keyboard
['es_mx', '0000080A'], // Latin American keyboard
['pt', '00000816'], // Portuguese keyboard
['pt_pt', '00000816'], // Portuguese keyboard
['el', '00000408'], // Greek keyboard
['el_gr', '00000408'], // Greek keyboard
['us', '00000409'], // US English
['en_us', '00000409'], // US English
['gb', '00000809'], // British
['en_gb', '00000809'], // British
['dk', '00000406'], // Danish
['da_dk', '00000406'], // Danish
['fr', '0000040C'], // French
['fr_fr', '0000040C'], // French
['de', '00000407'], // German
['de_de', '00000407'], // German
['it', '00000410'], // Italian
['it_it', '00000410'], // Italian
['pt', '00000816'], // Portuguese (Portugal)
['pt_pt', '00000816'], // Portuguese (Portugal)
['br', '00000416'], // Portuguese (Brazil)
['pt_br', '00000416'], // Portuguese (Brazil)
['ru', '00000419'], // Russian
['ru_ru', '00000419'], // Russian
['ua', '00020422'], // Ukrainian
['uk_ua', '00020422'], // Ukrainian
['es', '0000040A'], // Spanish & Catalan (Spain)
['es_es', '0000040A'], // Spanish & Catalan (Spain)
['ca_es', '0000040A'], // Spanish & Catalan (Spain)
['latam', '0000080A'], // Spanish (Latin America)
['ch', '00000807'], // German (Switzerland)
['de_ch', '00000807'], // German (Switzerland)
['fr_ch', '0000100C'], // French and Italian (Switzerland)
['it_ch', '0000100C'], // French and Italian (Switzerland)
]);

export const keypadLocation = 3;
Loading

0 comments on commit 1c8bbbb

Please sign in to comment.