Bug 935501 - Get pointer events working for <input type=number>'s spin up/down buttons. r=smaug

This commit is contained in:
Jonathan Watt 2013-12-01 13:49:10 +00:00
parent 1eaff6defb
commit 5f5f7f6466
6 changed files with 288 additions and 4 deletions

View File

@ -21,6 +21,7 @@
#include "nsFocusManager.h"
#include "nsNumberControlFrame.h"
#include "nsPIDOMWindow.h"
#include "nsRepeatService.h"
#include "nsContentCID.h"
#include "nsIComponentManager.h"
#include "nsIDOMHTMLFormElement.h"
@ -1092,6 +1093,7 @@ HTMLInputElement::HTMLInputElement(already_AddRefed<nsINodeInfo> aNodeInfo,
, mHasRange(false)
, mIsDraggingRange(false)
, mProgressTimerIsActive(false)
, mNumberControlSpinnerIsSpinning(false)
{
// We are in a type=text so we now we currenty need a nsTextEditorState.
mInputData.mState = new nsTextEditorState(this);
@ -1114,6 +1116,9 @@ HTMLInputElement::~HTMLInputElement()
if (mFileList) {
mFileList->Disconnect();
}
if (mNumberControlSpinnerIsSpinning) {
StopNumberControlSpinnerSpin();
}
DestroyImageLoadingContent();
FreeData();
}
@ -2582,6 +2587,26 @@ HTMLInputElement::Notify(nsITimer* aTimer)
return NS_ERROR_INVALID_POINTER;
}
/* static */ void
HTMLInputElement::HandleNumberControlSpin(void* aData)
{
HTMLInputElement* input = static_cast<HTMLInputElement*>(aData);
NS_ASSERTION(input->mNumberControlSpinnerIsSpinning,
"Should have called nsRepeatService::Stop()");
nsNumberControlFrame* numberControlFrame =
do_QueryFrame(input->GetPrimaryFrame());
if (input->mType != NS_FORM_INPUT_NUMBER || !numberControlFrame) {
// Type has changed (and possibly our frame type hasn't been updated yet)
// or else we've lost our frame. Either way, stop the timer and don't do
// anything else.
input->StopNumberControlSpinnerSpin();
} else {
input->ApplyStep(input->mNumberControlSpinnerSpinsUp ? 1 : -1);
}
}
void
HTMLInputElement::MaybeDispatchProgressEvent(bool aFinalProgress)
{
@ -3274,6 +3299,42 @@ HTMLInputElement::PreHandleEvent(nsEventChainPreVisitor& aVisitor)
}
}
if (mType == NS_FORM_INPUT_NUMBER &&
aVisitor.mEvent->mFlags.mIsTrusted) {
if (mNumberControlSpinnerIsSpinning) {
// If the timer is running the user has depressed the mouse on one of the
// spin buttons. If the mouse exits the button we either want to reverse
// the direction of spin if it has moved over the other button, or else
// we want to end the spin. We do this here (rather than in
// PostHandleEvent) because we don't want to let content preventDefault()
// the end of the spin.
if (aVisitor.mEvent->message == NS_MOUSE_MOVE) {
// Be aggressive about stopping the spin:
bool stopSpin = true;
nsNumberControlFrame* numberControlFrame =
do_QueryFrame(GetPrimaryFrame());
if (numberControlFrame) {
switch (numberControlFrame->GetSpinButtonForPointerEvent(
aVisitor.mEvent->AsMouseEvent())) {
case nsNumberControlFrame::eSpinButtonUp:
mNumberControlSpinnerSpinsUp = true;
stopSpin = false;
break;
case nsNumberControlFrame::eSpinButtonDown:
mNumberControlSpinnerSpinsUp = false;
stopSpin = false;
break;
}
}
if (stopSpin) {
StopNumberControlSpinnerSpin();
}
} else if (aVisitor.mEvent->message == NS_MOUSE_BUTTON_UP) {
StopNumberControlSpinnerSpin();
}
}
}
nsresult rv = nsGenericHTMLFormElementWithState::PreHandleEvent(aVisitor);
// We do this after calling the base class' PreHandleEvent so that
@ -3397,6 +3458,34 @@ HTMLInputElement::SetValueOfRangeForUserEvent(Decimal aValue)
false);
}
void
HTMLInputElement::StartNumberControlSpinnerSpin()
{
MOZ_ASSERT(!mNumberControlSpinnerIsSpinning);
mNumberControlSpinnerIsSpinning = true;
nsRepeatService::GetInstance()->Start(HandleNumberControlSpin, this);
// Capture the mouse so that we can tell if the pointer moves from one
// spin button to the other, or to some other element:
nsIPresShell::SetCapturingContent(this, CAPTURE_IGNOREALLOWED);
}
void
HTMLInputElement::StopNumberControlSpinnerSpin()
{
if (mNumberControlSpinnerIsSpinning) {
if (nsIPresShell::GetCapturingContent() == this) {
nsIPresShell::SetCapturingContent(nullptr, 0); // cancel capture
}
nsRepeatService::GetInstance()->Stop(HandleNumberControlSpin, this);
mNumberControlSpinnerIsSpinning = false;
}
}
static bool
SelectTextFieldOnFocus()
{
@ -3484,9 +3573,12 @@ HTMLInputElement::PostHandleEvent(nsEventChainPostVisitor& aVisitor)
GetValueInternal(mFocusedValue);
}
if (mIsDraggingRange &&
aVisitor.mEvent->message == NS_BLUR_CONTENT) {
FinishRangeThumbDrag();
if (aVisitor.mEvent->message == NS_BLUR_CONTENT) {
if (mIsDraggingRange) {
FinishRangeThumbDrag();
} else if (mNumberControlSpinnerIsSpinning) {
StopNumberControlSpinnerSpin();
}
}
UpdateValidityUIBits(aVisitor.mEvent->message == NS_FOCUS_CONTENT);
@ -3837,7 +3929,43 @@ HTMLInputElement::PostHandleEvent(nsEventChainPostVisitor& aVisitor)
rv = NS_ERROR_FAILURE;
}
}
}
if (mType == NS_FORM_INPUT_NUMBER &&
aVisitor.mEvent->mFlags.mIsTrusted) {
if (mouseEvent->button == WidgetMouseEvent::eLeftButton &&
!(mouseEvent->IsShift() || mouseEvent->IsControl() ||
mouseEvent->IsAlt() || mouseEvent->IsMeta() ||
mouseEvent->IsAltGraph() || mouseEvent->IsFn() ||
mouseEvent->IsOS())) {
nsNumberControlFrame* numberControlFrame =
do_QueryFrame(GetPrimaryFrame());
if (numberControlFrame) {
if (aVisitor.mEvent->message == NS_MOUSE_BUTTON_DOWN) {
switch (numberControlFrame->GetSpinButtonForPointerEvent(
aVisitor.mEvent->AsMouseEvent())) {
case nsNumberControlFrame::eSpinButtonUp:
ApplyStep(1);
mNumberControlSpinnerSpinsUp = true;
StartNumberControlSpinnerSpin();
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
break;
case nsNumberControlFrame::eSpinButtonDown:
ApplyStep(-1);
mNumberControlSpinnerSpinsUp = false;
StartNumberControlSpinnerSpin();
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
break;
}
}
}
}
if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
// We didn't handle this to step up/down. Whatever this was, be
// aggressive about stopping the spin. (And don't set
// nsEventStatus_eConsumeNoDefault after doing so, since that
// might prevent, say, the context menu from opening.)
StopNumberControlSpinnerSpin();
}
}
break;
}

