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 5011334
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: KeyboardLayoutClosure;
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);
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)) : 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));
}

// 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;
131 changes: 131 additions & 0 deletions packages/playwright-core/src/server/keyboards/layouts/00000406.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// This file is generated by generate_keyboard_layouts.js, do not edit manually.

import type { KeyboardLayout } from '../types';

// KLID 00000406 - Danish
const keyboardLayout: KeyboardLayout = {
Escape: { key: 'Escape', keyCode: 27 },
F1: { key: 'F1', keyCode: 112 },
F2: { key: 'F2', keyCode: 113 },
F3: { key: 'F3', keyCode: 114 },
F4: { key: 'F4', keyCode: 115 },
F5: { key: 'F5', keyCode: 116 },
F6: { key: 'F6', keyCode: 117 },
F7: { key: 'F7', keyCode: 118 },
F8: { key: 'F8', keyCode: 119 },
F9: { key: 'F9', keyCode: 120 },
F10: { key: 'F10', keyCode: 121 },
F11: { key: 'F11', keyCode: 122 },
F12: { key: 'F12', keyCode: 123 },
Backquote: { key: '½', keyCode: 220, shiftKey: '§' },
Digit1: { key: '1', keyCode: 49, shiftKey: '!' },
Digit2: { key: '2', keyCode: 50, shiftKey: '"' },
Digit3: { key: '3', keyCode: 51, shiftKey: '#' },
Digit4: { key: '4', keyCode: 52, shiftKey: '¤' },
Digit5: { key: '5', keyCode: 53, shiftKey: '%' },
Digit6: { key: '6', keyCode: 54, shiftKey: '&' },
Digit7: { key: '7', keyCode: 55, shiftKey: '/' },
Digit8: { key: '8', keyCode: 56, shiftKey: '(' },
Digit9: { key: '9', keyCode: 57, shiftKey: ')' },
Digit0: { key: '0', keyCode: 48, shiftKey: '=' },
Minus: { key: '+', keyCode: 187, shiftKey: '?' },
Equal: { key: '´', keyCode: 219, shiftKey: '`', deadKeyMappings: { 'a': 'á', 'e': 'é', 'i': 'í', 'o': 'ó', 'u': 'ú', 'y': 'ý', 'A': 'Á', 'E': 'É', 'I': 'Í', 'O': 'Ó', 'U': 'Ú', 'Y': 'Ý', ' ': '´' }, shiftDeadKeyMappings: { 'a': 'à', 'e': 'è', 'i': 'ì', 'o': 'ò', 'u': 'ù', 'A': 'À', 'E': 'È', 'I': 'Ì', 'O': 'Ò', 'U': 'Ù', ' ': '`' } },
Backspace: { key: 'Backspace', keyCode: 8 },
Tab: { key: 'Tab', keyCode: 9 },
KeyQ: { key: 'q', keyCode: 81, shiftKey: 'Q' },
KeyW: { key: 'w', keyCode: 87, shiftKey: 'W' },
KeyE: { key: 'e', keyCode: 69, shiftKey: 'E' },
KeyR: { key: 'r', keyCode: 82, shiftKey: 'R' },
KeyT: { key: 't', keyCode: 84, shiftKey: 'T' },
KeyY: { key: 'y', keyCode: 89, shiftKey: 'Y' },
KeyU: { key: 'u', keyCode: 85, shiftKey: 'U' },
KeyI: { key: 'i', keyCode: 73, shiftKey: 'I' },
KeyO: { key: 'o', keyCode: 79, shiftKey: 'O' },
KeyP: { key: 'p', keyCode: 80, shiftKey: 'P' },
BracketLeft: { key: 'å', keyCode: 221, shiftKey: 'Å' },
BracketRight: { key: '¨', keyCode: 186, shiftKey: '^', deadKeyMappings: { 'a': 'ä', 'e': 'ë', 'i': 'ï', 'o': 'ö', 'u': 'ü', 'y': 'ÿ', 'A': 'Ä', 'E': 'Ë', 'I': 'Ï', 'O': 'Ö', 'U': 'Ü', ' ': '¨' }, shiftDeadKeyMappings: { 'a': 'â', 'e': 'ê', 'i': 'î', 'o': 'ô', 'u': 'û', 'A': 'Â', 'E': 'Ê', 'I': 'Î', 'O': 'Ô', 'U': 'Û', ' ': '^' } },
Enter: { key: 'Enter', keyCode: 13, text: '\r' },
CapsLock: { key: 'CapsLock', keyCode: 20 },
KeyA: { key: 'a', keyCode: 65, shiftKey: 'A' },
KeyS: { key: 's', keyCode: 83, shiftKey: 'S' },
KeyD: { key: 'd', keyCode: 68, shiftKey: 'D' },
KeyF: { key: 'f', keyCode: 70, shiftKey: 'F' },
KeyG: { key: 'g', keyCode: 71, shiftKey: 'G' },
KeyH: { key: 'h', keyCode: 72, shiftKey: 'H' },
KeyJ: { key: 'j', keyCode: 74, shiftKey: 'J' },
KeyK: { key: 'k', keyCode: 75, shiftKey: 'K' },
KeyL: { key: 'l', keyCode: 76, shiftKey: 'L' },
Semicolon: { key: 'æ', keyCode: 192, shiftKey: 'Æ' },
Quote: { key: 'ø', keyCode: 222, shiftKey: 'Ø' },
Backslash: { key: '\'', keyCode: 191, shiftKey: '*' },
ShiftLeft: { key: 'Shift', keyCode: 160, keyCodeWithoutLocation: 16, location: 1 },
IntlBackslash: { key: '<', keyCode: 226, shiftKey: '>' },
KeyZ: { key: 'z', keyCode: 90, shiftKey: 'Z' },
KeyX: { key: 'x', keyCode: 88, shiftKey: 'X' },
KeyC: { key: 'c', keyCode: 67, shiftKey: 'C' },
KeyV: { key: 'v', keyCode: 86, shiftKey: 'V' },
KeyB: { key: 'b', keyCode: 66, shiftKey: 'B' },
KeyN: { key: 'n', keyCode: 78, shiftKey: 'N' },
KeyM: { key: 'm', keyCode: 77, shiftKey: 'M' },
Comma: { key: ',', keyCode: 188, shiftKey: ';' },
Period: { key: '.', keyCode: 190, shiftKey: ':' },
Slash: { key: '-', keyCode: 189, shiftKey: '_' },
ShiftRight: { key: 'Shift', keyCode: 161, keyCodeWithoutLocation: 16, location: 2 },
ControlLeft: { key: 'Control', keyCode: 162, keyCodeWithoutLocation: 17, location: 1 },
MetaLeft: { key: 'Meta', keyCode: 91, location: 1 },
AltLeft: { key: 'Alt', keyCode: 164, keyCodeWithoutLocation: 18, location: 1 },
Space: { key: ' ', keyCode: 32 },
AltRight: { key: 'Alt', keyCode: 165, keyCodeWithoutLocation: 18, location: 2 },
AltGraph: { key: 'AltGraph', keyCode: 225 },
MetaRight: { key: 'Meta', keyCode: 92, location: 2 },
ContextMenu: { key: 'ContextMenu', keyCode: 93 },
ControlRight: { key: 'Control', keyCode: 163, keyCodeWithoutLocation: 17, location: 2 },
PrintScreen: { key: 'PrintScreen', keyCode: 44 },
ScrollLock: { key: 'ScrollLock', keyCode: 145 },
Pause: { key: 'Pause', keyCode: 19 },
PageUp: { key: 'PageUp', keyCode: 33 },
PageDown: { key: 'PageDown', keyCode: 34 },
Insert: { key: 'Insert', keyCode: 45 },
Delete: { key: 'Delete', keyCode: 46 },
Home: { key: 'Home', keyCode: 36 },
End: { key: 'End', keyCode: 35 },
ArrowLeft: { key: 'ArrowLeft', keyCode: 37 },
ArrowUp: { key: 'ArrowUp', keyCode: 38 },
ArrowRight: { key: 'ArrowRight', keyCode: 39 },
ArrowDown: { key: 'ArrowDown', keyCode: 40 },
NumLock: { key: 'NumLock', keyCode: 144 },
NumpadDivide: { key: '/', keyCode: 111, location: 3 },
NumpadMultiply: { key: '*', keyCode: 106, location: 3 },
NumpadSubtract: { key: '-', keyCode: 109, location: 3 },
Numpad7: { key: 'Home', keyCode: 36, shiftKey: '7', shiftKeyCode: 103, location: 3 },
Numpad8: { key: 'ArrowUp', keyCode: 38, shiftKey: '8', shiftKeyCode: 104, location: 3 },
Numpad9: { key: 'PageUp', keyCode: 33, shiftKey: '9', shiftKeyCode: 105, location: 3 },
Numpad4: { key: 'ArrowLeft', keyCode: 37, shiftKey: '4', shiftKeyCode: 100, location: 3 },
Numpad5: { key: 'Clear', keyCode: 12, shiftKey: '5', shiftKeyCode: 101, location: 3 },
Numpad6: { key: 'ArrowRight', keyCode: 39, shiftKey: '6', shiftKeyCode: 102, location: 3 },
NumpadAdd: { key: '+', keyCode: 107, location: 3 },
Numpad1: { key: 'End', keyCode: 35, shiftKey: '1', shiftKeyCode: 97, location: 3 },
Numpad2: { key: 'ArrowDown', keyCode: 40, shiftKey: '2', shiftKeyCode: 98, location: 3 },
Numpad3: { key: 'PageDown', keyCode: 34, shiftKey: '3', shiftKeyCode: 99, location: 3 },
Numpad0: { key: 'Insert', keyCode: 45, shiftKey: '0', shiftKeyCode: 96, location: 3 },
NumpadDecimal: { key: '\u0000', keyCode: 46, shiftKey: '.', shiftKeyCode: 110, location: 3 },
NumpadEnter: { key: 'Enter', keyCode: 13, text: '\r', location: 3 },
};

export default keyboardLayout;
Loading

0 comments on commit 5011334

Please sign in to comment.