author | Paolo Amadini <paolo.mozmail@amadzone.org> |
Mon, 08 Jun 2015 16:37:25 +0200 | |
changeset 248064 | 1391f87657d9abefd9e75971801c8ee2e96e8a96 |
parent 248063 | 5097e037c4b0de532f2e8571e2a05d8335108f5f |
child 248065 | 571e6f0262a24874b4156119805d927a2ad7f8b3 |
push id | 60888 |
push user | kwierso@gmail.com |
push date | Thu, 11 Jun 2015 01:38:38 +0000 |
treeherder | mozilla-inbound@39e638ed06bf [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | Gijs |
bugs | 1164028 |
milestone | 41.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -1286,8 +1286,12 @@ toolbarpaletteitem[place="palette"][hidd .popup-notification-footer[popupid="bad-content"] { display: none; } .popup-notification-footer[popupid="bad-content"][mixedblockdisabled], .popup-notification-footer[popupid="bad-content"][trackingblockdisabled] { display: block; } + +#login-fill-doorhanger:not([inDetailView]) > #login-fill-clickcapturer { + pointer-events: none; +}
--- a/browser/base/content/popup-notifications.inc +++ b/browser/base/content/popup-notifications.inc @@ -57,22 +57,32 @@ <popupnotification id="password-notification" hidden="true"> <popupnotificationcontent orient="vertical"> <textbox id="password-notification-username"/> <textbox id="password-notification-password" type="password" disabled="true"/> </popupnotificationcontent> </popupnotification> - <vbox id="login-fill-doorhanger" hidden="true"> - <description id="login-fill-testing" - value="Thanks for testing the login fill doorhanger!"/> - <textbox id="login-fill-filter"/> - <richlistbox id="login-fill-list"/> - </vbox> + <stack id="login-fill-doorhanger" hidden="true"> + <vbox id="login-fill-mainview"> + <description id="login-fill-testing" + value="Thanks for testing the login fill doorhanger!"/> + <textbox id="login-fill-filter"/> + <richlistbox id="login-fill-list"/> + </vbox> + <vbox id="login-fill-clickcapturer"/> + <vbox id="login-fill-details"> + <textbox id="login-fill-username" readonly="true"/> + <textbox id="login-fill-password" type="password" disabled="true"/> + <hbox> + <button id="login-fill-use" label="Use in form"/> + </hbox> + </vbox> + </stack> #ifdef E10S_TESTING_ONLY <popupnotification id="enable-e10s-notification" hidden="true"> <popupnotificationcontent orient="vertical"/> </popupnotification> #endif <popupnotification id="addon-progress-notification" hidden="true">
--- a/browser/themes/shared/login-doorhanger.inc.css +++ b/browser/themes/shared/login-doorhanger.inc.css @@ -1,8 +1,43 @@ +#notification-popup[popupid="login-fill"] > .panel-arrowcontainer > .panel-arrowcontent { + /* Since we display a sliding subview that extends to the border, we cannot + * keep the default padding of arrow panels. We use the same padding in the + * individual content views instead. Since we removed the padding, we also + * have to ensure the contents are clipped to the border box. */ + padding: 0; + overflow: hidden; +} + +#login-fill-mainview, +#login-fill-details { + padding: var(--panel-arrowcontent-padding); +} + +#login-fill-doorhanger[inDetailView] > #login-fill-mainview { + transform: translateX(-14px); +} + +#login-fill-mainview, +#login-fill-details { + transition: transform 150ms; +} + +#login-fill-doorhanger:not([inDetailView]) > #login-fill-details { + transform: translateX(105%); +} + +#login-fill-doorhanger:not([inDetailView]) > #login-fill-details:-moz-locale-dir(rtl) { + transform: translateX(-105%); +} + +#login-fill-doorhanger[inDetailView] > #login-fill-clickcapturer { + background-color: hsla(210,4%,10%,.1); +} + #login-fill-testing { color: #b33; font-weight: bold; } #login-fill-list { border: 1px solid black; max-height: 20em; @@ -26,8 +61,19 @@ color: #888; font-style: italic; } .login-username { margin: 4px; color: #888; } + +#login-fill-details { + padding: 4px; + background: var(--panel-arrowcontent-background); + color: var(--panel-arrowcontent-color); + background-clip: padding-box; + border-left: 1px solid hsla(210,4%,10%,.3); + box-shadow: 0 3px 5px hsla(210,4%,10%,.1), + 0 0 7px hsla(210,4%,10%,.1); + -moz-margin-start: 38px; +}
--- a/toolkit/components/passwordmgr/LoginDoorhangers.jsm +++ b/toolkit/components/passwordmgr/LoginDoorhangers.jsm @@ -10,29 +10,49 @@ this.EXPORTED_SYMBOLS = [ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/LoginManagerParent.jsm"); const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +// Helper function needed because the "disabled" property may not be available +// if the XBL binding of the UI control has not been constructed yet. +function setDisabled(element, disabled) { + if (disabled) { + element.setAttribute("disabled", "true"); + } else { + element.removeAttribute("disabled"); + } +} + this.LoginDoorhangers = {}; /** * Doorhanger for selecting and filling logins. * * @param {Object} properties * Properties from this object will be applied to the new instance. */ this.LoginDoorhangers.FillDoorhanger = function (properties) { - this.onFilterInput = this.onFilterInput.bind(this); - this.onListDblClick = this.onListDblClick.bind(this); - this.onListKeyPress = this.onListKeyPress.bind(this); - + // Set up infrastructure to access our elements and listen to events. + this.el = new Proxy({}, { + get: (target, name) => { + return this.chromeDocument.getElementById("login-fill-" + name); + }, + }); + this.eventHandlers = []; + for (let elementName of Object.keys(this.events)) { + let handlers = this.events[elementName]; + for (let eventName of Object.keys(handlers)) { + let handler = handlers[eventName]; + this.eventHandlers.push([elementName, eventName, handler.bind(this)]); + } + }; for (let name of Object.getOwnPropertyNames(properties)) { this[name] = properties[name]; } }; this.LoginDoorhangers.FillDoorhanger.prototype = { /** * Whether the elements for this doorhanger are currently in the document. @@ -47,17 +67,17 @@ this.LoginDoorhangers.FillDoorhanger.pro * web page is moved to a different chrome window by the swapDocShells method. */ set browser(browser) { const MAX_DATE_VALUE = new Date(8640000000000000); this._browser = browser; let doorhanger = this; - let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications; + let PopupNotifications = this.chromeDocument.defaultView.PopupNotifications; let notification = PopupNotifications.show( browser, "login-fill", "", "login-fill-notification-icon", null, null, { @@ -66,18 +86,17 @@ this.LoginDoorhangers.FillDoorhanger.pro // visible. We'll remove the notification manually when the page // changes, after we had time to check its final state asynchronously. timeout: MAX_DATE_VALUE, eventCallback: function (topic, otherBrowser) { switch (topic) { case "shown": // Since we specified the "dismissed" option, this event will only // be called after the "show" method returns, so the reference to - // "this.notification" will be available at this point. - doorhanger.bound = true; + // "this.notification" will be available in "bind" at this point. doorhanger.promiseHidden = new Promise(resolve => doorhanger.onUnbind = resolve); doorhanger.bind(); break; case "dismissed": case "removed": if (doorhanger.bound) { @@ -104,25 +123,25 @@ this.LoginDoorhangers.FillDoorhanger.pro _browser: null, /** * DOM document to which the doorhanger is currently associated. * * This may change during the lifetime of the doorhanger, in case the web page * is moved to a different chrome window by the swapDocShells method. */ - get chomeDocument() { + get chromeDocument() { return this.browser.ownerDocument; }, /** * Hides this notification, if the notification panel is currently open. */ hide() { - let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications; + let PopupNotifications = this.chromeDocument.defaultView.PopupNotifications; if (PopupNotifications.isPanelOpen) { PopupNotifications.panel.hidePopup(); } }, /** * Promise resolved as soon as the notification is hidden. */ @@ -134,46 +153,57 @@ this.LoginDoorhangers.FillDoorhanger.pro remove() { this.notification.remove(); }, /** * Binds this doorhanger to its UI controls. */ bind() { - this.element = this.chomeDocument.getElementById("login-fill-doorhanger"); - this.list = this.chomeDocument.getElementById("login-fill-list"); - this.filter = this.chomeDocument.getElementById("login-fill-filter"); - - this.filter.setAttribute("value", this.filterString); + // Since this may ask for the master password, we must do it at bind time. + if (this.autoDetailLogin) { + let formLogins = Services.logins.findLogins({}, this.loginFormOrigin, "", + null); + if (formLogins.length == 1) { + this.detailLogin = formLogins[0]; + } + this.autoDetailLogin = false; + } + this.el.filter.setAttribute("value", this.filterString); this.refreshList(); + this.refreshDetailView(); - this.filter.addEventListener("input", this.onFilterInput); - this.list.addEventListener("dblclick", this.onListDblClick); - this.list.addEventListener("keypress", this.onListKeyPress); + this.eventHandlers.forEach(([elementName, eventName, handler]) => { + this.el[elementName].addEventListener(eventName, handler, true); + }); // Move the main element to the notification panel for displaying. - this.notification.owner.panel.firstElementChild.appendChild(this.element); - this.element.hidden = false; + this.notification.owner.panel.firstElementChild.appendChild(this.el.doorhanger); + this.el.doorhanger.hidden = false; + + this.bound = true; }, /** * Unbinds this doorhanger from its UI controls. */ unbind() { - this.filter.removeEventListener("input", this.onFilterInput); - this.list.removeEventListener("dblclick", this.onListDblClick); - this.list.removeEventListener("keypress", this.onListKeyPress); + this.bound = false; + + this.eventHandlers.forEach(([elementName, eventName, handler]) => { + this.el[elementName].removeEventListener(eventName, handler, true); + }); this.clearList(); // Place the element back in the document for the next time we need it. - this.element.hidden = true; - this.chomeDocument.getElementById("mainPopupSet").appendChild(this.element); + this.el.doorhanger.hidden = true; + this.chromeDocument.getElementById("mainPopupSet") + .appendChild(this.el.doorhanger); }, /** * Origin for which the manual fill UI should be displayed, for example * "http://www.example.com". */ loginFormOrigin: "", @@ -184,21 +214,88 @@ this.LoginDoorhangers.FillDoorhanger.pro loginFormPresent: false, /** * User-editable string used to filter the list of all logins. */ filterString: "", /** - * Handles text changes in the filter textbox. + * Show login details automatically when the panel is first opened. + */ + autoDetailLogin: false, + + /** + * Indicates which particular login to show in the detail view. + */ + set detailLogin(detailLogin) { + this._detailLogin = detailLogin; + if (this.bound) { + this.refreshDetailView(); + } + }, + get detailLogin() { + return this._detailLogin; + }, + _detailLogin: null, + + /** + * Prototype functions for event handling. */ - onFilterInput() { - this.filterString = this.filter.value; - this.refreshList(); + events: { + mainview: { + focus(event) { + // If keyboard focus returns to any control in the the main view (for + // example using SHIFT+TAB) close the details view. + this.detailLogin = null; + }, + }, + filter: { + input(event) { + this.filterString = this.el.filter.value; + this.refreshList(); + }, + }, + list: { + click(event) { + if (event.button == 0 && this.el.list.selectedItem) { + this.displaySelectedLoginDetails(); + } + }, + keypress(event) { + if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN && + this.el.list.selectedItem) { + this.displaySelectedLoginDetails(); + } + }, + }, + clickcapturer: { + click(event) { + this.detailLogin = null; + }, + }, + details: { + transitionend(event) { + // We must set focus to the detail controls only when the transition has + // ended, otherwise focus will interfere with the animation. We do this + // only when we're showing the detail view, not when leaving. + if (event.target == this.el.details && this.detailLogin) { + if (this.loginFormPresent) { + this.el.use.focus(); + } else { + this.el.username.focus(); + } + } + }, + }, + use: { + command(event) { + this.fillLogin(); + }, + }, }, /** * Rebuilds the list of logins. */ refreshList() { this.clearList(); @@ -207,73 +304,76 @@ this.LoginDoorhangers.FillDoorhanger.pro if (filterToUse) { formLogins = formLogins.filter(login => { return login.hostname.toLowerCase().indexOf(filterToUse) != -1 || login.username.toLowerCase().indexOf(filterToUse) != -1 ; }); } for (let { hostname, username } of formLogins) { - let item = this.chomeDocument.createElementNS(XUL_NS, "richlistitem"); + let item = this.chromeDocument.createElementNS(XUL_NS, "richlistitem"); item.classList.add("login-fill-item"); item.setAttribute("hostname", hostname); item.setAttribute("username", username); if (hostname != this.loginFormOrigin) { item.classList.add("different-hostname"); } - if (!this.loginFormPresent) { - item.setAttribute("disabled", "true"); - } - this.list.appendChild(item); + this.el.list.appendChild(item); } }, /** * Clears the list of logins. */ clearList() { - while (this.list.firstChild) { - this.list.removeChild(this.list.firstChild); + let list = this.el.list; + while (list.firstChild) { + list.firstChild.remove(); } }, /** - * Handles the action associated to a login item. + * Updates all the controls of the detail view based on the chosen login. */ - onListDblClick(event) { - if (event.button != 0 || !this.list.selectedItem) { - return; + refreshDetailView() { + if (this.detailLogin) { + this.el.username.setAttribute("value", this.detailLogin.username); + this.el.password.setAttribute("value", this.detailLogin.password); + this.el.doorhanger.setAttribute("inDetailView", "true"); + setDisabled(this.el.username, false); + setDisabled(this.el.use, !this.loginFormPresent); + } else { + this.el.doorhanger.removeAttribute("inDetailView"); + // We must disable all the detail controls to ensure they cannot be + // selected with the keyboard while they are outside the visible area. + setDisabled(this.el.username, true); + setDisabled(this.el.use, true); } - this.fillLogin(); }, - onListKeyPress(event) { - if (event.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_RETURN || - !this.list.selectedItem) { + + displaySelectedLoginDetails() { + let selectedItem = this.el.list.selectedItem; + let hostLogins = Services.logins.findLogins({}, + selectedItem.getAttribute("hostname"), "", null); + let login = hostLogins.find(login => { + return login.username == selectedItem.getAttribute("username"); + }); + if (!login) { + Cu.reportError("The selected login has been removed in the meantime."); return; } - this.fillLogin(); + this.detailLogin = login; }, + fillLogin() { - if (this.list.selectedItem.hasAttribute("disabled")) { - return; - } - let formLogins = Services.logins.findLogins({}, "", "", null); - let login = formLogins.find(login => { - return login.hostname == this.list.selectedItem.getAttribute("hostname") && - login.username == this.list.selectedItem.getAttribute("username"); - }); - if (login) { - LoginManagerParent.fillForm({ - browser: this.browser, - loginFormOrigin: this.loginFormOrigin, - login, - }).catch(Cu.reportError); - } else { - Cu.reportError("The selected login has been removed in the meantime."); - } + LoginManagerParent.fillForm({ + browser: this.browser, + loginFormOrigin: this.loginFormOrigin, + login: this.detailLogin, + }).catch(Cu.reportError); this.hide(); }, }; /** * Retrieves an existing FillDoorhanger associated with a browser, or null if an * associated doorhanger of that type cannot be found. *
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm @@ -602,21 +602,24 @@ var LoginManagerParent = { if (!showLoginAnchor) { fillDoorhanger.remove(); return; } // We should only update the state of the doorhanger while it is hidden. yield fillDoorhanger.promiseHidden; fillDoorhanger.loginFormPresent = loginFormPresent; fillDoorhanger.loginFormOrigin = loginFormOrigin; - fillDoorhanger.filterString = loginFormOrigin; + fillDoorhanger.filterString = hasLogins ? loginFormOrigin : ""; + fillDoorhanger.detailLogin = null; + fillDoorhanger.autoDetailLogin = true; return; } if (showLoginAnchor) { fillDoorhanger = new LoginDoorhangers.FillDoorhanger({ browser, loginFormPresent, loginFormOrigin, - filterString: loginFormOrigin, + filterString: hasLogins ? loginFormOrigin : "", + autoDetailLogin: true, }); } }), };
--- a/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js +++ b/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js @@ -46,20 +46,30 @@ add_task(function* test_fill() { "Shown"); anchor.click(); yield promiseShown; let list = document.getElementById("login-fill-list"); Assert.equal(list.childNodes.length, 1, "list.childNodes.length === 1"); + // The button will be focused after the "transitionend" event. + list.focus(); + yield new Promise(resolve => executeSoon(resolve)); + let details = document.getElementById("login-fill-details"); + let promiseSubview = BrowserTestUtils.waitForEvent(details, + "transitionend", true, + e => e.target == details); + EventUtils.sendMouseEvent({ type: "click" }, list.childNodes[0]); + yield promiseSubview; + + // Clicking the button will dismiss the panel. let promiseHidden = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); - list.focus(); - EventUtils.sendMouseEvent({ type: "dblclick" }, list.childNodes[0]); + document.getElementById("login-fill-use").click(); yield promiseHidden; let result = yield ContentTask.spawn(browser, null, function* () { let doc = content.document; return { username: doc.getElementById("form-basic-username").value, password: doc.getElementById("form-basic-password").value, }
--- a/toolkit/themes/linux/global/popup.css +++ b/toolkit/themes/linux/global/popup.css @@ -1,14 +1,22 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +/* ::::: Variables ::::: */ +.panel-arrowcontent { + --panel-arrowcontent-padding: 10px; + --panel-arrowcontent-background: -moz-Dialog; + --panel-arrowcontent-color: -moz-DialogText; + --panel-arrowcontent-border: 1px solid ThreeDShadow; +} + /* ::::: menupopup ::::: */ menupopup, panel { -moz-appearance: menupopup; min-width: 1px; color: MenuText; } @@ -27,20 +35,20 @@ panel[type="arrow"][side="bottom"] { panel[type="arrow"][side="left"], panel[type="arrow"][side="right"] { margin-top: -16px; margin-bottom: -16px; } .panel-arrowcontent { - padding: 10px; - color: -moz-DialogText; - background: -moz-Dialog; - border: 1px solid ThreeDShadow; + padding: var(--panel-arrowcontent-padding); + color: var(--panel-arrowcontent-color); + background: var(--panel-arrowcontent-background); + border: var(--panel-arrowcontent-border); } .panel-arrow[side="top"], .panel-arrow[side="bottom"] { list-style-image: url("chrome://global/skin/icons/panelarrow-vertical.svg"); position: relative; margin-left: 6px; margin-right: 6px;
--- a/toolkit/themes/osx/global/popup.css +++ b/toolkit/themes/osx/global/popup.css @@ -1,15 +1,16 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); .panel-arrowcontent { + --panel-arrowcontent-padding: 16px; --panel-arrowcontent-background: linear-gradient(hsla(0,0%,99%,1), hsla(0,0%,99%,.975) 10%, hsla(0,0%,98%,.975)); --panel-arrowcontent-color: hsl(0,0%,10%); --panel-arrowcontent-border: none; } menupopup, panel { -moz-appearance: menupopup; @@ -47,17 +48,17 @@ panel[type="arrow"][side="right"] { .panel-arrowcontent { -moz-appearance: none; background: var(--panel-arrowcontent-background); border-radius: 3.5px; box-shadow: 0 0 0 1px hsla(210,4%,10%,.05); color: var(--panel-arrowcontent-color); border: var(--panel-arrowcontent-border); - padding: 16px; + padding: var(--panel-arrowcontent-padding); margin: 1px; } .panel-arrow[side="top"] { list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical.png"); margin-left: 16px; margin-right: 16px; margin-bottom: -1px;
--- a/toolkit/themes/windows/global/popup.css +++ b/toolkit/themes/windows/global/popup.css @@ -1,16 +1,17 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* ::::: Variables ::::: */ .panel-arrowcontent { + --panel-arrowcontent-padding: 10px; --panel-arrowcontent-background: -moz-field; --panel-arrowcontent-color: -moz-FieldText; --panel-arrowcontent-border: 1px solid ThreeDShadow; } /* ::::: menupopup ::::: */ menupopup, @@ -50,17 +51,17 @@ panel[type="arrow"][side="bottom"] { panel[type="arrow"][side="left"], panel[type="arrow"][side="right"] { margin-top: -20px; margin-bottom: -20px; } .panel-arrowcontent { - padding: 10px; + padding: var(--panel-arrowcontent-padding); color: var(--panel-arrowcontent-color); background: var(--panel-arrowcontent-background); background-clip: padding-box; border: var(--panel-arrowcontent-border); margin: 4px; } %ifdef XP_WIN