View File

@ -678,6 +678,15 @@ public:
HTMLInputElement* GetOwnerNumberControl();
void StartNumberControlSpinnerSpin();
void StopNumberControlSpinnerSpin();
/**
* The callback function used by the nsRepeatService that we use to spin the
* spinner for <input type=number>.
*/
static void HandleNumberControlSpin(void* aData);
bool MozIsTextField(bool aExcludePassword);
nsIEditor* GetEditor();
@ -1231,6 +1240,8 @@ protected:
bool mHasRange : 1;
bool mIsDraggingRange : 1;
bool mProgressTimerIsActive : 1;
bool mNumberControlSpinnerIsSpinning : 1;
bool mNumberControlSpinnerSpinsUp : 1;
private:
static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes,

View File

@ -24,6 +24,9 @@ support-files =
[test_input_file_picker.html]
[test_input_list_attribute.html]
[test_input_number_key_events.html]
# Spin buttons are hidden on Firefox OS and Firefox for Android:
skip-if = toolkit == "gonk" || os == 'android'
[test_input_number_mouse_events.html]
[test_input_number_rounding.html]
[test_input_range_attr_order.html]
[test_input_range_key_events.html]

View File

@ -0,0 +1,111 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=935501
-->
<head>
<title>Test mouse events for number</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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
input {
margin: 0 ! important;
border: 0 ! important;
padding: 0 ! important;
}
</style>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935501">Mozilla Bug 935501</a>
<p id="display"></p>
<div id="content">
<input id="input" type="number">
</div>
<pre id="test">
<script type="application/javascript">
/**
* Test for Bug 935501
* This test checks how the value of <input type=number> changes in response to
* various mouse events.
**/
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
test();
SimpleTest.finish();
});
function test() {
var input = document.getElementById("input");
var inputRect = input.getBoundingClientRect();
// Points over the input's spin-up and spin-down buttons (as offsets from the
// top-left of the input's bounding client rect):
const SPIN_UP_X = inputRect.width - 3;
const SPIN_UP_Y = 3;
const SPIN_DOWN_X = inputRect.width - 3;
const SPIN_DOWN_Y = inputRect.height - 3;
// Test click on spin-up button:
synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
is(input.value, 1, "Test step-up on mousedown on spin-up button");
synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
is(input.value, 1, "Test mouseup on spin-up button");
// Test click on spin-down button:
synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
is(input.value, 0, "Test step-down on mousedown on spin-down button");
synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
is(input.value, 0, "Test mouseup on spin-down button");
// Test that preventDefault() works:
function preventDefault(e) {
e.preventDefault();
}
input.value = 1;
input.addEventListener("mousedown", preventDefault, false);
synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, {});
is(input.value, 1, "Test that preventDefault() works for click on spin-up button");
synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, {});
is(input.value, 1, "Test that preventDefault() works for click on spin-down button");
input.removeEventListener("mousedown", preventDefault, false);
// XXX TODO
// Test spining when the mouse button is kept depressed on the spin-up
// button:
// XXX TODO
// Test spining when the mouse button is kept depressed on the spin-down
// button:
// XXX TODO
// Test spin direction reverses when the mouse button is depressod on the
// spin-up button, then moved over the spin-down button once the spin begins:
// XXX TODO
// Test spin direction reverses when the mouse button is depressod on the
// spin-down button, then moved over the spin-up button once the spin begins:
// XXX TODO
// Test that the spin is stopped when the mouse button is depressod on the
// spin-down button, then moved outside both buttons once the spin starts:
// XXX TODO
// Test that the spin is stopped when the mouse button is depressod on the
// spin-up button, then moved outside both buttons once the spin starts:
// XXX TODO
// Test that changing the input type in the middle of a spin cancels the spin:
// XXX TODO
// Check that we do not spin when a mousedown occurs outside the spin
// buttons and then the mouse is moved over the buttons:
}
</script>
</pre>
</body>
</html>

