--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -74,22 +74,28 @@ const PAGECONTENT_SOMEHIDDEN =
const PAGECONTENT_TRANSLATED =
"<html><body>" +
"<div id='div'>" +
"<iframe id='frame' width='320' height='295' style='border: none;'" +
" src='data:text/html,<select id=select autofocus><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" +
"</iframe>" +
"</div></body></html>";
-function openSelectPopup(selectPopup, withMouse, selector = "select", win = window) {
+function openSelectPopup(selectPopup, mode = "key", selector = "select", win = window) {
let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
- if (withMouse) {
- return Promise.all([popupShownPromise,
- BrowserTestUtils.synthesizeMouseAtCenter(selector, { }, win.gBrowser.selectedBrowser)]);
+ if (mode == "click" || mode == "mousedown") {
+ let mousePromise;
+ if (mode == "click") {
+ mousePromise = BrowserTestUtils.synthesizeMouseAtCenter(selector, { }, win.gBrowser.selectedBrowser);
+ } else {
+ mousePromise = BrowserTestUtils.synthesizeMouse(selector, 5, 5, { type: "mousedown" }, win.gBrowser.selectedBrowser);
+ }
+
+ return Promise.all([popupShownPromise, mousePromise]);
}
EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, code: "ArrowDown" }, win);
return popupShownPromise;
}
function hideSelectPopup(selectPopup, mode = "enter", win = window) {
let browser = win.gBrowser.selectedBrowser;
@@ -180,26 +186,26 @@ function* doSelectTests(contentType, dtd
yield hideSelectPopup(selectPopup);
is(menulist.selectedIndex, 3, "Item 3 still selected");
is((yield getInputEvents()), 1, "After closed - number of input events");
is((yield getChangeEvents()), 1, "After closed - number of change events");
// Opening and closing the popup without changing the value should not fire a change event.
- yield openSelectPopup(selectPopup, true);
+ yield openSelectPopup(selectPopup, "click");
yield hideSelectPopup(selectPopup, "escape");
is((yield getInputEvents()), 1, "Open and close with no change - number of input events");
is((yield getChangeEvents()), 1, "Open and close with no change - number of change events");
EventUtils.synthesizeKey("VK_TAB", { });
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
is((yield getInputEvents()), 1, "Tab away from select with no change - number of input events");
is((yield getChangeEvents()), 1, "Tab away from select with no change - number of change events");
- yield openSelectPopup(selectPopup, true);
+ yield openSelectPopup(selectPopup, "click");
EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
yield hideSelectPopup(selectPopup, "escape");
is((yield getInputEvents()), isWindows ? 2 : 1, "Open and close with change - number of input events");
is((yield getChangeEvents()), isWindows ? 2 : 1, "Open and close with change - number of change events");
EventUtils.synthesizeKey("VK_TAB", { });
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
is((yield getInputEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of input events");
is((yield getChangeEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of change events");
@@ -231,42 +237,42 @@ add_task(function*() {
add_task(function*() {
const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
let menulist = document.getElementById("ContentSelectDropdown");
let selectPopup = menulist.menupopup;
// First, try it when a different <select> element than the one that is open is removed
- yield openSelectPopup(selectPopup, true, "#one");
+ yield openSelectPopup(selectPopup, "click", "#one");
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
content.document.body.removeChild(content.document.getElementById("two"));
});
// Wait a bit just to make sure the popup won't close.
yield new Promise(resolve => setTimeout(resolve, 1000));
is(selectPopup.state, "open", "Different popup did not affect open popup");
yield hideSelectPopup(selectPopup);
// Next, try it when the same <select> element than the one that is open is removed
- yield openSelectPopup(selectPopup, true, "#three");
+ yield openSelectPopup(selectPopup, "click", "#three");
let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
content.document.body.removeChild(content.document.getElementById("three"));
});
yield popupHiddenPromise;
ok(true, "Popup hidden when select is removed");
// Finally, try it when the tab is closed while the select popup is open.
- yield openSelectPopup(selectPopup, true, "#one");
+ yield openSelectPopup(selectPopup, "click", "#one");
popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
yield BrowserTestUtils.removeTab(tab);
yield popupHiddenPromise;
ok(true, "Popup hidden when tab is closed");
});
@@ -274,17 +280,17 @@ add_task(function*() {
add_task(function*() {
const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED);
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
let menulist = document.getElementById("ContentSelectDropdown");
let selectPopup = menulist.menupopup;
// First, get the position of the select popup when no translations have been applied.
- yield openSelectPopup(selectPopup, false);
+ yield openSelectPopup(selectPopup);
let rect = selectPopup.getBoundingClientRect();
let expectedX = rect.left;
let expectedY = rect.top;
yield hideSelectPopup(selectPopup);
// Iterate through a set of steps which each add more translation to the select's expected position.
@@ -315,17 +321,17 @@ add_task(function*() {
resolve();
});
elem.style = contentStep[1];
elem.getBoundingClientRect();
});
});
- yield openSelectPopup(selectPopup, false);
+ yield openSelectPopup(selectPopup);
expectedX += step[2];
expectedY += step[3];
let popupRect = selectPopup.getBoundingClientRect();
is(popupRect.left, expectedX, "step " + (stepIndex + 1) + " x");
is(popupRect.top, expectedY, "step " + (stepIndex + 1) + " y");
@@ -386,17 +392,17 @@ add_task(function* test_event_order() {
type: "click",
cancelable: true,
targetIsOption: true,
},
];
for (let mode of ["enter", "click"]) {
let expected = mode == "enter" ? expectedEnter : expectedClick;
- yield openSelectPopup(selectPopup, true, mode == "enter" ? "#one" : "#two");
+ yield openSelectPopup(selectPopup, "click", mode == "enter" ? "#one" : "#two");
let eventsPromise = ContentTask.spawn(browser, [mode, expected], function*([contentMode, contentExpected]) {
return new Promise((resolve) => {
function onEvent(event) {
select.removeEventListener(event.type, onEvent);
Assert.ok(contentExpected.length, "Unexpected event " + event.type);
let expectation = contentExpected.shift();
Assert.equal(event.type, expectation.type,
@@ -438,25 +444,57 @@ function* performLargePopupTests(win) {
select.options[60].selected = true;
select.focus();
});
let selectPopup = win.document.getElementById("ContentSelectDropdown").menupopup;
let browserRect = browser.getBoundingClientRect();
+ // Check if a drag-select works and scrolls the list.
+ yield openSelectPopup(selectPopup, "mousedown", "select", win);
+
+ let scrollPos = selectPopup.scrollBox.scrollTop;
+ let popupRect = selectPopup.getBoundingClientRect();
+
+ // First, check that scrolling does not occur when the mouse is moved over the
+ // anchor button but not the popup yet.
+ EventUtils.synthesizeMouseAtPoint(popupRect.left + 5, popupRect.top - 10, { type: "mousemove" }, win);
+ is(selectPopup.scrollBox.scrollTop, scrollPos, "scroll position after mousemove over button");
+
+ EventUtils.synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top + 10, { type: "mousemove" }, win);
+
+ // Dragging above the popup scrolls it up.
+ EventUtils.synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top - 20, { type: "mousemove" }, win);
+ ok(selectPopup.scrollBox.scrollTop < scrollPos - 5, "scroll position at drag up");
+
+ // Dragging below the popup scrolls it down.
+ scrollPos = selectPopup.scrollBox.scrollTop;
+ EventUtils.synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }, win);
+ ok(selectPopup.scrollBox.scrollTop > scrollPos + 5, "scroll position at drag down");
+
+ // Releasing the mouse button and moving the mouse does not change the scroll position.
+ scrollPos = selectPopup.scrollBox.scrollTop;
+ EventUtils.synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 25, { type: "mouseup" }, win);
+ is(selectPopup.scrollBox.scrollTop, scrollPos, "scroll position at mouseup");
+
+ EventUtils.synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }, win);
+ is(selectPopup.scrollBox.scrollTop, scrollPos, "scroll position at mouseup again");
+
+ yield hideSelectPopup(selectPopup, "escape", win);
+
let positions = [
"margin-top: 300px;",
"position: fixed; bottom: 100px;",
"width: 100%; height: 9999px;"
];
let position;
- while (true) {
- yield openSelectPopup(selectPopup, false, "select", win);
+ while (positions.length) {
+ yield openSelectPopup(selectPopup, "key", "select", win);
let rect = selectPopup.getBoundingClientRect();
ok(rect.top >= browserRect.top, "Popup top position in within browser area");
ok(rect.bottom <= browserRect.bottom, "Popup bottom position in within browser area");
// Don't check the scroll position for the last step as the popup will be cut off.
if (positions.length > 0) {
let cs = win.getComputedStyle(selectPopup);
@@ -476,24 +514,21 @@ function* performLargePopupTests(win) {
SimpleTest.isfuzzy(selectPopup.childNodes[selectedOption].getBoundingClientRect().bottom,
selectPopup.getBoundingClientRect().bottom - bpBottom,
1, "Popup scroll at correct position " + bpBottom);
}
yield hideSelectPopup(selectPopup, "enter", win);
position = positions.shift();
- if (!position) {
- break;
- }
let contentPainted = BrowserTestUtils.contentPainted(browser);
yield ContentTask.spawn(browser, position, function*(contentPosition) {
let select = content.document.getElementById("one");
- select.setAttribute("style", contentPosition);
+ select.setAttribute("style", contentPosition || "");
select.getBoundingClientRect();
});
yield contentPainted;
}
}
function* performSelectSearchTests(win) {
let browser = win.gBrowser.selectedBrowser;
@@ -617,17 +652,17 @@ add_task(function* test_mousemove_correc
});
yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mouseup" }, gBrowser.selectedBrowser);
yield hideSelectPopup(selectPopup);
// The popup should be closed when fullscreen mode is entered or exited.
for (let steps = 0; steps < 2; steps++) {
- yield openSelectPopup(selectPopup, true);
+ yield openSelectPopup(selectPopup, "click");
let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
let sizeModeChanged = BrowserTestUtils.waitForEvent(window, "sizemodechange");
BrowserFullScreen();
yield sizeModeChanged;
yield popupHiddenPromise;
}
yield BrowserTestUtils.removeTab(tab);
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -10,25 +10,31 @@ this.EXPORTED_SYMBOLS = [
const {utils: Cu} = Components;
const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
// Maximum number of rows to display in the select dropdown.
const MAX_ROWS = 20;
+// Interval between autoscrolls
+const AUTOSCROLL_INTERVAL = 25;
+
// Minimum elements required to show select search
const SEARCH_MINIMUM_ELEMENTS = 40;
var currentBrowser = null;
var currentMenulist = null;
var currentZoom = 1;
var closedWithEnter = false;
this.SelectParentHelper = {
+ draggedOverPopup: false,
+ scrollTimer: 0,
+
populate(menulist, items, selectedIndex, zoom) {
// Clear the current contents of the popup
menulist.menupopup.textContent = "";
currentZoom = zoom;
currentMenulist = menulist;
populateChildren(menulist, items, selectedIndex, zoom);
},
@@ -60,38 +66,85 @@ this.SelectParentHelper = {
menupopup.classList.toggle("isOpenedViaTouch", isOpenedViaTouch);
let constraintRect = browser.getBoundingClientRect();
constraintRect = new win.DOMRect(constraintRect.left + win.mozInnerScreenX,
constraintRect.top + win.mozInnerScreenY,
constraintRect.width, constraintRect.height);
menupopup.setConstraintRect(constraintRect);
menupopup.openPopupAtScreenRect(AppConstants.platform == "macosx" ? "selection" : "after_start", rect.left, rect.top, rect.width, rect.height, false, false);
+
+ // Set up for dragging
+ menupopup.setCaptureAlways();
+ this.draggedOverPopup = false;
+ menupopup.addEventListener("mousemove", this);
},
hide(menulist, browser) {
if (currentBrowser == browser) {
menulist.menupopup.hidePopup();
}
},
+ clearScrollTimer() {
+ if (this.scrollTimer) {
+ let win = currentBrowser.ownerDocument.defaultView;
+ win.clearInterval(this.scrollTimer);
+ this.scrollTimer = 0;
+ }
+ },
+
handleEvent(event) {
switch (event.type) {
case "mouseup":
+ this.clearScrollTimer();
+ currentMenulist.menupopup.removeEventListener("mousemove", this);
currentBrowser.messageManager.sendAsyncMessage("Forms:MouseUp", {});
break;
case "mouseover":
currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {});
break;
case "mouseout":
currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOut", {});
break;
+ case "mousemove":
+ let menupopup = currentMenulist.menupopup;
+ let popupRect = menupopup.getOuterScreenRect();
+
+ this.clearScrollTimer();
+
+ // If dragging outside the top or bottom edge of the popup, but within
+ // the popup area horizontally, scroll the list in that direction. The
+ // draggedOverPopup flag is used to ensure that scrolling does not start
+ // until the mouse has moved over the popup first, preventing scrolling
+ // while over the dropdown button.
+ if (event.screenX >= popupRect.left && event.screenX <= popupRect.right) {
+ if (!this.draggedOverPopup) {
+ if (event.screenY > popupRect.top && event.screenY < popupRect.bottom) {
+ this.draggedOverPopup = true;
+ }
+ }
+
+ if (this.draggedOverPopup &&
+ (event.screenY <= popupRect.top || event.screenY >= popupRect.bottom)) {
+ let scrollAmount = event.screenY <= popupRect.top ? -1 : 1;
+ menupopup.scrollBox.scrollByIndex(scrollAmount);
+
+ let win = currentBrowser.ownerDocument.defaultView;
+ this.scrollTimer = win.setInterval(function() {
+ menupopup.scrollBox.scrollByIndex(scrollAmount);
+ }, AUTOSCROLL_INTERVAL);
+ }
+ }
+
+ break;
+
case "keydown":
if (event.keyCode == event.DOM_VK_RETURN) {
closedWithEnter = true;
}
break;
case "command":
if (event.target.hasAttribute("value")) {
@@ -107,16 +160,18 @@ this.SelectParentHelper = {
currentMenulist.menupopup.hidePopup();
}
break;
case "popuphidden":
currentBrowser.messageManager.sendAsyncMessage("Forms:DismissedDropDown", {});
let popup = event.target;
this._unregisterListeners(currentBrowser, popup);
+ this.clearScrollTimer();
+ popup.releaseCapture();
popup.parentNode.hidden = true;
currentBrowser = null;
currentMenulist = null;
currentZoom = 1;
break;
}
},