mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-23 02:05:42 +00:00
Bug 935506 - Increase/decrease the value of <input type=number> in response to the up/down arrow keys. r=smaug
This commit is contained in:
parent
8ee979dd87
commit
b62a2fff8e
@ -3601,7 +3601,32 @@ HTMLInputElement::PostHandleEvent(nsEventChainPostVisitor& aVisitor)
|
||||
}
|
||||
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
if (nsEventStatus_eIgnore == aVisitor.mEventStatus) {
|
||||
WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent();
|
||||
if (mType == NS_FORM_INPUT_NUMBER &&
|
||||
keyEvent && keyEvent->message == NS_KEY_PRESS &&
|
||||
aVisitor.mEvent->mFlags.mIsTrusted &&
|
||||
(keyEvent->keyCode == NS_VK_UP || keyEvent->keyCode == NS_VK_DOWN) &&
|
||||
!(keyEvent->IsShift() || keyEvent->IsControl() ||
|
||||
keyEvent->IsAlt() || keyEvent->IsMeta() ||
|
||||
keyEvent->IsAltGraph() || keyEvent->IsFn() ||
|
||||
keyEvent->IsOS())) {
|
||||
// We handle the up/down arrow keys specially for <input type=number>.
|
||||
// On some platforms the editor for the nested text control will
|
||||
// process these keys to send the cursor to the start/end of the text
|
||||
// control and as a result aVisitor.mEventStatus will already have been
|
||||
// set to nsEventStatus_eConsumeNoDefault. However, we know that
|
||||
// whenever the up/down arrow keys cause the value of the number
|
||||
// control to change the string in the text control will change, and
|
||||
// the cursor will be moved to the end of the text control, overwriting
|
||||
// the editor's handling of up/down keypress events. For that reason we
|
||||
// just ignore aVisitor.mEventStatus here and go ahead and handle the
|
||||
// event to increase/decrease the value of the number control.
|
||||
// XXX we still need to allow script to call preventDefault() on the
|
||||
// event, but right now we can't tell the difference between the editor
|
||||
// on script doing that (bug 930374).
|
||||
ApplyStep(keyEvent->keyCode == NS_VK_UP ? 1 : -1);
|
||||
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
|
||||
} else if (nsEventStatus_eIgnore == aVisitor.mEventStatus) {
|
||||
switch (aVisitor.mEvent->message) {
|
||||
|
||||
case NS_FOCUS_CONTENT:
|
||||
|
@ -23,6 +23,7 @@ support-files =
|
||||
[test_input_event.html]
|
||||
[test_input_file_picker.html]
|
||||
[test_input_list_attribute.html]
|
||||
[test_input_number_key_events.html]
|
||||
[test_input_number_rounding.html]
|
||||
[test_input_range_attr_order.html]
|
||||
[test_input_range_key_events.html]
|
||||
@ -31,6 +32,7 @@ support-files =
|
||||
[test_input_sanitization.html]
|
||||
[test_input_textarea_set_value_no_scroll.html]
|
||||
[test_input_typing_sanitization.html]
|
||||
[test_input_untrusted_key_events.html]
|
||||
[test_input_url.html]
|
||||
[test_label_control_attribute.html]
|
||||
[test_max_attribute.html]
|
||||
|
@ -175,7 +175,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=722599
|
||||
number.value = "";
|
||||
number.focus();
|
||||
synthesizeKey("1", {});
|
||||
is(numberChange, 0, "Change event shouldn't be dispatched on number input element for keyboard input until it is looses focus");
|
||||
is(numberChange, 0, "Change event shouldn't be dispatched on number input element for keyboard input until it loses focus");
|
||||
number.blur();
|
||||
is(numberChange, 1, "Change event should be dispatched on number input element on blur");
|
||||
|
||||
@ -187,7 +187,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=722599
|
||||
is(rangeChange, 0, "Change event shouldn't be dispatched on range input element for key changes that don't change its value");
|
||||
range.focus();
|
||||
synthesizeKey("VK_HOME", {});
|
||||
is(rangeChange, 0, "Change event shouldn't be dispatched on range input element for key changes until it is looses focus");
|
||||
is(rangeChange, 0, "Change event shouldn't be dispatched on range input element for key changes until it loses focus");
|
||||
range.blur();
|
||||
is(rangeChange, 1, "Change event should be dispatched on range input element on blur");
|
||||
range.focus();
|
||||
|
@ -0,0 +1,186 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=935506
|
||||
-->
|
||||
<head>
|
||||
<title>Test key events for number control</title>
|
||||
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935506">Mozilla Bug 935506</a>
|
||||
<p id="display"></p>
|
||||
<div id="content">
|
||||
<input id="input" type="number">
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script type="application/javascript">
|
||||
|
||||
/**
|
||||
* Test for Bug 935506
|
||||
* This test checks how the value of <input type=number> changes in response to
|
||||
* key events while it is in various states.
|
||||
**/
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
// Turn off Spatial Navigation because it hijacks arrow keydown events:
|
||||
SpecialPowers.setBoolPref("snav.enabled", false);
|
||||
|
||||
SimpleTest.waitForFocus(function() {
|
||||
test();
|
||||
SimpleTest.finish();
|
||||
});
|
||||
|
||||
const defaultMinimum = "NaN";
|
||||
const defaultMaximum = "NaN";
|
||||
const defaultStep = 1;
|
||||
|
||||
// Helpers:
|
||||
// For the sake of simplicity, we do not currently support fractional value,
|
||||
// step, etc.
|
||||
|
||||
function getMinimum(element) {
|
||||
return Number(element.min || defaultMinimum);
|
||||
}
|
||||
|
||||
function getMaximum(element) {
|
||||
return Number(element.max || defaultMaximum);
|
||||
}
|
||||
|
||||
function getDefaultValue(element) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getValue(element) {
|
||||
return Number(element.value || getDefaultValue(element));
|
||||
}
|
||||
|
||||
function getStep(element) {
|
||||
if (element.step == "any") {
|
||||
return "any";
|
||||
}
|
||||
var step = Number(element.step || defaultStep);
|
||||
return step <= 0 ? defaultStep : step;
|
||||
}
|
||||
|
||||
function getStepBase(element) {
|
||||
return Number(element.getAttribute("min") || "NaN") ||
|
||||
Number(element.getAttribute("value") || "NaN") || 0;
|
||||
}
|
||||
|
||||
function floorModulo(x, y) {
|
||||
return (x - y * Math.floor(x / y));
|
||||
}
|
||||
|
||||
function expectedValueAfterStepUpOrDown(stepFactor, element) {
|
||||
var value = getValue(element);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
var step = getStep(element);
|
||||
if (step == "any") {
|
||||
return value;
|
||||
}
|
||||
|
||||
var minimum = getMinimum(element);
|
||||
var maximum = getMaximum(element);
|
||||
if (!isNaN(maximum)) {
|
||||
// "max - (max - stepBase) % step" is the nearest valid value to max.
|
||||
maximum = maximum - floorModulo(maximum - getStepBase(element), step);
|
||||
}
|
||||
|
||||
// Cases where we are clearly going in the wrong way.
|
||||
// We don't use ValidityState because we can be higher than the maximal
|
||||
// allowed value and still not suffer from range overflow in the case of
|
||||
// of the value specified in @max isn't in the step.
|
||||
if ((value <= minimum && stepFactor < 0) ||
|
||||
(value >= maximum && stepFactor > 0)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (element.validity.stepMismatch &&
|
||||
value != minimum && value != maximum) {
|
||||
if (stepFactor > 0) {
|
||||
value -= floorModulo(value - getStepBase(element), step);
|
||||
} else if (stepFactor < 0) {
|
||||
value -= floorModulo(value - getStepBase(element), step);
|
||||
value += step;
|
||||
}
|
||||
}
|
||||
|
||||
value += step * stepFactor;
|
||||
|
||||
// When stepUp() is called and the value is below minimum, we should clamp on
|
||||
// minimum unless stepUp() moves us higher than minimum.
|
||||
if (element.validity.rangeUnderflow && stepFactor > 0 &&
|
||||
value <= minimum) {
|
||||
value = minimum;
|
||||
} else if (element.validity.rangeOverflow && stepFactor < 0 &&
|
||||
value >= maximum) {
|
||||
value = maximum;
|
||||
} else if (stepFactor < 0 && !isNaN(minimum)) {
|
||||
value = Math.max(value, minimum);
|
||||
} else if (stepFactor > 0 && !isNaN(maximum)) {
|
||||
value = Math.min(value, maximum);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function expectedValAfterKeyEvent(key, element) {
|
||||
return expectedValueAfterStepUpOrDown(key == "VK_UP" ? 1 : -1, element);
|
||||
}
|
||||
|
||||
function test() {
|
||||
var elem = document.getElementById("input");
|
||||
elem.focus();
|
||||
|
||||
elem.min = -3;
|
||||
elem.max = 3;
|
||||
elem.step = 2;
|
||||
var defaultValue = 0;
|
||||
var oldVal, expectedVal;
|
||||
|
||||
for (key of ["VK_UP", "VK_DOWN"]) {
|
||||
// Start at middle:
|
||||
oldVal = elem.value = 0;
|
||||
expectedVal = expectedValAfterKeyEvent(key, elem);
|
||||
synthesizeKey(key, {});
|
||||
is(elem.value, expectedVal, "Test " + key + " for number control with value set to the midpoint (" + oldVal + ")");
|
||||
|
||||
// Same again:
|
||||
expectedVal = expectedValAfterKeyEvent(key, elem);
|
||||
synthesizeKey(key, {});
|
||||
is(elem.value, expectedVal, "Test repeat of " + key + " for number control");
|
||||
|
||||
// Start at maximum:
|
||||
oldVal = elem.value = elem.max;
|
||||
expectedVal = expectedValAfterKeyEvent(key, elem);
|
||||
synthesizeKey(key, {});
|
||||
is(elem.value, expectedVal, "Test " + key + " for number control with value set to the maximum (" + oldVal + ")");
|
||||
|
||||
// Same again:
|
||||
expectedVal = expectedValAfterKeyEvent(key, elem);
|
||||
synthesizeKey(key, {});
|
||||
is(elem.value, expectedVal, "Test repeat of " + key + " for number control");
|
||||
|
||||
// Start at minimum:
|
||||
oldVal = elem.value = elem.min;
|
||||
expectedVal = expectedValAfterKeyEvent(key, elem);
|
||||
synthesizeKey(key, {});
|
||||
is(elem.value, expectedVal, "Test " + key + " for number control with value set to the minimum (" + oldVal + ")");
|
||||
|
||||
// Same again:
|
||||
expectedVal = expectedValAfterKeyEvent(key, elem);
|
||||
synthesizeKey(key, {});
|
||||
is(elem.value, expectedVal, "Test repeat of " + key + " for number control");
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -69,6 +69,9 @@ function test() {
|
||||
* of them will fail when the widget will be implemented.
|
||||
*/
|
||||
|
||||
/* No other implementations implement this, so we don't either, for now.
|
||||
Seems like it might be nice though.
|
||||
|
||||
for (var i = 1; i < pgUpDnVals.length; ++i) {
|
||||
synthesizeKey("VK_PAGE_UP", {});
|
||||
todo_is(elem.value, pgUpDnVals[i], "Test VK_PAGE_UP");
|
||||
@ -82,10 +85,11 @@ function test() {
|
||||
todo_is(elem.value, pgUpDnVals[i], "Test VK_PAGE_DOWN");
|
||||
is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
|
||||
}
|
||||
*/
|
||||
|
||||
for (var i = 1; i < stepVals.length; ++i) {
|
||||
synthesizeKey("VK_UP", {});
|
||||
todo_is(elem.value, stepVals[i], "Test VK_UP");
|
||||
is(elem.value, stepVals[i], "Test VK_UP");
|
||||
is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
|
||||
}
|
||||
|
||||
@ -93,7 +97,7 @@ function test() {
|
||||
synthesizeKey("VK_DOWN", {});
|
||||
// TODO: this condition is there because the todo_is() below would pass otherwise.
|
||||
if (stepVals[i] == 0) { continue; }
|
||||
todo_is(elem.value, stepVals[i], "Test VK_DOWN");
|
||||
is(elem.value, stepVals[i], "Test VK_DOWN");
|
||||
is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,99 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test for untrusted DOM KeyboardEvent on input element</title>
|
||||
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display"></p>
|
||||
<div id="content">
|
||||
<input id="input">
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script type="application/javascript">
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.waitForFocus(runNextTest, window);
|
||||
|
||||
const kTests = Iterator([
|
||||
{ type: "text", value: "foo", key: "b", expectedNewValue: "foo" },
|
||||
{ type: "number", value: "123", key: "4", expectedNewValue: "123" },
|
||||
{ type: "number", value: "123", key: KeyEvent.DOM_VK_UP, expectedNewValue: "123" },
|
||||
{ type: "number", value: "123", key: KeyEvent.DOM_VK_DOWN, expectedNewValue: "123" },
|
||||
]);
|
||||
|
||||
function sendUntrustedKeyEvent(eventType, keyCode, target) {
|
||||
var evt = document.createEvent("KeyboardEvent");
|
||||
var canBubbleArg = true;
|
||||
var cancelableArg = true;
|
||||
var viewArg = document.defaultView;
|
||||
var ctrlKeyArg = false;
|
||||
var altKeyArg = false;
|
||||
var shiftKeyArg = false;
|
||||
var metaKeyArg = false;
|
||||
var keyCodeArg = keyCode;
|
||||
var charCodeArg = 0;
|
||||
evt.initKeyEvent(eventType, canBubbleArg, cancelableArg, viewArg,
|
||||
ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
|
||||
keyCodeArg, charCodeArg);
|
||||
target.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
var input = document.getElementById("input");
|
||||
|
||||
var gotEvents = {};
|
||||
|
||||
function handleEvent(event) {
|
||||
gotEvents[event.type] = true;
|
||||
}
|
||||
|
||||
input.addEventListener("keydown", handleEvent, false);
|
||||
input.addEventListener("keyup", handleEvent, false);
|
||||
input.addEventListener("keypress", handleEvent, false);
|
||||
|
||||
var previousTest = null;
|
||||
|
||||
function runNextTest() {
|
||||
if (previousTest) {
|
||||
var msg = "For <input " + "type=" + previousTest.type + ">, ";
|
||||
is(gotEvents.keydown, true, msg + "checking got keydown");
|
||||
is(gotEvents.keyup, true, msg + "checking got keyup");
|
||||
is(gotEvents.keypress, true, msg + "checking got keypress");
|
||||
is(input.value, previousTest.expectedNewValue, msg + "checking element " +
|
||||
" after being sent '" + previousTest.key + "' key events");
|
||||
}
|
||||
|
||||
// reset flags
|
||||
gotEvents.keydown = false;
|
||||
gotEvents.keyup = false;
|
||||
gotEvents.keypress = false;
|
||||
|
||||
try {
|
||||
var [index, test] = kTests.next();
|
||||
} catch(e) {
|
||||
if (e == StopIteration) {
|
||||
SimpleTest.finish();
|
||||
return; // We're all done
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
input.type = test.type;
|
||||
input.focus(); // make sure we still have focus after type change
|
||||
input.value = test.value;
|
||||
|
||||
sendUntrustedKeyEvent("keydown", test.key, input);
|
||||
sendUntrustedKeyEvent("keyup", test.key, input);
|
||||
sendUntrustedKeyEvent("keypress", test.key, input);
|
||||
|
||||
previousTest = test;
|
||||
|
||||
SimpleTest.executeSoon(runNextTest);
|
||||
};
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user