View File

@ -14,6 +14,7 @@
#include "nsGkAtoms.h"
#include "nsINodeInfo.h"
#include "nsINameSpaceManager.h"
#include "mozilla/BasicEvents.h"
#include "nsContentUtils.h"
#include "nsContentCreatorFunctions.h"
#include "nsContentList.h"
@ -315,6 +316,21 @@ nsNumberControlFrame::GetAnonTextControl()
return mTextField ? HTMLInputElement::FromContent(mTextField) : nullptr;
}
int32_t
nsNumberControlFrame::GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const
{
MOZ_ASSERT(aEvent->eventStructType == NS_MOUSE_EVENT,
"Unexpected event type");
if (aEvent->originalTarget == mSpinUp) {
return eSpinButtonUp;
}
if (aEvent->originalTarget == mSpinDown) {
return eSpinButtonDown;
}
return eSpinButtonNone;
}
void
nsNumberControlFrame::AppendAnonymousContentTo(nsBaseContentList& aElements,
uint32_t aFilter)

View File

@ -14,6 +14,7 @@
class nsPresContext;
namespace mozilla {
class WidgetGUIEvent;
namespace dom {
class HTMLInputElement;
}
@ -29,6 +30,7 @@ class nsNumberControlFrame MOZ_FINAL : public nsContainerFrame
NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
typedef mozilla::dom::HTMLInputElement HTMLInputElement;
typedef mozilla::WidgetGUIEvent WidgetGUIEvent;
nsNumberControlFrame(nsStyleContext* aContext);
@ -86,6 +88,19 @@ public:
HTMLInputElement* GetAnonTextControl();
enum SpinButtonEnum {
eSpinButtonNone,
eSpinButtonUp,
eSpinButtonDown
};
/**
* Returns one of the SpinButtonEnum values to depending on whether the
* pointer event is over the spin-up button, the spin-down button, or
* neither.
*/
int32_t GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const;
private:
nsresult MakeAnonymousElement(nsIContent** aResult,