Bug 143038 Make users can scroll contents horizontally with vertical wheel operation with a modifier r?smaug draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 05 Oct 2017 01:12:35 +0900
changeset 682142 ff998ee7b6b1456fefb2872bbbe18ab51115d594
parent 681956 f78d5947333422ab09ec23e3dab0d48538c9d6ad
child 682643 fbc355e3c0425a495c78c3c028308dac26617283
push id85019
push usermasayuki@d-toybox.com
push dateWed, 18 Oct 2017 06:16:21 +0000
reviewerssmaug
bugs143038
milestone58.0a1
Bug 143038 Make users can scroll contents horizontally with vertical wheel operation with a modifier r?smaug This patch declares a new default action, "horizontal scroll", this scrolls content horizontally with deltaY of wheel events and ignores deltaX and deltaZ. This is used for default action with Shift key in default setting except on macOS. On macOS, legacy mouse's vertical wheel operation with Shift key causes native horizontal wheel event. Therefore, we don't need to use this new default action on macOS. Additionally, old default action with Shift key, navigating history, is moved to with Alt key. This makes same settings between macOS and the others. So, this is better for users who use macOS and another OS and web app developers who check wheel events only on macOS or other platform(s). For simpler implementation, default action handlers moves deltaY values to deltaX values temporarily *only* while they handle wheel events. This is performed by AutoWheelDeltaAdjuster and restored after handling it automatically. So, in other words, even if default action is "horizontal scroll", web apps receives wheel events whose deltaY is not zero but its content will be scrolled horizontally. This is same as Chromium, so, this behavior shouldn't cause any incompatible behavior with it. MozReview-Commit-ID: E4X3yZzLEAl
browser/app/profile/firefox.js
dom/events/EventStateManager.cpp
dom/events/EventStateManager.h
dom/events/WheelHandlingHelper.cpp
dom/events/WheelHandlingHelper.h
dom/events/moz.build
dom/events/test/window_wheel_default_action.html
gfx/layers/apz/public/IAPZCTreeManager.cpp
modules/libpref/init/all.js
widget/MouseEvents.h
widget/nsGUIEventIPC.h
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -606,35 +606,42 @@ pref("browser.gesture.twist.end", "cmd_g
 pref("browser.gesture.tap", "cmd_fullZoomReset");
 
 pref("browser.snapshots.limit", 0);
 
 // 0: Nothing happens
 // 1: Scrolling contents
 // 2: Go back or go forward, in your history
 // 3: Zoom in or out.
+// 4: Treat vertical wheel as horizontal scroll
 #ifdef XP_MACOSX
-// On OS X, if the wheel has one axis only, shift+wheel comes through as a
+// On macOS, if the wheel has one axis only, shift+wheel comes through as a
 // horizontal scroll event. Thus, we can't assign anything other than normal
 // scrolling to shift+wheel.
+pref("mousewheel.with_shift.action", 1);
 pref("mousewheel.with_alt.action", 2);
-pref("mousewheel.with_shift.action", 1);
 // On MacOS X, control+wheel is typically handled by system and we don't
 // receive the event.  So, command key which is the main modifier key for
 // acceleration is the best modifier for zoom-in/out.  However, we should keep
 // the control key setting for backward compatibility.
 pref("mousewheel.with_meta.action", 3); // command key on Mac
-// Disable control-/meta-modified horizontal mousewheel events, since
-// those are used on Mac as part of modified swipe gestures (e.g.
-// Left swipe+Cmd = go back in a new tab).
+// Disable control-/meta-modified horizontal wheel events, since those are
+// used on Mac as part of modified swipe gestures (e.g. Left swipe+Cmd is
+// "go back" in a new tab).
 pref("mousewheel.with_control.action.override_x", 0);
 pref("mousewheel.with_meta.action.override_x", 0);
 #else
-pref("mousewheel.with_alt.action", 1);
-pref("mousewheel.with_shift.action", 2);
+// On the other platforms (non-macOS), user may use legacy mouse which supports
+// only vertical wheel but want to scroll horizontally.  For such users, we
+// should provide horizontal scroll with shift+wheel (same as Chrome).
+// However, shift+wheel was used for navigating history.  For users who want
+// to keep using this feature, let's enable it with alt+wheel.  This is better
+// for consistency with macOS users.
+pref("mousewheel.with_shift.action", 4);
+pref("mousewheel.with_alt.action", 2);
 pref("mousewheel.with_meta.action", 1); // win key on Win, Super/Hyper on Linux
 #endif
 pref("mousewheel.with_control.action",3);
 pref("mousewheel.with_win.action", 1);
 
 pref("browser.xul.error_pages.enabled", true);
 pref("browser.xul.error_pages.expert_bad_cert", false);
 
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -3260,38 +3260,46 @@ EventStateManager::PostHandleEvent(nsPre
       MOZ_ASSERT(aEvent->IsTrusted());
 
       if (*aStatus == nsEventStatus_eConsumeNoDefault) {
         ScrollbarsForWheel::Inactivate();
         break;
       }
 
       WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent();
+      MOZ_ASSERT(wheelEvent);
+
+      // When APZ is enabled, the actual scroll animation might be handled by
+      // the compositor.
+      WheelPrefs::Action action =
+        wheelEvent->mFlags.mHandledByAPZ ?
+          WheelPrefs::ACTION_NONE :
+          WheelPrefs::GetInstance()->ComputeActionFor(wheelEvent);
+
+      // Make the wheel event a horizontal scroll event.  I.e., deltaY values
+      // are set to deltaX and deltaY and deltaZ values are set to 0.
+      // When AutoWheelDeltaAdjuster instance is destroyed, the delta values
+      // are restored and make overflow deltaX becomes 0.
+      AutoWheelDeltaAdjuster adjuster(*wheelEvent);
 
       // Check if the frame to scroll before checking the default action
       // because if the scroll target is a plugin, the default action should be
       // chosen by the plugin rather than by our prefs.
       nsIFrame* frameToScroll =
         ComputeScrollTarget(mCurrentTarget, wheelEvent,
                             COMPUTE_DEFAULT_ACTION_TARGET);
       nsPluginFrame* pluginFrame = do_QueryFrame(frameToScroll);
-
-      // When APZ is enabled, the actual scroll animation might be handled by
-      // the compositor.
-      WheelPrefs::Action action;
       if (pluginFrame) {
         MOZ_ASSERT(pluginFrame->WantsToHandleWheelEventAsDefaultAction());
         action = WheelPrefs::ACTION_SEND_TO_PLUGIN;
-      } else if (wheelEvent->mFlags.mHandledByAPZ) {
-        action = WheelPrefs::ACTION_NONE;
-      } else {
-        action = WheelPrefs::GetInstance()->ComputeActionFor(wheelEvent);
       }
+
       switch (action) {
-        case WheelPrefs::ACTION_SCROLL: {
+        case WheelPrefs::ACTION_SCROLL:
+        case WheelPrefs::ACTION_HORIZONTAL_SCROLL: {
           // For scrolling of default action, we should honor the mouse wheel
           // transaction.
 
           ScrollbarsForWheel::PrepareToScrollText(this, mCurrentTarget, wheelEvent);
 
           if (aEvent->mMessage != eWheel ||
               (!wheelEvent->mDeltaX && !wheelEvent->mDeltaY)) {
             break;
@@ -5652,21 +5660,20 @@ EventStateManager::WheelPrefs::~WheelPre
 {
   Preferences::UnregisterPrefixCallback(OnPrefChanged, "mousewheel.");
 }
 
 void
 EventStateManager::WheelPrefs::Reset()
 {
   memset(mInit, 0, sizeof(mInit));
-
 }
 
 EventStateManager::WheelPrefs::Index
-EventStateManager::WheelPrefs::GetIndexFor(WidgetWheelEvent* aEvent)
+EventStateManager::WheelPrefs::GetIndexFor(const WidgetWheelEvent* aEvent)
 {
   if (!aEvent) {
     return INDEX_DEFAULT;
   }
 
   Modifiers modifiers =
     (aEvent->mModifiers & (MODIFIER_ALT |
                            MODIFIER_CONTROL |
@@ -5754,47 +5761,72 @@ EventStateManager::WheelPrefs::Init(Even
     NS_WARNING("Unsupported action pref value, replaced with 'Scroll'.");
     action = ACTION_SCROLL;
   }
   mActions[aIndex] = static_cast<Action>(action);
 
   // Compute action values overridden by .override_x pref.
   // At present, override is possible only for the x-direction
   // because this pref is introduced mainly for tilt wheels.
+  // Note that ACTION_HORIZONTAL_SCROLL isn't a valid value for this pref
+  // because it affects only to deltaY.
   prefNameAction.AppendLiteral(".override_x");
   int32_t actionOverrideX = Preferences::GetInt(prefNameAction.get(), -1);
-  if (actionOverrideX < -1 || actionOverrideX > int32_t(ACTION_LAST)) {
+  if (actionOverrideX < -1 || actionOverrideX > int32_t(ACTION_LAST) ||
+      actionOverrideX == ACTION_HORIZONTAL_SCROLL) {
     NS_WARNING("Unsupported action override pref value, didn't override.");
     actionOverrideX = -1;
   }
   mOverriddenActionsX[aIndex] = (actionOverrideX == -1)
                               ? static_cast<Action>(action)
                               : static_cast<Action>(actionOverrideX);
 }
 
 void
+EventStateManager::WheelPrefs::GetMultiplierForDeltaXAndY(
+                                 const WidgetWheelEvent* aEvent,
+                                 Index aIndex,
+                                 double* aMultiplierForDeltaX,
+                                 double* aMultiplierForDeltaY)
+{
+  // If the event should be treated as horizontal wheel operation, deltaY
+  // should be multiplied by mMultiplierY, however, it might be moved to
+  // deltaX for handling default action.  In such case, we need to treat
+  // mMultiplierX and mMultiplierY as swapped.
+  *aMultiplierForDeltaX = mMultiplierX[aIndex];
+  *aMultiplierForDeltaY = mMultiplierY[aIndex];
+  if (aEvent->mDeltaValuesAdjustedForDefaultHandler &&
+      ComputeActionFor(aEvent) == ACTION_HORIZONTAL_SCROLL) {
+    std::swap(*aMultiplierForDeltaX, *aMultiplierForDeltaY);
+  }
+}
+
+void
 EventStateManager::WheelPrefs::ApplyUserPrefsToDelta(WidgetWheelEvent* aEvent)
 {
   if (aEvent->mCustomizedByUserPrefs) {
     return;
   }
 
   Index index = GetIndexFor(aEvent);
   Init(index);
 
-  aEvent->mDeltaX *= mMultiplierX[index];
-  aEvent->mDeltaY *= mMultiplierY[index];
+  double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0;
+  GetMultiplierForDeltaXAndY(aEvent, index,
+                             &multiplierForDeltaX, &multiplierForDeltaY);
+  aEvent->mDeltaX *= multiplierForDeltaX;
+  aEvent->mDeltaY *= multiplierForDeltaY;
   aEvent->mDeltaZ *= mMultiplierZ[index];
 
   // If the multiplier is 1.0 or -1.0, i.e., it doesn't change the absolute
   // value, we should use lineOrPageDelta values which were set by widget.
   // Otherwise, we need to compute them from accumulated delta values.
   if (!NeedToComputeLineOrPageDelta(aEvent)) {
-    aEvent->mLineOrPageDeltaX *= static_cast<int32_t>(mMultiplierX[index]);
-    aEvent->mLineOrPageDeltaY *= static_cast<int32_t>(mMultiplierY[index]);
+    aEvent->mLineOrPageDeltaX *= static_cast<int32_t>(multiplierForDeltaX);
+    aEvent->mLineOrPageDeltaY *= static_cast<int32_t>(multiplierForDeltaY);
   } else {
     aEvent->mLineOrPageDeltaX = 0;
     aEvent->mLineOrPageDeltaY = 0;
   }
 
   aEvent->mCustomizedByUserPrefs =
     ((mMultiplierX[index] != 1.0) || (mMultiplierY[index] != 1.0) ||
      (mMultiplierZ[index] != 1.0));
@@ -5808,111 +5840,142 @@ EventStateManager::WheelPrefs::CancelApp
   Init(index);
 
   // XXX If the multiplier pref value is negative, the scroll direction was
   //     changed and caused to scroll different direction.  In such case,
   //     this method reverts the sign of overflowDelta.  Does it make widget
   //     happy?  Although, widget can know the pref applied delta values by
   //     referrencing the deltaX and deltaY of the event.
 
-  if (mMultiplierX[index]) {
-    aEvent->mOverflowDeltaX /= mMultiplierX[index];
-  }
-  if (mMultiplierY[index]) {
-    aEvent->mOverflowDeltaY /= mMultiplierY[index];
+  double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0;
+  GetMultiplierForDeltaXAndY(aEvent, index,
+                             &multiplierForDeltaX, &multiplierForDeltaY);
+  if (multiplierForDeltaX) {
+    aEvent->mOverflowDeltaX /= multiplierForDeltaX;
+  }
+  if (multiplierForDeltaY) {
+    aEvent->mOverflowDeltaY /= multiplierForDeltaY;
   }
 }
 
 EventStateManager::WheelPrefs::Action
-EventStateManager::WheelPrefs::ComputeActionFor(WidgetWheelEvent* aEvent)
+EventStateManager::WheelPrefs::ComputeActionFor(const WidgetWheelEvent* aEvent)
 {
   Index index = GetIndexFor(aEvent);
   Init(index);
 
   bool deltaXPreferred =
     (Abs(aEvent->mDeltaX) > Abs(aEvent->mDeltaY) &&
      Abs(aEvent->mDeltaX) > Abs(aEvent->mDeltaZ));
   Action* actions = deltaXPreferred ? mOverriddenActionsX : mActions;
-  if (actions[index] == ACTION_NONE || actions[index] == ACTION_SCROLL) {
+  if (actions[index] == ACTION_NONE ||
+      actions[index] == ACTION_SCROLL ||
+      actions[index] == ACTION_HORIZONTAL_SCROLL) {
     return actions[index];
   }
 
   // Momentum events shouldn't run special actions.
   if (aEvent->mIsMomentum) {
     // Use the default action.  Note that user might kill the wheel scrolling.
     Init(INDEX_DEFAULT);
-    return (actions[INDEX_DEFAULT] == ACTION_SCROLL) ? ACTION_SCROLL :
-                                                       ACTION_NONE;
+    if (actions[INDEX_DEFAULT] == ACTION_SCROLL ||
+        actions[INDEX_DEFAULT] == ACTION_HORIZONTAL_SCROLL) {
+      return actions[INDEX_DEFAULT];
+    }
+    return ACTION_NONE;
   }
 
   return actions[index];
 }
 
 bool
 EventStateManager::WheelPrefs::NeedToComputeLineOrPageDelta(
-                                 WidgetWheelEvent* aEvent)
+                                 const WidgetWheelEvent* aEvent)
 {
   Index index = GetIndexFor(aEvent);
   Init(index);
 
   return (mMultiplierX[index] != 1.0 && mMultiplierX[index] != -1.0) ||
          (mMultiplierY[index] != 1.0 && mMultiplierY[index] != -1.0);
 }
 
 void
-EventStateManager::WheelPrefs::GetUserPrefsForEvent(WidgetWheelEvent* aEvent,
-                                                    double* aOutMultiplierX,
-                                                    double* aOutMultiplierY)
+EventStateManager::WheelPrefs::GetUserPrefsForEvent(
+                                 const WidgetWheelEvent* aEvent,
+                                 double* aOutMultiplierX,
+                                 double* aOutMultiplierY)
 {
   Index index = GetIndexFor(aEvent);
   Init(index);
 
-  *aOutMultiplierX = mMultiplierX[index];
-  *aOutMultiplierY = mMultiplierY[index];
+  double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0;
+  GetMultiplierForDeltaXAndY(aEvent, index,
+                             &multiplierForDeltaX, &multiplierForDeltaY);
+  *aOutMultiplierX = multiplierForDeltaX;
+  *aOutMultiplierY = multiplierForDeltaY;
 }
 
 // static
 bool
 EventStateManager::WheelPrefs::WheelEventsEnabledOnPlugins()
 {
   if (!sInstance) {
     GetInstance(); // initializing sWheelEventsEnabledOnPlugins
   }
   return sWheelEventsEnabledOnPlugins;
 }
 
+// static
 bool
-EventStateManager::WheelEventIsScrollAction(WidgetWheelEvent* aEvent)
+EventStateManager::WheelEventIsScrollAction(const WidgetWheelEvent* aEvent)
 {
-  return aEvent->mMessage == eWheel &&
-         WheelPrefs::GetInstance()->ComputeActionFor(aEvent) == WheelPrefs::ACTION_SCROLL;
+  if (aEvent->mMessage != eWheel) {
+    return false;
+  }
+  WheelPrefs::Action action =
+    WheelPrefs::GetInstance()->ComputeActionFor(aEvent);
+  return action == WheelPrefs::ACTION_SCROLL ||
+         action == WheelPrefs::ACTION_HORIZONTAL_SCROLL;
+}
+
+// static
+bool
+EventStateManager::WheelEventIsHorizontalScrollAction(
+                     const WidgetWheelEvent* aEvent)
+{
+  if (aEvent->mMessage != eWheel) {
+    return false;
+  }
+  WheelPrefs::Action action =
+    WheelPrefs::GetInstance()->ComputeActionFor(aEvent);
+  return action == WheelPrefs::ACTION_HORIZONTAL_SCROLL;
 }
 
 void
-EventStateManager::GetUserPrefsForWheelEvent(WidgetWheelEvent* aEvent,
+EventStateManager::GetUserPrefsForWheelEvent(const WidgetWheelEvent* aEvent,
                                              double* aOutMultiplierX,
                                              double* aOutMultiplierY)
 {
   WheelPrefs::GetInstance()->GetUserPrefsForEvent(
     aEvent, aOutMultiplierX, aOutMultiplierY);
 }
 
 bool
 EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedX(
-                                 WidgetWheelEvent* aEvent)
+                                 const WidgetWheelEvent* aEvent)
 {
   Index index = GetIndexFor(aEvent);
   Init(index);
   return Abs(mMultiplierX[index]) >=
            MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
 }
 
 bool
 EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedY(
-                                 WidgetWheelEvent* aEvent)
+                                 const WidgetWheelEvent* aEvent)
 {
   Index index = GetIndexFor(aEvent);
   Init(index);
   return Abs(mMultiplierY[index]) >=
            MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
 }
 
 /******************************************************************/
--- a/dom/events/EventStateManager.h
+++ b/dom/events/EventStateManager.h
@@ -299,20 +299,24 @@ public:
                                nsIContent* aContent);
 
   // Sets the full-screen event state on aElement to aIsFullScreen.
   static void SetFullScreenState(dom::Element* aElement, bool aIsFullScreen);
 
   static bool IsRemoteTarget(nsIContent* aTarget);
 
   // Returns true if the given WidgetWheelEvent will resolve to a scroll action.
-  static bool WheelEventIsScrollAction(WidgetWheelEvent* aEvent);
+  static bool WheelEventIsScrollAction(const WidgetWheelEvent* aEvent);
+
+  // Returns true if the given WidgetWheelEvent will resolve to a horizontal
+  // scroll action but it's a vertical wheel operation.
+  static bool WheelEventIsHorizontalScrollAction(const WidgetWheelEvent* aEvet);
 
   // Returns user-set multipliers for a wheel event.
-  static void GetUserPrefsForWheelEvent(WidgetWheelEvent* aEvent,
+  static void GetUserPrefsForWheelEvent(const WidgetWheelEvent* aEvent,
                                         double* aOutMultiplierX,
                                         double* aOutMultiplierY);
 
   // Returns whether or not a frame can be vertically scrolled with a mouse
   // wheel (as opposed to, say, a selection or touch scroll).
   static bool CanVerticallyScrollFrameWithWheel(nsIFrame* aFrame);
 
   // Holds the point in screen coords that a mouse event was dispatched to,
@@ -533,17 +537,17 @@ protected:
      * user prefs.
      */
     void ApplyUserPrefsToDelta(WidgetWheelEvent* aEvent);
 
     /**
      * Returns whether or not ApplyUserPrefsToDelta() would change the delta
      * values of an event.
      */
-    void GetUserPrefsForEvent(WidgetWheelEvent* aEvent,
+    void GetUserPrefsForEvent(const WidgetWheelEvent* aEvent,
                               double* aOutMultiplierX,
                               double* aOutMultiplierY);
 
     /**
      * If ApplyUserPrefsToDelta() changed the delta values with customized
      * prefs, the overflowDelta values would be inflated.
      * CancelApplyingUserPrefsFromOverflowDelta() cancels the inflation.
      */
@@ -553,35 +557,36 @@ protected:
      * Computes the default action for the aEvent with the prefs.
      */
     enum Action : uint8_t
     {
       ACTION_NONE = 0,
       ACTION_SCROLL,
       ACTION_HISTORY,
       ACTION_ZOOM,
-      ACTION_LAST = ACTION_ZOOM,
+      ACTION_HORIZONTAL_SCROLL,
+      ACTION_LAST = ACTION_HORIZONTAL_SCROLL,
       // Following actions are used only by internal processing.  So, cannot
       // specified by prefs.
-      ACTION_SEND_TO_PLUGIN
+      ACTION_SEND_TO_PLUGIN,
     };
-    Action ComputeActionFor(WidgetWheelEvent* aEvent);
+    Action ComputeActionFor(const WidgetWheelEvent* aEvent);
 
     /**
      * NeedToComputeLineOrPageDelta() returns if the aEvent needs to be
      * computed the lineOrPageDelta values.
      */
-    bool NeedToComputeLineOrPageDelta(WidgetWheelEvent* aEvent);
+    bool NeedToComputeLineOrPageDelta(const WidgetWheelEvent* aEvent);
 
     /**
      * IsOverOnePageScrollAllowed*() checks whether wheel scroll amount should
      * be rounded down to the page width/height (false) or not (true).
      */
-    bool IsOverOnePageScrollAllowedX(WidgetWheelEvent* aEvent);
-    bool IsOverOnePageScrollAllowedY(WidgetWheelEvent* aEvent);
+    bool IsOverOnePageScrollAllowedX(const WidgetWheelEvent* aEvent);
+    bool IsOverOnePageScrollAllowedY(const WidgetWheelEvent* aEvent);
 
     /**
      * WheelEventsEnabledOnPlugins() returns true if user wants to use mouse
      * wheel on plugins.
      */
     static bool WheelEventsEnabledOnPlugins();
 
   private:
@@ -604,32 +609,51 @@ protected:
     /**
      * GetIndexFor() returns the index of the members which should be used for
      * the aEvent.  When only one modifier key of MODIFIER_ALT,
      * MODIFIER_CONTROL, MODIFIER_META, MODIFIER_SHIFT or MODIFIER_OS is
      * pressed, returns the index for the modifier.  Otherwise, this return the
      * default index which is used at either no modifier key is pressed or
      * two or modifier keys are pressed.
      */
-    Index GetIndexFor(WidgetWheelEvent* aEvent);
+    Index GetIndexFor(const WidgetWheelEvent* aEvent);
 
     /**
      * GetPrefNameBase() returns the base pref name for aEvent.
      * It's decided by GetModifierForPref() which modifier should be used for
      * the aEvent.
      *
      * @param aBasePrefName The result, must be "mousewheel.with_*." or
      *                      "mousewheel.default.".
      */
     void GetBasePrefName(Index aIndex, nsACString& aBasePrefName);
 
     void Init(Index aIndex);
 
     void Reset();
 
+    /**
+     * Retrieve multiplier for aEvent->mDeltaX and aEvent->mDeltaY.
+     * If the default action is ACTION_HORIZONTAL_SCROLL and the delta values
+     * are adjusted by AutoWheelDeltaAdjuster(), this treats mMultiplierX as
+     * multiplier for deltaY and mMultiplierY as multiplier for deltaY.
+     *
+     * @param aEvent    The event which is being handled.
+     * @param aIndex    The index of mMultiplierX and mMultiplierY.
+     *                  Should be result of GetIndexFor(aEvent).
+     * @param aMultiplierForDeltaX      Will be set to multiplier for
+     *                                  aEvent->mDeltaX.
+     * @param aMultiplierForDeltaY      Will be set to multiplier for
+     *                                  aEvent->mDeltaY.
+     */
+    void GetMultiplierForDeltaXAndY(const WidgetWheelEvent* aEvent,
+                                    Index aIndex,
+                                    double* aMultiplierForDeltaX,
+                                    double* aMultiplierForDeltaY);
+
     bool mInit[COUNT_OF_MULTIPLIERS];
     double mMultiplierX[COUNT_OF_MULTIPLIERS];
     double mMultiplierY[COUNT_OF_MULTIPLIERS];
     double mMultiplierZ[COUNT_OF_MULTIPLIERS];
     Action mActions[COUNT_OF_MULTIPLIERS];
     /**
      * action values overridden by .override_x pref.
      * If an .override_x value is -1, same as the
--- a/dom/events/WheelHandlingHelper.cpp
+++ b/dom/events/WheelHandlingHelper.cpp
@@ -524,18 +524,19 @@ ScrollbarsForWheel::DeactivateAllTempora
         scrollbarMediator->ScrollbarActivityStopped();
       }
       *scrollTarget = nullptr;
     }
   }
 }
 
 /******************************************************************/
-/* mozilla::WheelTransaction                                      */
+/* mozilla::WheelTransaction::Prefs                               */
 /******************************************************************/
+
 int32_t WheelTransaction::Prefs::sMouseWheelAccelerationStart = -1;
 int32_t WheelTransaction::Prefs::sMouseWheelAccelerationFactor = -1;
 uint32_t WheelTransaction::Prefs::sMouseWheelTransactionTimeout = 1500;
 uint32_t WheelTransaction::Prefs::sMouseWheelTransactionIgnoreMoveDelay = 100;
 bool WheelTransaction::Prefs::sTestMouseScroll = false;
 
 /* static */ void
 WheelTransaction::Prefs::InitializeStatics()
@@ -550,9 +551,52 @@ WheelTransaction::Prefs::InitializeStati
                                  "mousewheel.transaction.timeout", 1500);
     Preferences::AddUintVarCache(&sMouseWheelTransactionIgnoreMoveDelay,
                                  "mousewheel.transaction.ignoremovedelay", 100);
     Preferences::AddBoolVarCache(&sTestMouseScroll, "test.mousescroll", false);
     sIsInitialized = true;
   }
 }
 
+/******************************************************************/
+/* mozilla::AutoWheelDeltaAdjuster                                */
+/******************************************************************/
+
+AutoWheelDeltaAdjuster::AutoWheelDeltaAdjuster(WidgetWheelEvent& aWheelEvent)
+  : mWheelEvent(aWheelEvent)
+  , mOldDeltaX(aWheelEvent.mDeltaX)
+  , mOldDeltaZ(aWheelEvent.mDeltaZ)
+  , mOldOverflowDeltaX(aWheelEvent.mOverflowDeltaX)
+  , mOldLineOrPageDeltaX(aWheelEvent.mLineOrPageDeltaX)
+  , mTreatedVerticalWheelAsHorizontalScroll(false)
+{
+  MOZ_ASSERT(!aWheelEvent.mDeltaValuesAdjustedForDefaultHandler);
+
+  if (EventStateManager::WheelEventIsHorizontalScrollAction(&aWheelEvent)) {
+    // Move deltaY values to deltaX and set both deltaY and deltaZ to 0.
+    mWheelEvent.mDeltaX = mWheelEvent.mDeltaY;
+    mWheelEvent.mDeltaY = 0.0;
+    mWheelEvent.mDeltaZ = 0.0;
+    mWheelEvent.mOverflowDeltaX = mWheelEvent.mOverflowDeltaY;
+    mWheelEvent.mOverflowDeltaY = 0.0;
+    mWheelEvent.mLineOrPageDeltaX = mWheelEvent.mLineOrPageDeltaY;
+    mWheelEvent.mLineOrPageDeltaY = 0;
+    mWheelEvent.mDeltaValuesAdjustedForDefaultHandler = true;
+    mTreatedVerticalWheelAsHorizontalScroll = true;
+  }
+}
+
+AutoWheelDeltaAdjuster::~AutoWheelDeltaAdjuster()
+{
+  if (mTreatedVerticalWheelAsHorizontalScroll &&
+      mWheelEvent.mDeltaValuesAdjustedForDefaultHandler) {
+    mWheelEvent.mDeltaY = mWheelEvent.mDeltaX;
+    mWheelEvent.mDeltaX = mOldDeltaX;
+    mWheelEvent.mDeltaZ = mOldDeltaZ;
+    mWheelEvent.mOverflowDeltaY = mWheelEvent.mOverflowDeltaX;
+    mWheelEvent.mOverflowDeltaX = mOldOverflowDeltaX;
+    mWheelEvent.mLineOrPageDeltaY = mWheelEvent.mLineOrPageDeltaX;
+    mWheelEvent.mLineOrPageDeltaX = mOldLineOrPageDeltaX;
+    mWheelEvent.mDeltaValuesAdjustedForDefaultHandler = false;
+  }
+}
+
 } // namespace mozilla
--- a/dom/events/WheelHandlingHelper.h
+++ b/dom/events/WheelHandlingHelper.h
@@ -199,11 +199,40 @@ protected:
     static int32_t sMouseWheelAccelerationStart;
     static int32_t sMouseWheelAccelerationFactor;
     static uint32_t sMouseWheelTransactionTimeout;
     static uint32_t sMouseWheelTransactionIgnoreMoveDelay;
     static bool sTestMouseScroll;
   };
 };
 
+/**
+ * When a wheel event should be treated as specially, e.g., it's a vertical
+ * wheel operation but user wants to scroll the target horizontally, this
+ * class adjust the delta values automatically.  Then, restores the original
+ * value when the instance is destroyed.
+ */
+class MOZ_STACK_CLASS AutoWheelDeltaAdjuster final
+{
+public:
+  /**
+   * @param aWheelEvent        A wheel event.  The delta values may be
+   *                           modified for default handler.
+   *                           Its mDeltaValuesAdjustedForDefaultHandler
+   *                           must not be true because if it's true,
+   *                           the event has already been adjusted the
+   *                           delta values for default handler.
+   */
+  explicit AutoWheelDeltaAdjuster(WidgetWheelEvent& aWheelEvent);
+  ~AutoWheelDeltaAdjuster();
+
+private:
+  WidgetWheelEvent& mWheelEvent;
+  double mOldDeltaX;
+  double mOldDeltaZ;
+  double mOldOverflowDeltaX;
+  int32_t mOldLineOrPageDeltaX;
+  bool mTreatedVerticalWheelAsHorizontalScroll;
+};
+
 } // namespace mozilla
 
 #endif // mozilla_WheelHandlingHelper_h_
--- a/dom/events/moz.build
+++ b/dom/events/moz.build
@@ -31,16 +31,17 @@ EXPORTS.mozilla += [
     'EventStates.h',
     'IMEStateManager.h',
     'InternalMutationEvent.h',
     'JSEventHandler.h',
     'KeyNameList.h',
     'PhysicalKeyCodeNameList.h',
     'TextComposition.h',
     'VirtualKeyCodeList.h',
+    'WheelHandlingHelper.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'AnimationEvent.h',
     'BeforeUnloadEvent.h',
     'ClipboardEvent.h',
     'CommandEvent.h',
     'CompositionEvent.h',
--- a/dom/events/test/window_wheel_default_action.html
+++ b/dom/events/test/window_wheel_default_action.html
@@ -73,16 +73,28 @@ SimpleTest.requestFlakyTimeout("untriage
 var winUtils = SpecialPowers.getDOMWindowUtils(window);
 // grab refresh driver
 winUtils.advanceTimeAndRefresh(100);
 
 var gScrollableElement = document.getElementById("scrollable");
 var gScrolledElement = document.getElementById("scrolled");
 var gSpacerForBodyElement = document.getElementById("spacerForBody");
 
+const kDefaultActionNone             = 0;
+const kDefaultActionScroll           = 1;
+const kDefaultActionHistory          = 2;
+const kDefaultActionZoom             = 3;
+const kDefaultActionHorizontalScroll = 4;
+
+const kDefaultActionOverrideXNoOverride = -1;
+const kDefaultActionOverrideXNone       = kDefaultActionNone;
+const kDefaultActionOverrideXScroll     = kDefaultActionScroll;
+const kDefaultActionOverrideXHistory    = kDefaultActionHistory;
+const kDefaultActionOverrideXZoom       = kDefaultActionZoom;
+
 function is()
 {
   window.opener.is.apply(window.opener, arguments);
 }
 
 function ok()
 {
   window.opener.ok.apply(window.opener, arguments);
@@ -533,108 +545,108 @@ function doTestScroll(aSettings, aCallba
 
     // momentum scroll should cause scroll even if the action is history, but if the default action is none,
     // shouldn't do it.
     { description: "Scroll to bottom by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollDown },
     { description: "Scroll to bottom by momentum pixel scroll when lineOrPageDelta is 1, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollDown },
     { description: "Scroll to top by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollUp },
     { description: "Scroll to top by momentum pixel scroll when lineOrPageDelta is -1, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollUp },
     { description: "Scroll to right by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollRight },
     { description: "Scroll to right by momentum pixel scroll when lineOrPageDelta is 1, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollRight },
     { description: "Scroll to left by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollLeft },
     { description: "Scroll to left by momentum pixel scroll when lineOrPageDelta is -1, even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0,
                lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollLeft },
     { description: "Scroll to bottom-right by momentum pixel scroll even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollDown | kScrollRight },
     { description: "Scroll to bottom-left by momentum pixel scroll even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollDown | kScrollLeft },
     { description: "Scroll to top-left by momentum pixel scroll even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollUp | kScrollLeft },
     { description: "Scroll to top-right by momentum pixel scroll even if the action is history",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kScrollUp | kScrollRight },
     { description: "Not Scroll by momentum pixel scroll for z (action is history)",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kNoScroll },
     { description: "Not Scroll by momentum pixel scroll if default action is none (action is history)",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0,
                lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true,
                expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
-               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+               shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false },
       expected: kNoScroll,
       prepare: function (cb) { SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.action", 0]]}, cb); },
       cleanup: function (cb) { SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.action", 1]]}, cb); } },
 
     // Don't scroll along axis whose overflow style is hidden.
     { description: "Scroll to only bottom by oblique pixel wheel event with overflow-x: hidden",
       event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
                deltaX: 16.0, deltaY: 16.0, deltaZ: 0.0,
@@ -941,16 +953,360 @@ function doTestScroll(aSettings, aCallba
         winUtils.advanceTimeAndRefresh(100);
         doNextTest();
       }
     });
   }
   doNextTest();
 }
 
+function doTestHorizontalScroll(aSettings, aCallback)
+{
+  const kNoScroll    = 0x00;
+  const kScrollLeft  = 0x01;
+  const kScrollRight = 0x02;
+
+  const kTests = [
+    { description: "Scroll to right by pixel scroll even if lineOrPageDelta is 0",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll to right by pixel scroll when lineOrPageDelta is 1",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll to left by pixel scroll even if lineOrPageDelta is 0",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Scroll to left by pixel scroll when lineOrPageDelta is -1",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Don't scroll by deltaX (pixel scroll, lineOrPageDelta is 0)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by deltaX (pixel scroll, lineOrPageDelta is 1)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by negative deltaX (pixel scroll, lineOrPageDelta is 0)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by negative deltaX (pixel scroll, lineOrPageDelta is -1)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Scroll only to right by diagonal pixel scroll (to bottom-right)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll only to right by diagonal pixel scroll (to bottom-left)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll only to left by diagonal pixel scroll (to top-left)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Scroll only to left by pixel scroll (to bottom-right)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Don't scroll by pixel scroll for z-axis",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+               deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+
+    { description: "Scroll to right by line scroll even if lineOrPageDelta is 0",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll to right by line scroll when lineOrPageDelta is 1",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll to left by line scroll even if lineOrPageDelta is 0",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Scroll to left by line scroll when lineOrPageDelta is -1",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Don't scroll by deltaX (line scroll, lineOrPageDelta is 0)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by deltaX (line scroll, lineOrPageDelta is 1)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by negative deltaX (line scroll, lineOrPageDelta is 0)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by negative deltaY (line scroll, lineOrPageDelta is -1)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Scroll only to right by diagonal line scroll (to bottom-right)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll only to right by diagonal line scroll (to bottom-left)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll only to left by diagonal line scroll (to top-left)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Scroll only to left by line scroll (to top-right)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Don't scroll by line scroll for z-axis",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+
+    { description: "Scroll to right by page scroll even if lineOrPageDelta is 0",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll to right by page scroll when lineOrPageDelta is 1",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll to left by page scroll even if lineOrPageDelta is 0",
+      event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
+               deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Scroll to left by page scroll when lineOrPageDelta is -1",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is 0)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is 1)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is 0)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is -1)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0,
+               lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+    { description: "Scroll only to right by diagonal page scroll (to bottom-right)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll only to right by diagonal page scroll (to bottom-left)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollRight },
+    { description: "Scroll only to left by diagonal page scroll (to top-left)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Scroll only to left by diagonal page scroll (to top-right)",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kScrollLeft },
+    { description: "Don't scroll by page scroll for z-axis",
+      event: { deltaMode: WheelEvent.DOM_DELTA_PAGE,
+               deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0,
+               lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false,
+               expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0,
+               shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false },
+      expected: kNoScroll },
+  ];
+
+  var description;
+
+  var currentTestIndex = -1;
+  // deltaY should cause horizontal scroll and affected by deltaMultiplierY.
+  // So, horizontal scroll amount and direction is affected by deltaMultiplierY.
+  var isXReverted = (aSettings.deltaMultiplierY < 0);
+
+  function doNextTest()
+  {
+    if (++currentTestIndex >= kTests.length) {
+      SimpleTest.executeSoon(aCallback);
+      return;
+    }
+
+    gScrollableElement.scrollTop = 1000;
+    gScrollableElement.scrollLeft = 1000;
+
+    var currentTest = kTests[currentTestIndex];
+    description = "doTestHorizontalScroll(aSettings=" + aSettings.description + "), " + currentTest.description + ": ";
+    if (currentTest.prepare) {
+      currentTest.prepare(doTestCurrentScroll);
+    } else {
+      doTestCurrentScroll();
+    }
+  }
+
+  function doTestCurrentScroll() {
+    var currentTest = kTests[currentTestIndex];
+    sendWheelAndWait(10, 10, currentTest.event, function () {
+      is(gScrollableElement.scrollTop, 1000, description + "scrolled vertical");
+      if (currentTest.expected == kNoScroll) {
+        is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal");
+      } else {
+        var scrollLeft = !isXReverted ? (currentTest.expected & kScrollLeft) :
+                                        (currentTest.expected & kScrollRight);
+        var scrollRight = !isXReverted ? (currentTest.expected & kScrollRight) :
+                                         (currentTest.expected & kScrollLeft);
+        if (scrollLeft) {
+          ok(gScrollableElement.scrollLeft < 1000, description + "not scrolled to left, got " + gScrollableElement.scrollLeft);
+        } else if (scrollRight) {
+          ok(gScrollableElement.scrollLeft > 1000, description + "not scrolled to right, got " + gScrollableElement.scrollLeft);
+        } else {
+          is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal");
+        }
+      }
+      if (currentTest.cleanup) {
+        currentTest.cleanup(nextStep);
+      } else {
+        nextStep();
+      }
+
+      function nextStep() {
+        winUtils.advanceTimeAndRefresh(100);
+        doNextTest();
+      }
+    });
+  }
+  doNextTest();
+}
+
 function doTestZoom(aSettings, aCallback)
 {
   if ((aSettings.deltaMultiplierX != 1.0  && aSettings.deltaMultiplierX != -1.0) ||
       (aSettings.deltaMultiplierY != 1.0  && aSettings.deltaMultiplierY != -1.0)) {
     todo(false, "doTestZoom doesn't support to test with aSettings=" + aSettings.description);
     SimpleTest.executeSoon(aCallback);
     return;
   }
@@ -1654,119 +2010,119 @@ function doTestActionOverride(aCallback)
 {
   const kNoScroll    = 0x00;
   const kScrollUp    = 0x01;
   const kScrollDown  = 0x02;
   const kScrollLeft  = 0x04;
   const kScrollRight = 0x08;
 
   const kTests = [
-    { action: 1, override_x: -1,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNoOverride,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 1, override_x: 0,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNone,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 1, override_x: 1,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXScroll,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 0, override_x: -1,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNoOverride,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 0, override_x: 0,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNone,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 0, override_x: 1,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXScroll,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 1, override_x: -1,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNoOverride,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 0.5,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 1, override_x: 0,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNone,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 0.5,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 1, override_x: 1,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXScroll,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 0.5,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 0, override_x: -1,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNoOverride,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 0.5,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 0, override_x: 0,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNone,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 0.5,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 0, override_x: 1,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXScroll,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 1.0, deltaY: 0.5,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 1, override_x: -1,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNoOverride,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 0.5, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 1, override_x: 0,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNone,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 0.5, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 1, override_x: 1,
+    { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXScroll,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 0.5, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kScrollDown | kScrollRight
     },
-    { action: 0, override_x: -1,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNoOverride,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 0.5, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 0, override_x: 0,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNone,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 0.5, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
-    { action: 0, override_x: 1,
+    { action: kDefaultActionNone, override_x: kDefaultActionOverrideXScroll,
       event: { deltaMode: WheelEvent.DOM_DELTA_LINE,
                deltaX: 0.5, deltaY: 1.0,
                lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 },
       expected: kNoScroll
     },
   ];
 
   var index = 0;
@@ -1815,22 +2171,24 @@ function doTestActionOverride(aCallback)
   doIt();
 }
 
 function runTests()
 {
   SpecialPowers.pushPrefEnv({"set": [
     ["test.events.async.enabled", true],
     ["general.smoothScroll", false],
-    ["mousewheel.default.action", 1],      // scroll
-    ["mousewheel.default.action.override_x", -1],
-    ["mousewheel.with_shift.action", 2],   // history
-    ["mousewheel.with_shift.action.override_x", -1],
-    ["mousewheel.with_control.action", 3], // zoom
-    ["mousewheel.with_control.action.override_x", -1]]},
+    ["mousewheel.default.action", kDefaultActionScroll],
+    ["mousewheel.default.action.override_x", kDefaultActionOverrideXNoOverride],
+    ["mousewheel.with_shift.action", kDefaultActionHorizontalScroll],
+    ["mousewheel.with_shift.action.override_x", kDefaultActionOverrideXNoOverride],
+    ["mousewheel.with_control.action", kDefaultActionZoom],
+    ["mousewheel.with_control.action.override_x", kDefaultActionOverrideXNoOverride],
+    ["mousewheel.with_alt.action", kDefaultActionHistory],
+    ["mousewheel.with_alt.action.override_x", kDefaultActionOverrideXNoOverride]]},
     runTests2);
 }
 
 function runTests2()
 {
   const kSettings = [
     { description: "all delta values are not customized",
       deltaMultiplierX:  1.0, deltaMultiplierY:  1.0, deltaMultiplierZ:  1.0 },
@@ -1854,30 +2212,32 @@ function runTests2()
       deltaMultiplierX:  1.0, deltaMultiplierY:  1.0, deltaMultiplierZ: -2.0 },
   ];
 
   var index = 0;
 
   function doTest() {
     setDeltaMultiplierSettings(kSettings[index], function () {
       doTestScroll(kSettings[index], function () {
-        doTestZoom(kSettings[index], function() {
-          if (++index == kSettings.length) {
-            setDeltaMultiplierSettings(kSettings[0], function() {
-              doTestZoomedScroll(function() {
-                doTestWholeScroll(function() {
-                  doTestActionOverride(function() {
-                    finishTests();
+        doTestHorizontalScroll(kSettings[index], function() {
+          doTestZoom(kSettings[index], function() {
+            if (++index == kSettings.length) {
+              setDeltaMultiplierSettings(kSettings[0], function() {
+                doTestZoomedScroll(function() {
+                  doTestWholeScroll(function() {
+                    doTestActionOverride(function() {
+                      finishTests();
+                    });
                   });
                 });
               });
-            });
-          } else {
-            doTest();
-          }
+            } else {
+              doTest();
+            }
+          });
         });
       });
     });
   }
   doTest();
 }
 
 function finishTests()
--- a/gfx/layers/apz/public/IAPZCTreeManager.cpp
+++ b/gfx/layers/apz/public/IAPZCTreeManager.cpp
@@ -8,16 +8,17 @@
 
 #include "gfxPrefs.h"                       // for gfxPrefs
 #include "InputData.h"                      // for InputData, etc
 #include "mozilla/EventStateManager.h"      // for WheelPrefs
 #include "mozilla/layers/APZThreadUtils.h"  // for AssertOnCompositorThread, etc
 #include "mozilla/MouseEvents.h"            // for WidgetMouseEvent
 #include "mozilla/TextEvents.h"             // for WidgetKeyboardEvent
 #include "mozilla/TouchEvents.h"            // for WidgetTouchEvent
+#include "mozilla/WheelHandlingHelper.h"    // for AutoWheelDeltaAdjuster
 
 namespace mozilla {
 namespace layers {
 
 static bool
 WillHandleMouseEvent(const WidgetMouseEventBase& aEvent)
 {
   return aEvent.mMessage == eMouseMove ||
@@ -108,41 +109,49 @@ IAPZCTreeManager::ReceiveInputEvent(
             ((wheelEvent.mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_LINE &&
               gfxPrefs::WheelSmoothScrollEnabled()) ||
              (wheelEvent.mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_PAGE &&
               gfxPrefs::PageSmoothScrollEnabled())))
         {
           scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
         }
 
-        ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
-        ScrollWheelInput input(wheelEvent.mTime, wheelEvent.mTimeStamp, 0,
-                               scrollMode,
-                               ScrollWheelInput::DeltaTypeForDeltaMode(
-                                                   wheelEvent.mDeltaMode),
-                               origin,
-                               wheelEvent.mDeltaX, wheelEvent.mDeltaY,
-                               wheelEvent.mAllowToOverrideSystemScrollSpeed);
+        // AutoWheelDeltaAdjuster may adjust the delta values for default
+        // action hander.  The delta values will be restored automatically
+        // when its instance is destroyed.
+        AutoWheelDeltaAdjuster adjuster(wheelEvent);
+
+        // If the wheel event becomes no-op event, don't handle it as scroll.
+        if (wheelEvent.mDeltaX || wheelEvent.mDeltaY) {
+          ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
+          ScrollWheelInput input(wheelEvent.mTime, wheelEvent.mTimeStamp, 0,
+                                 scrollMode,
+                                 ScrollWheelInput::DeltaTypeForDeltaMode(
+                                                     wheelEvent.mDeltaMode),
+                                 origin,
+                                 wheelEvent.mDeltaX, wheelEvent.mDeltaY,
+                                 wheelEvent.mAllowToOverrideSystemScrollSpeed);
 
-        // We add the user multiplier as a separate field, rather than premultiplying
-        // it, because if the input is converted back to a WidgetWheelEvent, then
-        // EventStateManager would apply the delta a second time. We could in theory
-        // work around this by asking ESM to customize the event much sooner, and
-        // then save the "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for
-        // now, this seems easier.
-        EventStateManager::GetUserPrefsForWheelEvent(&wheelEvent,
-          &input.mUserDeltaMultiplierX,
-          &input.mUserDeltaMultiplierY);
+          // We add the user multiplier as a separate field, rather than premultiplying
+          // it, because if the input is converted back to a WidgetWheelEvent, then
+          // EventStateManager would apply the delta a second time. We could in theory
+          // work around this by asking ESM to customize the event much sooner, and
+          // then save the "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for
+          // now, this seems easier.
+          EventStateManager::GetUserPrefsForWheelEvent(&wheelEvent,
+            &input.mUserDeltaMultiplierX,
+            &input.mUserDeltaMultiplierY);
 
-        nsEventStatus status = ReceiveInputEvent(input, aOutTargetGuid, aOutInputBlockId);
-        wheelEvent.mRefPoint.x = input.mOrigin.x;
-        wheelEvent.mRefPoint.y = input.mOrigin.y;
-        wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
-        wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
-        return status;
+          nsEventStatus status = ReceiveInputEvent(input, aOutTargetGuid, aOutInputBlockId);
+          wheelEvent.mRefPoint.x = input.mOrigin.x;
+          wheelEvent.mRefPoint.y = input.mOrigin.y;
+          wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+          wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+          return status;
+        }
       }
 
       UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage);
       ProcessUnhandledEvent(&aEvent.mRefPoint, aOutTargetGuid, &aEvent.mFocusSequenceNumber);
       return nsEventStatus_eIgnore;
 
     }
     case eKeyboardEventClass: {
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2726,27 +2726,33 @@ pref("mousewheel.system_scroll_override_
 
 // mousewheel.*.action can specify the action when you use mosue wheel.
 // When no modifier keys are pressed or two or more modifires are pressed,
 // .default is used.
 // 0: Nothing happens
 // 1: Scrolling contents
 // 2: Go back or go forward, in your history
 // 3: Zoom in or out.
+// 4: Treat vertical wheel as horizontal scroll
+//      This treats vertical wheel operation (i.e., deltaY) as horizontal
+//      scroll.  deltaX and deltaZ are always ignored.  So, only
+//      "delta_multiplier_y" pref affects the scroll speed.
 pref("mousewheel.default.action", 1);
 pref("mousewheel.with_alt.action", 2);
 pref("mousewheel.with_control.action", 3);
 pref("mousewheel.with_meta.action", 1);  // command key on Mac
-pref("mousewheel.with_shift.action", 1);
+pref("mousewheel.with_shift.action", 4);
 pref("mousewheel.with_win.action", 1);
 
 // mousewheel.*.action.override_x will override the action
 // when the mouse wheel is rotated along the x direction.
 // -1: Don't override the action.
 // 0 to 3: Override the action with the specified value.
+// Note that 4 isn't available because it doesn't make sense to apply the
+// default action only for y direction to this pref.
 pref("mousewheel.default.action.override_x", -1);
 pref("mousewheel.with_alt.action.override_x", -1);
 pref("mousewheel.with_control.action.override_x", -1);
 pref("mousewheel.with_meta.action.override_x", -1);  // command key on Mac
 pref("mousewheel.with_shift.action.override_x", -1);
 pref("mousewheel.with_win.action.override_x", -1);
 
 // mousewheel.*.delta_multiplier_* can specify the value muliplied by the delta
--- a/widget/MouseEvents.h
+++ b/widget/MouseEvents.h
@@ -485,16 +485,17 @@ private:
     , mLineOrPageDeltaY(0)
     , mScrollType(SCROLL_DEFAULT)
     , mCustomizedByUserPrefs(false)
     , mIsMomentum(false)
     , mIsNoLineOrPageDelta(false)
     , mViewPortIsOverscrolled(false)
     , mCanTriggerSwipe(false)
     , mAllowToOverrideSystemScrollSpeed(false)
+    , mDeltaValuesAdjustedForDefaultHandler(false)
   {
   }
 
 public:
   virtual WidgetWheelEvent* AsWheelEvent() override { return this; }
 
   WidgetWheelEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget)
     : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, eWheelEventClass)
@@ -509,16 +510,17 @@ public:
     , mScrollType(SCROLL_DEFAULT)
     , mCustomizedByUserPrefs(false)
     , mMayHaveMomentum(false)
     , mIsMomentum(false)
     , mIsNoLineOrPageDelta(false)
     , mViewPortIsOverscrolled(false)
     , mCanTriggerSwipe(false)
     , mAllowToOverrideSystemScrollSpeed(true)
+    , mDeltaValuesAdjustedForDefaultHandler(false)
   {
   }
 
   virtual WidgetEvent* Duplicate() const override
   {
     MOZ_ASSERT(mClass == eWheelEventClass,
                "Duplicate() must be overridden by sub class");
     // Not copying widget, it is a weak reference.
@@ -626,16 +628,20 @@ public:
   // viewport.
   bool mCanTriggerSwipe;
 
   // If mAllowToOverrideSystemScrollSpeed is true, the scroll speed may be
   // overridden.  Otherwise, the scroll speed won't be overridden even if
   // it's enabled by the pref.
   bool mAllowToOverrideSystemScrollSpeed;
 
+  // While default handler handles a wheel event specially (e.g., treating
+  // mDeltaY as horizontal scroll), this is set to true.
+  bool mDeltaValuesAdjustedForDefaultHandler;
+
   void AssignWheelEventData(const WidgetWheelEvent& aEvent, bool aCopyTargets)
   {
     AssignMouseEventBaseData(aEvent, aCopyTargets);
 
     mDeltaX = aEvent.mDeltaX;
     mDeltaY = aEvent.mDeltaY;
     mDeltaZ = aEvent.mDeltaZ;
     mDeltaMode = aEvent.mDeltaMode;
@@ -647,16 +653,18 @@ public:
     mLineOrPageDeltaY = aEvent.mLineOrPageDeltaY;
     mScrollType = aEvent.mScrollType;
     mOverflowDeltaX = aEvent.mOverflowDeltaX;
     mOverflowDeltaY = aEvent.mOverflowDeltaY;
     mViewPortIsOverscrolled = aEvent.mViewPortIsOverscrolled;
     mCanTriggerSwipe = aEvent.mCanTriggerSwipe;
     mAllowToOverrideSystemScrollSpeed =
       aEvent.mAllowToOverrideSystemScrollSpeed;
+    mDeltaValuesAdjustedForDefaultHandler =
+      aEvent.mDeltaValuesAdjustedForDefaultHandler;
   }
 
   // System scroll speed settings may be too slow at using Gecko.  In such
   // case, we should override the scroll speed computed with system settings.
   // Following methods return preferred delta values which are multiplied by
   // factors specified by prefs.  If system scroll speed shouldn't be
   // overridden (e.g., this feature is disabled by pref), they return raw
   // delta values.
--- a/widget/nsGUIEventIPC.h
+++ b/widget/nsGUIEventIPC.h
@@ -194,16 +194,17 @@ struct ParamTraits<mozilla::WidgetWheelE
     WriteParam(aMsg, aParam.mLineOrPageDeltaX);
     WriteParam(aMsg, aParam.mLineOrPageDeltaY);
     WriteParam(aMsg, static_cast<uint8_t>(aParam.mScrollType));
     WriteParam(aMsg, aParam.mOverflowDeltaX);
     WriteParam(aMsg, aParam.mOverflowDeltaY);
     WriteParam(aMsg, aParam.mViewPortIsOverscrolled);
     WriteParam(aMsg, aParam.mCanTriggerSwipe);
     WriteParam(aMsg, aParam.mAllowToOverrideSystemScrollSpeed);
+    WriteParam(aMsg, aParam.mDeltaValuesAdjustedForDefaultHandler);
   }
 
   static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
   {
     uint8_t scrollType = 0;
     bool rv =
       ReadParam(aMsg, aIter,
                 static_cast<mozilla::WidgetMouseEventBase*>(aResult)) &&
@@ -217,17 +218,18 @@ struct ParamTraits<mozilla::WidgetWheelE
       ReadParam(aMsg, aIter, &aResult->mIsNoLineOrPageDelta) &&
       ReadParam(aMsg, aIter, &aResult->mLineOrPageDeltaX) &&
       ReadParam(aMsg, aIter, &aResult->mLineOrPageDeltaY) &&
       ReadParam(aMsg, aIter, &scrollType) &&
       ReadParam(aMsg, aIter, &aResult->mOverflowDeltaX) &&
       ReadParam(aMsg, aIter, &aResult->mOverflowDeltaY) &&
       ReadParam(aMsg, aIter, &aResult->mViewPortIsOverscrolled) &&
       ReadParam(aMsg, aIter, &aResult->mCanTriggerSwipe) &&
-      ReadParam(aMsg, aIter, &aResult->mAllowToOverrideSystemScrollSpeed);
+      ReadParam(aMsg, aIter, &aResult->mAllowToOverrideSystemScrollSpeed) &&
+      ReadParam(aMsg, aIter, &aResult->mDeltaValuesAdjustedForDefaultHandler);
     aResult->mScrollType =
       static_cast<mozilla::WidgetWheelEvent::ScrollType>(scrollType);
     return rv;
   }
 };
 
 template<>
 struct ParamTraits<mozilla::WidgetPointerHelper>