mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 22:32:46 +00:00
Bug 1840519 - Make typing surrogate pair behavior switchable with prefs r=m_kato,smaug
A lone surrogate should not appear in `DOMString` at least when the attribute values of events because of ill-formed UTF-16 string. `TextEventDispatcher` does not handle surrogate pairs correctly. It should not split surrogate pairs when it sets `KeyboardEvent.key` value for avoiding the problem in some DOM API wrappers, e.g., Rust-running-as-wasm. On the other hand, `.charCode` is an unsigned long attribute and web apps may use `String.fromCharCode(event.charCode)` to convert the input to string, and unfortunately, `fromCharCode` does not support Unicode character code points over `0xFFFF`. Therefore, we may need to keep dispatching 2 `keypress` events per surrogate pair for the backward compatibility. Therefore, this patch creates 2 prefs. One is for using single-keypress event model and double-keypress event model. The other is for the latter, whether `.key` value never has ill-formed UTF-16 or it's allowed. If using the single-keypress event model --this is compatible with Safari and Chrome in non-Windows platforms--, one `keypress` event is dispatched for typing a surrogate pair. Then, its `.charCode` is over `0xFFFF` which can work with `String.fromCodePoint()` instead of `String.fromCharCode()` and `.key` value is set to simply the surrogate pair (i.e., its length is 2). If using the double-keypress event model and disallowing ill-formed UTF-16 --this is the new default behavior for both avoiding ill-formed UTF-16 string creation and keeping backward compatibility with not-maintained web apps using `String.fromCharCode`--, 2 `keypress` events are dispatched. `.charCode` for first one is the code of the high-surrogate, but `.key` is the surrogate pair. Then, `.charCode` for second one is the low-surrogate and `.key` is empty string. In this mode, `TextEditor` and `HTMLEditor` ignores the second `keypress`. Therefore, web apps can cancel it only with the first `keypress`, but it indicates the `keypress` introduces a surrogate pair with `.key` attribute. Otherwise, if using the double-keypress event model and allowing ill-formed UTF-16 --this is the traditional our behavior and compatible with Chrome in Windows--, 2 `keypress` events are dispatched with same `.charCode` values as the previous mode, but first `.key` is the high-surrogate and the other's is the low surrogate. Therefore, web apps can cancel either one of them or both of them. Finally, this patch makes `TextEditor` and `HTMLEditor` handle text input with `keypress` events properly. Except in the last mode, `beforeinput` and `input` events are fired once and their `data` values are the surrogate pair. On the other hand, in the last mode, 2 sets of `beforeinput` and `input` are fired and their `.data` values has only the surrogate so that ill-formed UTF-16 values. Note that this patch also fixes an issue on Windows. Windows may send a high surrogate and a low surrogate with 2 sets of `WM_KEYDOWN` and `WM_KEYUP` whose virtual keycode is `VK_PACKET` (typically, this occurs with `SendInput` API). For handling this correctly, this patch changes `NativeKey` class to make it just store the high surrogate for the first `WM_KEYDOWN` and `WM_KEYUP` and use it when it'll receive another `WM_KEYDOWN` for a low surrogate. Differential Revision: https://phabricator.services.mozilla.com/D182142
This commit is contained in:
parent
3abfdd94e9
commit
8224e1138c
@ -2444,7 +2444,7 @@ function runConsumingKeydownBeforeCompositionTests()
|
||||
window.removeEventListener("keyup", handler, false);
|
||||
}
|
||||
|
||||
function runKeyTests()
|
||||
async function runKeyTests()
|
||||
{
|
||||
var description = "runKeyTests(): ";
|
||||
const kModifiers =
|
||||
@ -2637,6 +2637,127 @@ function runKeyTests()
|
||||
is(input.value, "abc",
|
||||
description + "input.value should be \"abc\"");
|
||||
|
||||
// Emulates pressing and releasing a key which introduces a surrogate pair.
|
||||
async function test_press_and_release_surrogate_pair_key(
|
||||
aTestPerSurrogateKeyPress,
|
||||
aTestIllFormedUTF16KeyValue = false
|
||||
) {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
|
||||
["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
|
||||
],
|
||||
});
|
||||
|
||||
const settingDescription =
|
||||
`aTestPerSurrogateKeyPress=${aTestPerSurrogateKeyPress}, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
|
||||
const allowIllFormedUTF16 = aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
|
||||
const keySurrogatePair = new KeyboardEvent("", { key: "\uD842\uDFB7", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
|
||||
|
||||
input.value = "";
|
||||
reset();
|
||||
doDefaultKeydown = TIP.keydown(keySurrogatePair);
|
||||
doDefaultKeyup = TIP.keyup(keySurrogatePair);
|
||||
|
||||
is(
|
||||
doDefaultKeydown,
|
||||
TIP.KEYPRESS_IS_CONSUMED,
|
||||
`${
|
||||
description
|
||||
}TIP.keydown(keySurrogatePair), ${
|
||||
settingDescription
|
||||
}, should return 0x02 because the keypress event should be consumed by the input element`
|
||||
);
|
||||
is(
|
||||
doDefaultKeyup,
|
||||
true,
|
||||
`${description}TIP.keyup(keySurrogatePair) should return true`
|
||||
);
|
||||
is(
|
||||
events.length,
|
||||
aTestPerSurrogateKeyPress ? 4 : 3,
|
||||
`${description}TIP.keydown(keySurrogatePair), ${
|
||||
settingDescription
|
||||
}, should cause keydown, keypress${
|
||||
aTestPerSurrogateKeyPress ? ", keypress" : ""
|
||||
} and keyup event`
|
||||
);
|
||||
checkKeyAttrs(
|
||||
`${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
|
||||
events[0],
|
||||
{
|
||||
type: "keydown",
|
||||
key: "\uD842\uDFB7",
|
||||
code: "KeyA",
|
||||
keyCode: KeyboardEvent.DOM_VK_A,
|
||||
charCode: 0,
|
||||
}
|
||||
);
|
||||
if (aTestPerSurrogateKeyPress) {
|
||||
checkKeyAttrs(
|
||||
`${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
|
||||
events[1],
|
||||
{
|
||||
type: "keypress",
|
||||
key: allowIllFormedUTF16
|
||||
? "\uD842"
|
||||
: "\uD842\uDFB7", // First keypress should have the surrogate pair
|
||||
code: "KeyA",
|
||||
keyCode: 0,
|
||||
charCode: "\uD842\uDFB7".charCodeAt(0),
|
||||
defaultPrevented: true,
|
||||
}
|
||||
);
|
||||
checkKeyAttrs(
|
||||
`${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
|
||||
events[2],
|
||||
{
|
||||
type: "keypress",
|
||||
key: allowIllFormedUTF16
|
||||
? "\uDFB7"
|
||||
: "", // But the following keypress should have empty string, instead
|
||||
code: "KeyA",
|
||||
keyCode: 0,
|
||||
charCode: "\uD842\uDFB7".charCodeAt(1),
|
||||
defaultPrevented: true,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
checkKeyAttrs(
|
||||
`${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
|
||||
events[1],
|
||||
{
|
||||
type: "keypress",
|
||||
key: "\uD842\uDFB7",
|
||||
code: "KeyA",
|
||||
keyCode: 0,
|
||||
charCode: 0x20BB7,
|
||||
defaultPrevented: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
checkKeyAttrs(
|
||||
`${description}TIP.keyup(keySurrogatePair), ${settingDescription}`,
|
||||
events[aTestPerSurrogateKeyPress ? 3 : 2],
|
||||
{
|
||||
type: "keyup",
|
||||
key: "\uD842\uDFB7",
|
||||
code: "KeyA",
|
||||
keyCode: KeyboardEvent.DOM_VK_A,
|
||||
charCode: 0,
|
||||
}
|
||||
);
|
||||
is(
|
||||
input.value,
|
||||
"\uD842\uDFB7",
|
||||
`${description}${settingDescription}, input.value should be the surrogate pair`
|
||||
);
|
||||
};
|
||||
|
||||
await test_press_and_release_surrogate_pair_key(true, true);
|
||||
await test_press_and_release_surrogate_pair_key(true, false);
|
||||
await test_press_and_release_surrogate_pair_key(false);
|
||||
|
||||
// If KEY_FORCE_PRINTABLE_KEY is specified, registered key names can be a printable key which inputs the specified value.
|
||||
input.value = "";
|
||||
var keyEnterPrintable = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN });
|
||||
@ -4730,7 +4851,7 @@ async function runTests()
|
||||
runCompositionTests();
|
||||
runCompositionWithKeyEventTests();
|
||||
runConsumingKeydownBeforeCompositionTests();
|
||||
runKeyTests();
|
||||
await runKeyTests();
|
||||
runErrorTests();
|
||||
runCommitCompositionTests();
|
||||
await runCallbackTests(false);
|
||||
|
@ -1448,7 +1448,21 @@ nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
|
||||
return NS_OK;
|
||||
}
|
||||
aKeyboardEvent->PreventDefault();
|
||||
nsAutoString str(aKeyboardEvent->mCharCode);
|
||||
// If we dispatch 2 keypress events for a surrogate pair and we set only
|
||||
// first `.key` value to the surrogate pair, the preceding one has it and the
|
||||
// other has empty string. In this case, we should handle only the first one
|
||||
// with the key value.
|
||||
if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() &&
|
||||
!StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() &&
|
||||
aKeyboardEvent->mKeyValue.IsEmpty() &&
|
||||
IS_SURROGATE(aKeyboardEvent->mCharCode)) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsAutoString str(aKeyboardEvent->mKeyValue);
|
||||
if (str.IsEmpty()) {
|
||||
str.Assign(static_cast<char16_t>(aKeyboardEvent->mCharCode));
|
||||
}
|
||||
// FYI: DIfferent from TextEditor, we can treat \r (CR) as-is in HTMLEditor.
|
||||
nsresult rv = OnInputText(str);
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
|
||||
return rv;
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "mozilla/mozalloc.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/PresShell.h"
|
||||
#include "mozilla/StaticPrefs_dom.h"
|
||||
#include "mozilla/StaticPrefs_editor.h"
|
||||
#include "mozilla/TextComposition.h"
|
||||
#include "mozilla/TextEvents.h"
|
||||
@ -317,18 +318,35 @@ nsresult TextEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
|
||||
// we don't PreventDefault() here or keybindings like control-x won't work
|
||||
return NS_OK;
|
||||
}
|
||||
// Our widget shouldn't set `\r` to `mCharCode`, but it may be synthesized
|
||||
aKeyboardEvent->PreventDefault();
|
||||
// If we dispatch 2 keypress events for a surrogate pair and we set only
|
||||
// first `.key` value to the surrogate pair, the preceding one has it and the
|
||||
// other has empty string. In this case, we should handle only the first one
|
||||
// with the key value.
|
||||
if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() &&
|
||||
!StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() &&
|
||||
aKeyboardEvent->mKeyValue.IsEmpty() &&
|
||||
IS_SURROGATE(aKeyboardEvent->mCharCode)) {
|
||||
return NS_OK;
|
||||
}
|
||||
// Our widget shouldn't set `\r` to `mKeyValue`, but it may be synthesized
|
||||
// keyboard event and its value may be `\r`. In such case, we should treat
|
||||
// it as `\n` for the backward compatibility because we stopped converting
|
||||
// `\r` and `\r\n` to `\n` at getting `HTMLInputElement.value` and
|
||||
// `HTMLTextAreaElement.value` for the performance (i.e., we don't need to
|
||||
// take care in `HTMLEditor`).
|
||||
char16_t charCode =
|
||||
static_cast<char16_t>(aKeyboardEvent->mCharCode) == nsCRT::CR
|
||||
? nsCRT::LF
|
||||
: static_cast<char16_t>(aKeyboardEvent->mCharCode);
|
||||
aKeyboardEvent->PreventDefault();
|
||||
nsAutoString str(charCode);
|
||||
nsAutoString str(aKeyboardEvent->mKeyValue);
|
||||
if (str.IsEmpty()) {
|
||||
MOZ_ASSERT(aKeyboardEvent->mCharCode <= 0xFFFF,
|
||||
"Non-BMP character needs special handling");
|
||||
str.Assign(aKeyboardEvent->mCharCode == nsCRT::CR
|
||||
? static_cast<char16_t>(nsCRT::LF)
|
||||
: static_cast<char16_t>(aKeyboardEvent->mCharCode));
|
||||
} else {
|
||||
MOZ_ASSERT(str.Find(u"\r\n"_ns) == kNotFound,
|
||||
"This assumes that typed text does not include CRLF");
|
||||
str.ReplaceChar('\r', '\n');
|
||||
}
|
||||
nsresult rv = OnInputText(str);
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
|
||||
return rv;
|
||||
|
@ -29,7 +29,7 @@ const kIsMac = navigator.platform.includes("Mac");
|
||||
const kIsWin = navigator.platform.includes("Win");
|
||||
const kIsLinux = navigator.platform.includes("Linux") || navigator.platform.includes("SunOS");
|
||||
|
||||
function runTests() {
|
||||
async function runTests() {
|
||||
document.execCommand("stylewithcss", false, "true");
|
||||
document.execCommand("defaultParagraphSeparator", false, "div");
|
||||
|
||||
@ -83,8 +83,13 @@ function runTests() {
|
||||
SpecialPowers.addSystemEventListener(window, "keypress", listener, false);
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function doTest(aElement, aDescription,
|
||||
aIsReadonly, aIsTabbable, aIsPlaintext) {
|
||||
async function doTest(
|
||||
aElement,
|
||||
aDescription,
|
||||
aIsReadonly,
|
||||
aIsTabbable,
|
||||
aIsPlaintext
|
||||
) {
|
||||
function reset(aText) {
|
||||
capturingPhase.fired = false;
|
||||
capturingPhase.prevented = false;
|
||||
@ -560,50 +565,193 @@ function runTests() {
|
||||
: "Mozilla <br>";
|
||||
})(),
|
||||
aDescription + "typed \"Mozilla \"");
|
||||
|
||||
// typing non-BMP character:
|
||||
async function test_typing_surrogate_pair(
|
||||
aTestPerSurrogateKeyPress,
|
||||
aTestIllFormedUTF16KeyValue = false
|
||||
) {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
|
||||
["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
|
||||
],
|
||||
});
|
||||
reset("");
|
||||
let events = [];
|
||||
function pushIntoEvents(aEvent) {
|
||||
events.push(aEvent);
|
||||
}
|
||||
function getEventData(aKeyboardEventOrInputEvent) {
|
||||
if (!aKeyboardEventOrInputEvent) {
|
||||
return "{}";
|
||||
}
|
||||
switch (aKeyboardEventOrInputEvent.type) {
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup":
|
||||
return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${
|
||||
aKeyboardEventOrInputEvent.key
|
||||
}", charCode=0x${
|
||||
aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase()
|
||||
} }`;
|
||||
default:
|
||||
return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${
|
||||
aKeyboardEventOrInputEvent.inputType
|
||||
}", data="${aKeyboardEventOrInputEvent.data}" }`;
|
||||
}
|
||||
}
|
||||
function getEventArrayData(aEvents) {
|
||||
if (!aEvents.length) {
|
||||
return "[]";
|
||||
}
|
||||
let result = "[\n";
|
||||
for (const e of aEvents) {
|
||||
result += ` ${getEventData(e)}\n`;
|
||||
}
|
||||
return result + "]";
|
||||
}
|
||||
aElement.addEventListener("keydown", pushIntoEvents);
|
||||
aElement.addEventListener("keypress", pushIntoEvents);
|
||||
aElement.addEventListener("keyup", pushIntoEvents);
|
||||
aElement.addEventListener("beforeinput", pushIntoEvents);
|
||||
aElement.addEventListener("input", pushIntoEvents);
|
||||
synthesizeKey("\uD842\uDFB7");
|
||||
aElement.removeEventListener("keydown", pushIntoEvents);
|
||||
aElement.removeEventListener("keypress", pushIntoEvents);
|
||||
aElement.removeEventListener("keyup", pushIntoEvents);
|
||||
aElement.removeEventListener("beforeinput", pushIntoEvents);
|
||||
aElement.removeEventListener("input", pushIntoEvents);
|
||||
const settingDescription =
|
||||
`aTestPerSurrogateKeyPress=${
|
||||
aTestPerSurrogateKeyPress
|
||||
}, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
|
||||
const allowIllFormedUTF16 =
|
||||
aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
|
||||
|
||||
check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly);
|
||||
is(
|
||||
aElement.textContent,
|
||||
!aIsReadonly ? "\uD842\uDFB7" : "",
|
||||
`${aDescription}, ${settingDescription}, The typed surrogate pair should've been inserted`
|
||||
);
|
||||
if (aIsReadonly) {
|
||||
is(
|
||||
getEventArrayData(events),
|
||||
getEventArrayData(
|
||||
aTestPerSurrogateKeyPress
|
||||
? (
|
||||
allowIllFormedUTF16
|
||||
? [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842", charCode: 0xD842 },
|
||||
{ type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
|
||||
{ type: "keypress", key: "", charCode: 0xDFB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
)
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
),
|
||||
`${aDescription}, ${
|
||||
settingDescription
|
||||
}, Typing a surrogate pair in readonly editor should not cause input events`
|
||||
);
|
||||
} else {
|
||||
is(
|
||||
getEventArrayData(events),
|
||||
getEventArrayData(
|
||||
aTestPerSurrogateKeyPress
|
||||
? (
|
||||
allowIllFormedUTF16
|
||||
? [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842", charCode: 0xD842 },
|
||||
{ type: "beforeinput", data: "\uD842", inputType: "insertText" },
|
||||
{ type: "input", data: "\uD842", inputType: "insertText" },
|
||||
{ type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
|
||||
{ type: "beforeinput", data: "\uDFB7", inputType: "insertText" },
|
||||
{ type: "input", data: "\uDFB7", inputType: "insertText" },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
|
||||
{ type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "keypress", key: "", charCode: 0xDFB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
)
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
|
||||
{ type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
),
|
||||
`${aDescription}, ${
|
||||
settingDescription
|
||||
}, Typing a surrogate pair in editor should cause input events`
|
||||
);
|
||||
}
|
||||
}
|
||||
await test_typing_surrogate_pair(true, true);
|
||||
await test_typing_surrogate_pair(true, false);
|
||||
await test_typing_surrogate_pair(false);
|
||||
}
|
||||
|
||||
doTest(htmlEditor, "contenteditable=\"true\"", false, true, false);
|
||||
await doTest(htmlEditor, "contenteditable=\"true\"", false, true, false);
|
||||
|
||||
const nsIEditor = SpecialPowers.Ci.nsIEditor;
|
||||
var editor = SpecialPowers.wrap(window).docShell.editor;
|
||||
var flags = editor.flags;
|
||||
// readonly
|
||||
editor.flags = flags | nsIEditor.eEditorReadonlyMask;
|
||||
doTest(htmlEditor, "readonly HTML editor", true, true, false);
|
||||
await doTest(htmlEditor, "readonly HTML editor", true, true, false);
|
||||
|
||||
// non-tabbable
|
||||
editor.flags = flags & ~(nsIEditor.eEditorAllowInteraction);
|
||||
doTest(htmlEditor, "non-tabbable HTML editor", false, false, false);
|
||||
await doTest(htmlEditor, "non-tabbable HTML editor", false, false, false);
|
||||
|
||||
// readonly and non-tabbable
|
||||
editor.flags =
|
||||
(flags | nsIEditor.eEditorReadonlyMask) &
|
||||
~(nsIEditor.eEditorAllowInteraction);
|
||||
doTest(htmlEditor, "readonly and non-tabbable HTML editor",
|
||||
await doTest(htmlEditor, "readonly and non-tabbable HTML editor",
|
||||
true, false, false);
|
||||
|
||||
// plaintext
|
||||
editor.flags = flags | nsIEditor.eEditorPlaintextMask;
|
||||
doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true);
|
||||
await doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true);
|
||||
|
||||
// plaintext and non-tabbable
|
||||
editor.flags = (flags | nsIEditor.eEditorPlaintextMask) &
|
||||
~(nsIEditor.eEditorAllowInteraction);
|
||||
doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode",
|
||||
await doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode",
|
||||
false, false, true);
|
||||
|
||||
|
||||
// readonly and plaintext
|
||||
editor.flags = flags | nsIEditor.eEditorPlaintextMask |
|
||||
nsIEditor.eEditorReadonlyMask;
|
||||
doTest(htmlEditor, "readonly HTML editor but plaintext mode",
|
||||
await doTest(htmlEditor, "readonly HTML editor but plaintext mode",
|
||||
true, true, true);
|
||||
|
||||
// readonly, plaintext and non-tabbable
|
||||
editor.flags = (flags | nsIEditor.eEditorPlaintextMask |
|
||||
nsIEditor.eEditorReadonlyMask) &
|
||||
~(nsIEditor.eEditorAllowInteraction);
|
||||
doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode",
|
||||
await doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode",
|
||||
true, false, true);
|
||||
|
||||
SpecialPowers.removeSystemEventListener(window, "keypress", listener, true);
|
||||
|
@ -31,7 +31,7 @@ const kIsMac = navigator.platform.includes("Mac");
|
||||
const kIsWin = navigator.platform.includes("Win");
|
||||
const kIsLinux = navigator.platform.includes("Linux");
|
||||
|
||||
function runTests() {
|
||||
async function runTests() {
|
||||
var fm = SpecialPowers.Services.focus;
|
||||
|
||||
var capturingPhase = { fired: false, prevented: false };
|
||||
@ -85,7 +85,7 @@ function runTests() {
|
||||
SpecialPowers.addSystemEventListener(parentElement, "keypress", listener,
|
||||
false);
|
||||
|
||||
function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) {
|
||||
async function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) {
|
||||
function reset(aText) {
|
||||
capturingPhase.fired = false;
|
||||
capturingPhase.prevented = false;
|
||||
@ -293,22 +293,169 @@ function runTests() {
|
||||
check(aDescription + "' '", true, true, !aIsReadonly);
|
||||
is(aElement.value, !aIsReadonly ? "Mozilla " : "",
|
||||
aDescription + "typed \"Mozilla \"");
|
||||
|
||||
// typing non-BMP character:
|
||||
async function test_typing_surrogate_pair(
|
||||
aTestPerSurrogateKeyPress,
|
||||
aTestIllFormedUTF16KeyValue = false
|
||||
) {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
|
||||
["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
|
||||
],
|
||||
});
|
||||
reset("");
|
||||
let events = [];
|
||||
function pushIntoEvents(aEvent) {
|
||||
events.push(aEvent);
|
||||
}
|
||||
function getEventData(aKeyboardEventOrInputEvent) {
|
||||
if (!aKeyboardEventOrInputEvent) {
|
||||
return "{}";
|
||||
}
|
||||
switch (aKeyboardEventOrInputEvent.type) {
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup":
|
||||
return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${
|
||||
aKeyboardEventOrInputEvent.key
|
||||
}", charCode=0x${
|
||||
aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase()
|
||||
} }`;
|
||||
default:
|
||||
return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${
|
||||
aKeyboardEventOrInputEvent.inputType
|
||||
}", data="${aKeyboardEventOrInputEvent.data}" }`;
|
||||
}
|
||||
}
|
||||
function getEventArrayData(aEvents) {
|
||||
if (!aEvents.length) {
|
||||
return "[]";
|
||||
}
|
||||
let result = "[\n";
|
||||
for (const e of aEvents) {
|
||||
result += ` ${getEventData(e)}\n`;
|
||||
}
|
||||
return result + "]";
|
||||
}
|
||||
aElement.addEventListener("keydown", pushIntoEvents);
|
||||
aElement.addEventListener("keypress", pushIntoEvents);
|
||||
aElement.addEventListener("keyup", pushIntoEvents);
|
||||
aElement.addEventListener("beforeinput", pushIntoEvents);
|
||||
aElement.addEventListener("input", pushIntoEvents);
|
||||
synthesizeKey("\uD842\uDFB7");
|
||||
aElement.removeEventListener("keydown", pushIntoEvents);
|
||||
aElement.removeEventListener("keypress", pushIntoEvents);
|
||||
aElement.removeEventListener("keyup", pushIntoEvents);
|
||||
aElement.removeEventListener("beforeinput", pushIntoEvents);
|
||||
aElement.removeEventListener("input", pushIntoEvents);
|
||||
const settingDescription =
|
||||
`aTestPerSurrogateKeyPress=${
|
||||
aTestPerSurrogateKeyPress
|
||||
}, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
|
||||
const allowIllFormedUTF16 =
|
||||
aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
|
||||
|
||||
check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly);
|
||||
is(
|
||||
aElement.value,
|
||||
!aIsReadonly ? "\uD842\uDFB7" : "",
|
||||
`${aDescription}, ${
|
||||
settingDescription
|
||||
}, The typed surrogate pair should've been inserted`
|
||||
);
|
||||
if (aIsReadonly) {
|
||||
is(
|
||||
getEventArrayData(events),
|
||||
getEventArrayData(
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
aTestPerSurrogateKeyPress
|
||||
? (
|
||||
allowIllFormedUTF16
|
||||
? [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842", charCode: 0xD842 },
|
||||
{ type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
|
||||
{ type: "keypress", key: "", charCode: 0xDFB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
)
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
),
|
||||
`${aDescription}, ${
|
||||
settingDescription
|
||||
}, Typing a surrogate pair in readonly editor should not cause input events`
|
||||
);
|
||||
} else {
|
||||
is(
|
||||
getEventArrayData(events),
|
||||
getEventArrayData(
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
aTestPerSurrogateKeyPress
|
||||
? (
|
||||
allowIllFormedUTF16
|
||||
? [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842", charCode: 0xD842 },
|
||||
{ type: "beforeinput", data: "\uD842", inputType: "insertText" },
|
||||
{ type: "input", data: "\uD842", inputType: "insertText" },
|
||||
{ type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
|
||||
{ type: "beforeinput", data: "\uDFB7", inputType: "insertText" },
|
||||
{ type: "input", data: "\uDFB7", inputType: "insertText" },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
|
||||
{ type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "keypress", key: "", charCode: 0xDFB7 },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
)
|
||||
: [
|
||||
{ type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
|
||||
{ type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
|
||||
{ type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
|
||||
{ type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
|
||||
]
|
||||
),
|
||||
`${aDescription}, ${
|
||||
settingDescription
|
||||
}, Typing a surrogate pair in editor should cause input events`
|
||||
);
|
||||
}
|
||||
}
|
||||
await test_typing_surrogate_pair(true, true);
|
||||
await test_typing_surrogate_pair(true, false);
|
||||
await test_typing_surrogate_pair(false);
|
||||
}
|
||||
|
||||
doTest(inputField, "<input type=\"text\">", true, false);
|
||||
await doTest(inputField, "<input type=\"text\">", true, false);
|
||||
|
||||
inputField.setAttribute("readonly", "readonly");
|
||||
doTest(inputField, "<input type=\"text\" readonly>", true, true);
|
||||
await doTest(inputField, "<input type=\"text\" readonly>", true, true);
|
||||
|
||||
doTest(passwordField, "<input type=\"password\">", true, false);
|
||||
await doTest(passwordField, "<input type=\"password\">", true, false);
|
||||
|
||||
passwordField.setAttribute("readonly", "readonly");
|
||||
doTest(passwordField, "<input type=\"password\" readonly>", true, true);
|
||||
await doTest(passwordField, "<input type=\"password\" readonly>", true, true);
|
||||
|
||||
doTest(textarea, "<textarea>", false, false);
|
||||
await doTest(textarea, "<textarea>", false, false);
|
||||
|
||||
textarea.setAttribute("readonly", "readonly");
|
||||
doTest(textarea, "<textarea readonly>", false, true);
|
||||
await doTest(textarea, "<textarea readonly>", false, true);
|
||||
|
||||
SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener,
|
||||
true);
|
||||
|
@ -2474,6 +2474,29 @@
|
||||
value: @IS_NOT_NIGHTLY_BUILD@
|
||||
mirror: always
|
||||
|
||||
# If this pref is set to true, typing a surrogate pair causes one `keypress`
|
||||
# event whose `charCode` stores the unicode code point over 0xFFFF. This is
|
||||
# compatible with Safari and Chrome in non-Windows platforms.
|
||||
# Otherwise, typing a surrogate pair causes two `keypress` events. This is
|
||||
# compatible with legacy web apps which does
|
||||
# `String.fromCharCode(event.charCode)`.
|
||||
- name: dom.event.keypress.dispatch_once_per_surrogate_pair
|
||||
type: bool
|
||||
value: false
|
||||
mirror: always
|
||||
|
||||
# This is meaningful only when `dispatch_once_per_surrogate_pair` is false.
|
||||
# If this pref is set to true, `.key` of the first `keypress` is set to the
|
||||
# high-surrogate and `.key` of the other is set to the low-surrogate.
|
||||
# Therefore, setting this exposing ill-formed UTF-16 string with `.key`.
|
||||
# (And also `InputEvent.data` if pressed in an editable element.)
|
||||
# Otherwise, `.key` of the first `keypress` is set to the surrogate pair, and
|
||||
# `.key` of the second `keypress` is set to the empty string.
|
||||
- name: dom.event.keypress.key.allow_lone_surrogate
|
||||
type: bool
|
||||
value: @IS_NOT_EARLY_BETA_OR_EARLIER@
|
||||
mirror: always
|
||||
|
||||
# Whether wheel event target's should be grouped. When enabled, all wheel
|
||||
# events that occur in a given wheel transaction have the same event target.
|
||||
- name: dom.event.wheel-event-groups.enabled
|
||||
|
@ -452,8 +452,25 @@ function sendChar(aChar, aWindow) {
|
||||
* key state on US keyboard layout.
|
||||
*/
|
||||
function sendString(aStr, aWindow) {
|
||||
for (var i = 0; i < aStr.length; ++i) {
|
||||
sendChar(aStr.charAt(i), aWindow);
|
||||
for (let i = 0; i < aStr.length; ++i) {
|
||||
// Do not split a surrogate pair to call synthesizeKey. Dispatching two
|
||||
// sets of keydown and keyup caused by two calls of synthesizeKey is not
|
||||
// good behavior. It could happen due to a bug, but a surrogate pair should
|
||||
// be introduced with one key press operation. Therefore, calling it with
|
||||
// a surrogate pair is the right thing.
|
||||
// Note that TextEventDispatcher will consider whether a surrogate pair
|
||||
// should cause one or two keypress events automatically. Therefore, we
|
||||
// don't need to check the related prefs here.
|
||||
if (
|
||||
(aStr.charCodeAt(i) & 0xfc00) == 0xd800 &&
|
||||
i + 1 < aStr.length &&
|
||||
(aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00
|
||||
) {
|
||||
sendChar(aStr.substring(i, i + 2));
|
||||
i++;
|
||||
} else {
|
||||
sendChar(aStr.charAt(i), aWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/StaticPrefs_dom.h"
|
||||
#include "nsCharTraits.h"
|
||||
#include "nsIFrame.h"
|
||||
#include "nsIWidget.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
@ -637,7 +638,60 @@ bool TextEventDispatcher::DispatchKeyboardEventInternal(
|
||||
// eKeyPress events are dispatched for every character.
|
||||
// So, each key value of eKeyPress events should be a character.
|
||||
if (ch) {
|
||||
keyEvent.mKeyValue.Assign(ch);
|
||||
if (!IS_SURROGATE(ch)) {
|
||||
keyEvent.mKeyValue.Assign(ch);
|
||||
} else {
|
||||
const bool isHighSurrogateFollowedByLowSurrogate =
|
||||
aIndexOfKeypress + 1 < keyEvent.mKeyValue.Length() &&
|
||||
NS_IS_HIGH_SURROGATE(ch) &&
|
||||
NS_IS_LOW_SURROGATE(keyEvent.mKeyValue[aIndexOfKeypress + 1]);
|
||||
const bool isLowSurrogateFollowingHighSurrogate =
|
||||
!isHighSurrogateFollowedByLowSurrogate && aIndexOfKeypress > 0 &&
|
||||
NS_IS_LOW_SURROGATE(ch) &&
|
||||
NS_IS_HIGH_SURROGATE(keyEvent.mKeyValue[aIndexOfKeypress - 1]);
|
||||
NS_WARNING_ASSERTION(isHighSurrogateFollowedByLowSurrogate ||
|
||||
isLowSurrogateFollowingHighSurrogate,
|
||||
"Lone surrogate input should not happen");
|
||||
if (StaticPrefs::
|
||||
dom_event_keypress_dispatch_once_per_surrogate_pair()) {
|
||||
if (isHighSurrogateFollowedByLowSurrogate) {
|
||||
keyEvent.mKeyValue.Assign(
|
||||
keyEvent.mKeyValue.BeginReading() + aIndexOfKeypress, 2);
|
||||
keyEvent.SetCharCode(
|
||||
SURROGATE_TO_UCS4(ch, keyEvent.mKeyValue[1]));
|
||||
} else if (isLowSurrogateFollowingHighSurrogate) {
|
||||
// Although not dispatching eKeyPress event (because it's already
|
||||
// dispatched for the low surrogate above), the caller should
|
||||
// treat that this dispatched eKeyPress event normally so that
|
||||
// return true here.
|
||||
return true;
|
||||
}
|
||||
// Do not expose ill-formed UTF-16 string because it's a
|
||||
// problematic for Rust-running-as-wasm for example.
|
||||
else {
|
||||
keyEvent.mKeyValue.Truncate();
|
||||
}
|
||||
} else if (!StaticPrefs::
|
||||
dom_event_keypress_key_allow_lone_surrogate()) {
|
||||
// If it's a high surrogate followed by a low surrogate, we should
|
||||
// expose the surrogate pair with .key value.
|
||||
if (isHighSurrogateFollowedByLowSurrogate) {
|
||||
keyEvent.mKeyValue.Assign(
|
||||
keyEvent.mKeyValue.BeginReading() + aIndexOfKeypress, 2);
|
||||
}
|
||||
// Do not expose low surrogate which should be handled by the
|
||||
// preceding keypress event. And also do not expose ill-formed
|
||||
// UTF-16 because it's a problematic for Rust-running-as-wasm for
|
||||
// example.
|
||||
else {
|
||||
keyEvent.mKeyValue.Truncate();
|
||||
}
|
||||
} else {
|
||||
// Here is a path for traditional behavior. We set `.key` to
|
||||
// high-surrogate and low-surrogate separately.
|
||||
keyEvent.mKeyValue.Assign(ch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keyEvent.mKeyValue.Truncate();
|
||||
}
|
||||
|
@ -115,6 +115,8 @@ skip-if = toolkit != "cocoa" # Cocoa widget test
|
||||
[test_standalone_native_menu.xhtml]
|
||||
skip-if = toolkit != "cocoa" # Cocoa widget test
|
||||
support-files = standalone_native_menu_window.xhtml
|
||||
[test_surrogate_pair_native_key_handling.xhtml]
|
||||
skip-if = toolkit != "windows" # Windows widget test
|
||||
[test_system_font_changes.xhtml]
|
||||
support-files = system_font_changes.xhtml
|
||||
run-if = toolkit == 'gtk' # Currently the test works on only gtk3
|
||||
|
178
widget/tests/test_surrogate_pair_native_key_handling.xhtml
Normal file
178
widget/tests/test_surrogate_pair_native_key_handling.xhtml
Normal file
@ -0,0 +1,178 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
|
||||
|
||||
<window id="window1" title="Test handling of native key input for surrogate pairs"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
|
||||
|
||||
<!-- test results are displayed in the html:body -->
|
||||
<body xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
|
||||
<script type="application/javascript"><![CDATA[
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.waitForFocus(async () => {
|
||||
function promiseSynthesizeNativeKey(
|
||||
aNativeKeyCode,
|
||||
aChars,
|
||||
) {
|
||||
return new Promise(resolve => {
|
||||
synthesizeNativeKey(
|
||||
KEYBOARD_LAYOUT_EN_US,
|
||||
aNativeKeyCode,
|
||||
{},
|
||||
aChars,
|
||||
aChars,
|
||||
resolve
|
||||
);
|
||||
});
|
||||
}
|
||||
function getEventData(aEvent) {
|
||||
return `{ type: "${aEvent.type}", key: "${aEvent.key}", code: "${
|
||||
aEvent.code
|
||||
}", keyCode: 0x${
|
||||
aEvent.keyCode.toString(16).toUpperCase()
|
||||
}, charCode: 0x${aEvent.charCode.toString(16).toUpperCase()} }`;
|
||||
}
|
||||
function getEventArrayData(aEvents) {
|
||||
if (!aEvents.length) {
|
||||
return "[]";
|
||||
}
|
||||
let result = "[\n";
|
||||
for (const e of aEvents) {
|
||||
result += ` ${getEventData(e)}\n`;
|
||||
}
|
||||
return result + "]";
|
||||
}
|
||||
let events = [];
|
||||
function onKey(aEvent) {
|
||||
events.push(aEvent);
|
||||
}
|
||||
window.addEventListener("keydown", onKey);
|
||||
window.addEventListener("keypress", onKey);
|
||||
window.addEventListener("keyup", onKey);
|
||||
|
||||
async function runTests(
|
||||
aTestPerSurrogateKeyPress,
|
||||
aTestIllFormedUTF16KeyValue = false
|
||||
) {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
|
||||
["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
|
||||
],
|
||||
});
|
||||
const settingDescription =
|
||||
`aTestPerSurrogateKeyPress=${
|
||||
aTestPerSurrogateKeyPress
|
||||
}, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
|
||||
const allowIllFormedUTF16 =
|
||||
aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
|
||||
|
||||
// If the keyboard layout has a key to introduce a surrogate pair,
|
||||
// one set of WM_KEYDOWN and WM_KEYUP are generated and it's translated
|
||||
// to two WM_CHARs.
|
||||
await (async function test_one_key_press() {
|
||||
events = [];
|
||||
await promiseSynthesizeNativeKey(WIN_VK_A, "\uD83E\uDD14");
|
||||
const keyCodeA = "A".charCodeAt(0);
|
||||
is(
|
||||
getEventArrayData(events),
|
||||
getEventArrayData(
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
aTestPerSurrogateKeyPress
|
||||
? (
|
||||
allowIllFormedUTF16
|
||||
? [
|
||||
{ type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
|
||||
{ type: "keypress", key: "\uD83E", code: "KeyA", keyCode: 0, charCode: 0xD83E },
|
||||
{ type: "keypress", key: "\uDD14", code: "KeyA", keyCode: 0, charCode: 0xDD14 },
|
||||
{ type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
|
||||
]
|
||||
: [
|
||||
{ type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
|
||||
{ type: "keypress", key: "\uD83E\uDD14", code: "KeyA", keyCode: 0, charCode: 0xD83E },
|
||||
{ type: "keypress", key: "", code: "KeyA", keyCode: 0, charCode: 0xDD14 },
|
||||
{ type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
|
||||
]
|
||||
)
|
||||
: [
|
||||
{ type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
|
||||
{ type: "keypress", key: "\uD83E\uDD14", code: "KeyA", keyCode: 0, charCode: 0x1F914 },
|
||||
{ type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
|
||||
]
|
||||
),
|
||||
`test_one_key_press(${
|
||||
settingDescription
|
||||
}): Typing surrogate pair should cause one set of keydown and keyup events with ${
|
||||
aTestPerSurrogateKeyPress ? "2 keypress events" : "a keypress event"
|
||||
}`
|
||||
);
|
||||
})();
|
||||
|
||||
// If a surrogate pair is sent with SendInput API, 2 sets of keyboard
|
||||
// events are generated. E.g., Emojis in the touch keyboard on Win10 or
|
||||
// later.
|
||||
await (async function test_send_input() {
|
||||
events = [];
|
||||
// Virtual keycode for the WM_KEYDOWN/WM_KEYUP is VK_PACKET.
|
||||
await promiseSynthesizeNativeKey(WIN_VK_PACKET, "\uD83E");
|
||||
await promiseSynthesizeNativeKey(WIN_VK_PACKET, "\uDD14");
|
||||
// WM_KEYDOWN, WM_CHAR and WM_KEYUP for the high surrogate input
|
||||
// shouldn't cause DOM events.
|
||||
is(
|
||||
getEventArrayData(events),
|
||||
getEventArrayData(
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
aTestPerSurrogateKeyPress
|
||||
? (
|
||||
allowIllFormedUTF16
|
||||
? [
|
||||
{ type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
|
||||
{ type: "keypress", key: "\uD83E", code: "", keyCode: 0, charCode: 0xD83E },
|
||||
{ type: "keypress", key: "\uDD14", code: "", keyCode: 0, charCode: 0xDD14 },
|
||||
{ type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
|
||||
]
|
||||
: [
|
||||
{ type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
|
||||
{ type: "keypress", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0xD83E },
|
||||
{ type: "keypress", key: "", code: "", keyCode: 0, charCode: 0xDD14 },
|
||||
{ type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
|
||||
]
|
||||
)
|
||||
: [
|
||||
{ type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
|
||||
{ type: "keypress", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0x1F914 },
|
||||
{ type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
|
||||
]
|
||||
),
|
||||
`test_send_input(${
|
||||
settingDescription
|
||||
}): Inputting surrogate pair should cause one set of keydown and keyup events ${
|
||||
aTestPerSurrogateKeyPress ? "2 keypress events" : "a keypress event"
|
||||
}`
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
await runTests(true, true);
|
||||
await runTests(true, false);
|
||||
await runTests(false);
|
||||
|
||||
window.removeEventListener("keydown", onKey);
|
||||
window.removeEventListener("keypress", onKey);
|
||||
window.removeEventListener("keyup", onKey);
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
]]></script>
|
||||
|
||||
</window>
|
@ -33,6 +33,7 @@
|
||||
#include "npapi.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <winnls.h>
|
||||
#include <winuser.h>
|
||||
#include <algorithm>
|
||||
|
||||
@ -1244,6 +1245,7 @@ NativeKey* NativeKey::sLatestInstance = nullptr;
|
||||
const MSG NativeKey::sEmptyMSG = {};
|
||||
MSG NativeKey::sLastKeyOrCharMSG = {};
|
||||
MSG NativeKey::sLastKeyMSG = {};
|
||||
char16_t NativeKey::sPendingHighSurrogate = 0;
|
||||
|
||||
NativeKey::NativeKey(nsWindow* aWidget, const MSG& aMessage,
|
||||
const ModifierKeyState& aModKeyState,
|
||||
@ -1413,11 +1415,14 @@ void NativeKey::InitIsSkippableForKeyOrChar(const MSG& aLastKeyMSG) {
|
||||
|
||||
void NativeKey::InitWithKeyOrChar() {
|
||||
MSG lastKeyMSG = sLastKeyMSG;
|
||||
char16_t pendingHighSurrogate = sPendingHighSurrogate;
|
||||
mScanCode = WinUtils::GetScanCode(mMsg.lParam);
|
||||
mIsExtended = WinUtils::IsExtendedScanCode(mMsg.lParam);
|
||||
switch (mMsg.message) {
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
sPendingHighSurrogate = 0;
|
||||
[[fallthrough]];
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYUP: {
|
||||
// Modify sLastKeyMSG now since retrieving following char messages may
|
||||
@ -1527,6 +1532,7 @@ void NativeKey::InitWithKeyOrChar() {
|
||||
case WM_CHAR:
|
||||
case WM_UNICHAR:
|
||||
case WM_SYSCHAR:
|
||||
sPendingHighSurrogate = 0;
|
||||
// If there is another instance and it is trying to remove a char message
|
||||
// from the queue, this message should be handled in the old instance.
|
||||
if (IsAnotherInstanceRemovingCharMessage()) {
|
||||
@ -1602,6 +1608,55 @@ void NativeKey::InitWithKeyOrChar() {
|
||||
Unused << NS_WARN_IF(charMsg.hwnd != mMsg.hwnd);
|
||||
mFollowingCharMsgs.AppendElement(charMsg);
|
||||
}
|
||||
if (mFollowingCharMsgs.Length() == 1) {
|
||||
// If we receive a keydown message for a high-surrogate, a low-surrogate
|
||||
// keydown message **will** and should follow it. We cannot translate the
|
||||
// following WM_KEYDOWN message for the low-surrogate right now since
|
||||
// it's not yet queued into the message queue yet. Therefore, we need to
|
||||
// wait next one to dispatch keypress event with setting its `.key` value
|
||||
// to a surrogate pair rather than setting it to a lone surrogate.
|
||||
// FYI: This may happen with typing a non-BMP character on the touch
|
||||
// keyboard on Windows 10 or later except when an IME is installed. (If
|
||||
// IME is installed, composition is used instead.)
|
||||
if (IS_HIGH_SURROGATE(mFollowingCharMsgs[0].wParam)) {
|
||||
if (pendingHighSurrogate) {
|
||||
MOZ_LOG(gKeyLog, LogLevel::Warning,
|
||||
("%p NativeKey::InitWithKeyOrChar(), there is pending "
|
||||
"high surrogate input, but received another high surrogate "
|
||||
"input. The previous one is discarded",
|
||||
this));
|
||||
}
|
||||
sPendingHighSurrogate = mFollowingCharMsgs[0].wParam;
|
||||
mFollowingCharMsgs.Clear();
|
||||
} else if (IS_LOW_SURROGATE(mFollowingCharMsgs[0].wParam)) {
|
||||
// If we stopped dispathing a keypress event for a preceding
|
||||
// high-surrogate, treat this keydown (for a low-surrogate) as
|
||||
// introducing both the high surrogate and the low surrogate.
|
||||
if (pendingHighSurrogate) {
|
||||
MSG charMsg = mFollowingCharMsgs[0];
|
||||
mFollowingCharMsgs[0].wParam = pendingHighSurrogate;
|
||||
mFollowingCharMsgs.AppendElement(std::move(charMsg));
|
||||
} else {
|
||||
MOZ_LOG(
|
||||
gKeyLog, LogLevel::Warning,
|
||||
("%p NativeKey::InitWithKeyOrChar(), there is no pending high "
|
||||
"surrogate input, but received lone low surrogate input",
|
||||
this));
|
||||
}
|
||||
} else {
|
||||
MOZ_LOG(gKeyLog, LogLevel::Warning,
|
||||
("%p NativeKey::InitWithKeyOrChar(), there is pending "
|
||||
"high surrogate input, but received non-surrogate input. "
|
||||
"The high surrogate input is discarded",
|
||||
this));
|
||||
}
|
||||
} else {
|
||||
MOZ_LOG(gKeyLog, LogLevel::Warning,
|
||||
("%p NativeKey::InitWithKeyOrChar(), there is pending "
|
||||
"high surrogate input, but received 2 or more character input. "
|
||||
"The high surrogate input is discarded",
|
||||
this));
|
||||
}
|
||||
}
|
||||
|
||||
keyboardLayout->InitNativeKey(*this);
|
||||
@ -2418,6 +2473,18 @@ bool NativeKey::HandleKeyDownMessage(bool* aEventDispatched) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sPendingHighSurrogate) {
|
||||
MOZ_LOG(gKeyLog, LogLevel::Info,
|
||||
("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown "
|
||||
"event because the key introduced only a high surrotate, so we "
|
||||
"should wait the following low surrogate input",
|
||||
this));
|
||||
if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) {
|
||||
RedirectedKeyDownMessageManager::Forget();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the widget has gone, we should do nothing.
|
||||
if (mWidget->Destroyed()) {
|
||||
MOZ_LOG(
|
||||
@ -2778,6 +2845,15 @@ bool NativeKey::HandleKeyUpMessage(bool* aEventDispatched) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sPendingHighSurrogate) {
|
||||
MOZ_LOG(gKeyLog, LogLevel::Info,
|
||||
("%p NativeKey::HandleKeyUpMessage(), doesn't dispatch keyup "
|
||||
"event because the key introduced only a high surrotate, so we "
|
||||
"should wait the following low surrogate input",
|
||||
this));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the widget has gone, we should do nothing.
|
||||
if (mWidget->Destroyed()) {
|
||||
MOZ_LOG(
|
||||
|
@ -787,6 +787,11 @@ class MOZ_STACK_CLASS NativeKey final {
|
||||
|
||||
static MSG sLastKeyMSG;
|
||||
|
||||
// Set to non-zero if we receive a WM_KEYDOWN message which introduces only
|
||||
// a high surrogate. Then, it'll be cleared when next keydown or char message
|
||||
// is received.
|
||||
static char16_t sPendingHighSurrogate;
|
||||
|
||||
static bool IsEmptyMSG(const MSG& aMSG) {
|
||||
return !memcmp(&aMSG, &sEmptyMSG, sizeof(MSG));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user