author | Matthew Noorenberghe <mozilla@noorenberghe.ca> |
Thu, 04 Dec 2014 14:40:03 -0800 | |
changeset 218483 | ef884e9f38d4224e9c6d88d2ea649d009f07d942 |
parent 218482 | eda93688e4b51393270c27e59adf802c89432018 |
child 218484 | fd1ce8cc5c029fb137ea672232c6f464da7faeee |
push id | 27932 |
push user | cbook@mozilla.com |
push date | Fri, 05 Dec 2014 12:05:46 +0000 |
treeherder | mozilla-central@18188c19a3c3 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | Unfocused |
bugs | 1104921 |
milestone | 37.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-loop.js +++ b/browser/base/content/browser-loop.js @@ -13,43 +13,59 @@ XPCOMUtils.defineLazyModuleGetter(this, (function() { LoopUI = { get toolbarButton() { delete this.toolbarButton; return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window); }, /** + * @return {Promise} + */ + promiseDocumentVisible(aDocument) { + if (!aDocument.hidden) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + aDocument.addEventListener("visibilitychange", function onVisibilityChanged() { + aDocument.removeEventListener("visibilitychange", onVisibilityChanged); + resolve(); + }); + }); + }, + + /** * Opens the panel for Loop and sizes it appropriately. * * @param {event} event The event opening the panel, used to anchor * the panel to the button which triggers it. * @param {String} [tabId] Identifier of the tab to select when the panel is * opened. Example: 'rooms', 'contacts', etc. * @return {Promise} */ openCallPanel: function(event, tabId = null) { return new Promise((resolve) => { let callback = iframe => { // Helper function to show a specific tab view in the panel. function showTab() { if (!tabId) { - resolve(); + resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument)); return; } let win = iframe.contentWindow; let ev = new win.CustomEvent("UIAction", Cu.cloneInto({ detail: { action: "selectTab", tab: tabId } }, win)); win.dispatchEvent(ev); - resolve(); + resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument)); } // If the panel has been opened and initialized before, we can skip waiting // for the content to load - because it's already there. if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") { showTab(); return; }
--- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -747,17 +747,17 @@ loop.panel = (function(_, mozL10n) { return RoomEntry({ key: room.roomToken, dispatcher: this.props.dispatcher, room: room} ); }, this) ), React.DOM.p(null, - React.DOM.button({className: "btn btn-info", + React.DOM.button({className: "btn btn-info new-room-button", onClick: this.handleCreateButtonClick, disabled: this._hasPendingOperation()}, mozL10n.get("rooms_new_room_button_label") ) ) ) ); }
--- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -747,17 +747,17 @@ loop.panel = (function(_, mozL10n) { return <RoomEntry key={room.roomToken} dispatcher={this.props.dispatcher} room={room} />; }, this) }</div> <p> - <button className="btn btn-info" + <button className="btn btn-info new-room-button" onClick={this.handleCreateButtonClick} disabled={this._hasPendingOperation()}> {mozL10n.get("rooms_new_room_button_label")} </button> </p> </div> ); }
--- a/browser/modules/UITour.jsm +++ b/browser/modules/UITour.jsm @@ -115,16 +115,43 @@ this.UITour = { ["help", {query: "#PanelUI-help"}], ["home", {query: "#home-button"}], ["forget", { query: "#panic-button", widgetName: "panic-button", allowAdd: true, }], ["loop", {query: "#loop-button"}], + ["loop-newRoom", { + query: (aDocument) => { + let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop"); + if (!loopBrowser) { + return null; + } + return loopBrowser.contentDocument.querySelector(".new-room-button"); + }, + }], + ["loop-roomList", { + query: (aDocument) => { + let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop"); + if (!loopBrowser) { + return null; + } + return loopBrowser.contentDocument.querySelector(".room-list"); + }, + }], + ["loop-signInUpLink", { + query: (aDocument) => { + let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop"); + if (!loopBrowser) { + return null; + } + return loopBrowser.contentDocument.querySelector(".signin-link"); + }, + }], ["privateWindow", {query: "#privatebrowsing-button"}], ["quit", {query: "#PanelUI-quit"}], ["search", { query: "#searchbar", widgetName: "search-container", }], ["searchProvider", { query: (aDocument) => { @@ -353,17 +380,17 @@ this.UITour = { if (!target.node) { log.error("UITour: Target could not be resolved: " + data.target); return; } let effect = undefined; if (this.highlightEffects.indexOf(data.effect) !== -1) { effect = data.effect; } - this.showHighlight(target, effect); + this.showHighlight(window, target, effect); }).catch(log.error); break; } case "hideHighlight": { this.hideHighlight(window); break; } @@ -409,17 +436,17 @@ this.UITour = { let infoOptions = {}; if (typeof data.closeButtonCallbackID == "string") infoOptions.closeButtonCallbackID = data.closeButtonCallbackID; if (typeof data.targetCallbackID == "string") infoOptions.targetCallbackID = data.targetCallbackID; - this.showInfo(messageManager, target, data.title, data.text, iconURL, buttons, infoOptions); + this.showInfo(window, messageManager, target, data.title, data.text, iconURL, buttons, infoOptions); }).catch(log.error); break; } case "hideInfo": { this.hideInfo(window); break; } @@ -730,20 +757,22 @@ this.UITour = { this.hideHighlight(aWindow); this.hideInfo(aWindow); // Ensure the menu panel is hidden before calling recreatePopup so popup events occur. this.hideMenu(aWindow, "appMenu"); this.hideMenu(aWindow, "loop"); } // Clean up panel listeners after we may have called hideMenu above. - aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations); - aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations); + aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hideAppMenuAnnotations); + aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hideAppMenuAnnotations); + aWindow.PanelUI.panel.removeEventListener("popuphidden", this.onPanelHidden); let loopPanel = aWindow.document.getElementById("loop-notification-panel"); - loopPanel.removeEventListener("popuphidden", this.onLoopPanelHidden); + loopPanel.removeEventListener("popuphidden", this.onPanelHidden); + loopPanel.removeEventListener("popuphiding", this.hideLoopPanelAnnotations); this.endUrlbarCapture(aWindow); this.removePinnedTab(aWindow); this.resetTheme(); }, getChromeWindow: function(aContentDocument) { return aContentDocument.defaultView @@ -787,17 +816,19 @@ this.UITour = { sendPageCallback: function(aMessageManager, aCallbackID, aData = {}) { let detail = {data: aData, callbackID: aCallbackID}; log.debug("sendPageCallback", detail); aMessageManager.sendAsyncMessage("UITour:SendPageCallback", detail); }, isElementVisible: function(aElement) { let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement); - return (targetStyle.display != "none" && targetStyle.visibility == "visible"); + return !aElement.ownerDocument.hidden && + targetStyle.display != "none" && + targetStyle.visibility == "visible"; }, getTarget: function(aWindow, aTargetName, aSticky = false) { log.debug("getTarget:", aTargetName); let deferred = Promise.defer(); if (typeof aTargetName != "string" || !aTargetName) { log.warn("getTarget: Invalid target name specified"); deferred.reject("Invalid target name specified"); @@ -948,49 +979,50 @@ this.UITour = { removePinnedTab: function(aWindow) { let tabInfo = this.pinnedTabs.get(aWindow); if (tabInfo) aWindow.gBrowser.removeTab(tabInfo.tab); }, /** + * @param aChromeWindow The chrome window that the highlight is in. Necessary since some targets + * are in a sub-frame so the defaultView is not the same as the chrome + * window. * @param aTarget The element to highlight. * @param aEffect (optional) The effect to use from UITour.highlightEffects or "none". * @see UITour.highlightEffects */ - showHighlight: function(aTarget, aEffect = "none") { - let window = aTarget.node.ownerDocument.defaultView; - + showHighlight: function(aChromeWindow, aTarget, aEffect = "none") { function showHighlightPanel() { if (aTarget.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) { // This won't affect normal higlights done via the panel, so we need to // manually hide those. - this.hideHighlight(window); + this.hideHighlight(aChromeWindow); aTarget.node.setAttribute("_moz-menuactive", true); return; } // Conversely, highlights for search engines are highlighted via CSS // rather than a panel, so need to be manually removed. - this._hideSearchEngineHighlight(window); + this._hideSearchEngineHighlight(aChromeWindow); - let highlighter = aTarget.node.ownerDocument.getElementById("UITourHighlight"); + let highlighter = aChromeWindow.document.getElementById("UITourHighlight"); let effect = aEffect; if (effect == "random") { // Exclude "random" from the randomly selected effects. let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1)); if (randomEffect == this.highlightEffects.length) randomEffect--; // On the order of 1 in 2^62 chance of this happening. effect = this.highlightEffects[randomEffect]; } // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays. highlighter.setAttribute("active", "none"); - aTarget.node.ownerDocument.defaultView.getComputedStyle(highlighter).animationName; + aChromeWindow.getComputedStyle(highlighter).animationName; highlighter.setAttribute("active", effect); highlighter.parentElement.setAttribute("targetName", aTarget.targetName); highlighter.parentElement.hidden = false; let highlightAnchor; // If the target is in the overflow panel, just highlight the overflow button. if (aTarget.node.getAttribute("overflowedItem")) { let doc = aTarget.node.ownerDocument; @@ -1020,17 +1052,17 @@ this.UITour = { // Close a previous highlight so we can relocate the panel. if (highlighter.parentElement.state == "showing" || highlighter.parentElement.state == "open") { log.debug("showHighlight: Closing previous highlight first"); highlighter.parentElement.hidePopup(); } /* The "overlap" position anchors from the top-left but we want to centre highlights at their minimum size. */ - let highlightWindow = aTarget.node.ownerDocument.defaultView; + let highlightWindow = aChromeWindow; let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement); let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop); let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft); let highlightStyle = highlightWindow.getComputedStyle(highlighter); let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight)); let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth)); let offsetX = paddingTopPx - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2); @@ -1041,17 +1073,17 @@ this.UITour = { } // Prevent showing a panel at an undefined position. if (!this.isElementVisible(aTarget.node)) { log.warn("showHighlight: Not showing a highlight since the target isn't visible", aTarget); return; } - this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight", + this._setAppMenuStateForAnnotation(aChromeWindow, "highlight", this.targetIsInAppMenu(aTarget), showHighlightPanel.bind(this)); }, hideHighlight: function(aWindow) { let tabData = this.pinnedTabs.get(aWindow); if (tabData && !tabData.sticky) this.removePinnedTab(aWindow); @@ -1080,31 +1112,32 @@ this.UITour = { for (let menuItem of searchPopup.children) menuItem.removeAttribute("_moz-menuactive"); } }, /** * Show an info panel. * + * @param {ChromeWindow} aChromeWindow * @param {nsIMessageSender} aMessageManager * @param {Node} aAnchor * @param {String} [aTitle=""] * @param {String} [aDescription=""] * @param {String} [aIconURL=""] * @param {Object[]} [aButtons=[]] * @param {Object} [aOptions={}] * @param {String} [aOptions.closeButtonCallbackID] */ - showInfo: function(aMessageManager, aAnchor, aTitle = "", aDescription = "", aIconURL = "", + showInfo: function(aChromeWindow, aMessageManager, aAnchor, aTitle = "", aDescription = "", aIconURL = "", aButtons = [], aOptions = {}) { function showInfoPanel(aAnchorEl) { aAnchorEl.focus(); - let document = aAnchorEl.ownerDocument; + let document = aChromeWindow.document; let tooltip = document.getElementById("UITourTooltip"); let tooltipTitle = document.getElementById("UITourTooltipTitle"); let tooltipDesc = document.getElementById("UITourTooltipDescription"); let tooltipIcon = document.getElementById("UITourTooltipIcon"); let tooltipButtons = document.getElementById("UITourTooltipButtons"); if (tooltip.state == "showing" || tooltip.state == "open") { tooltip.hidePopup(); @@ -1182,25 +1215,27 @@ this.UITour = { document.defaultView.addEventListener("endmodalstate", function endModalStateHandler() { document.defaultView.removeEventListener("endmodalstate", endModalStateHandler); tooltip.openPopup(aAnchorEl, alignment); }, false); } } // Prevent showing a panel at an undefined position. - if (!this.isElementVisible(aAnchor.node)) + if (!this.isElementVisible(aAnchor.node)) { + log.warn("showInfo: Not showing since the target isn't visible", aAnchor); return; + } // Due to a platform limitation, we can't anchor a panel to an element in a // <menupopup>. So we can't support showing info panels for search engines. if (aAnchor.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) return; - this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info", + this._setAppMenuStateForAnnotation(aChromeWindow, "info", this.targetIsInAppMenu(aAnchor), showInfoPanel.bind(this, aAnchor.node)); }, hideInfo: function(aWindow) { let document = aWindow.document; let tooltip = document.getElementById("UITourTooltip"); @@ -1230,18 +1265,19 @@ this.UITour = { } if (aMenuName == "appMenu") { aWindow.PanelUI.panel.setAttribute("noautohide", "true"); // If the popup is already opened, don't recreate the widget as it may cause a flicker. if (aWindow.PanelUI.panel.state != "open") { this.recreatePopup(aWindow.PanelUI.panel); } - aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations); - aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations); + aWindow.PanelUI.panel.addEventListener("popuphiding", this.hideAppMenuAnnotations); + aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hideAppMenuAnnotations); + aWindow.PanelUI.panel.addEventListener("popuphidden", this.onPanelHidden); if (aOpenCallback) { aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown); } aWindow.PanelUI.show(); } else if (aMenuName == "bookmarks") { let menuBtn = aWindow.document.getElementById("bookmarks-menu-button"); openMenuButton(menuBtn); } else if (aMenuName == "loop") { @@ -1249,81 +1285,92 @@ this.UITour = { if (!toolbarButton || !toolbarButton.node) { return; } let panel = aWindow.document.getElementById("loop-notification-panel"); panel.setAttribute("noautohide", true); if (panel.state != "open") { this.recreatePopup(panel); + this.availableTargetsCache.clear(); } // An event object is expected but we don't want to toggle the panel with a click if the panel // is already open. aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }).then(() => { if (aOpenCallback) { aOpenCallback(); } }); - panel.addEventListener("popuphidden", this.onLoopPanelHidden); + panel.addEventListener("popuphidden", this.onPanelHidden); + panel.addEventListener("popuphiding", this.hideLoopPanelAnnotations); } else if (aMenuName == "searchEngines") { this.getTarget(aWindow, "searchProvider").then(target => { openMenuButton(target.node); }).catch(log.error); } }, hideMenu: function(aWindow, aMenuName) { function closeMenuButton(aMenuBtn) { if (aMenuBtn && aMenuBtn.boxObject) aMenuBtn.boxObject.openMenu(false); } if (aMenuName == "appMenu") { - aWindow.PanelUI.panel.removeAttribute("noautohide"); aWindow.PanelUI.hide(); - this.recreatePopup(aWindow.PanelUI.panel); } else if (aMenuName == "bookmarks") { let menuBtn = aWindow.document.getElementById("bookmarks-menu-button"); closeMenuButton(menuBtn); } else if (aMenuName == "loop") { let panel = aWindow.document.getElementById("loop-notification-panel"); panel.hidePopup(); } else if (aMenuName == "searchEngines") { let menuBtn = this.targets.get("searchProvider").query(aWindow.document); closeMenuButton(menuBtn); } }, - hidePanelAnnotations: function(aEvent) { + hideAnnotationsForPanel: function(aEvent, aTargetPositionCallback) { let win = aEvent.target.ownerDocument.defaultView; let annotationElements = new Map([ // [annotationElement (panel), method to hide the annotation] [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)], [win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)], ]); annotationElements.forEach((hideMethod, annotationElement) => { if (annotationElement.state != "closed") { let targetName = annotationElement.getAttribute("targetName"); UITour.getTarget(win, targetName).then((aTarget) => { // Since getTarget is async, we need to make sure that the target hasn't // changed since it may have just moved to somewhere outside of the app menu. if (annotationElement.getAttribute("targetName") != aTarget.targetName || annotationElement.state == "closed" || - !UITour.targetIsInAppMenu(aTarget)) { + !aTargetPositionCallback(aTarget)) { return; } hideMethod(win); }).catch(log.error); } }); UITour.appMenuOpenForAnnotation.clear(); }, - onLoopPanelHidden: function(aEvent) { + hideAppMenuAnnotations: function(aEvent) { + UITour.hideAnnotationsForPanel(aEvent, UITour.targetIsInAppMenu); + }, + + hideLoopPanelAnnotations: function(aEvent) { + UITour.hideAnnotationsForPanel(aEvent, (aTarget) => { + // TODO: Bug 1104927 - Handle the conversation targets separately. + return aTarget.targetName.startsWith("loop-"); + }); + }, + + onPanelHidden: function(aEvent) { aEvent.target.removeAttribute("noautohide"); UITour.recreatePopup(aEvent.target); }, recreatePopup: function(aPanel) { // After changing popup attributes that relate to how the native widget is created // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect. if (aPanel.hidden) {
--- a/browser/modules/test/browser_UITour_loop.js +++ b/browser/modules/test/browser_UITour_loop.js @@ -45,28 +45,72 @@ let tests = [ }, "Menu should be visible after showMenu()"); // Leave it open so it gets torn down and we can test below that teardown was succesful. }), taskify(function* test_menu_cleanup() { // Test that the open menu from above was torn down fully. checkLoopPanelIsHidden(); }), + function test_availableTargets(done) { + gContentAPI.showMenu("loop"); + gContentAPI.getConfiguration("availableTargets", (data) => { + for (let targetName of ["loop-newRoom", "loop-roomList", "loop-signInUpLink"]) { + isnot(data.targets.indexOf(targetName), -1, targetName + " should exist"); + } + done(); + }); + }, + function test_hideMenuHidesAnnotations(done) { + let infoPanel = document.getElementById("UITourTooltip"); + let highlightPanel = document.getElementById("UITourHighlightContainer"); + + gContentAPI.showMenu("loop", function menuCallback() { + gContentAPI.showHighlight("loop-roomList"); + gContentAPI.showInfo("loop-newRoom", "Make a new room", "AKA. conversation"); + UITour.getTarget(window, "loop-newRoom").then((target) => { + waitForPopupAtAnchor(infoPanel, target.node, Task.async(function* checkPanelIsOpen() { + isnot(loopPanel.state, "closed", "Loop panel should still be open"); + ok(loopPanel.hasAttribute("noautohide"), "@noautohide should still be on the loop panel"); + is(highlightPanel.getAttribute("targetName"), "loop-roomList", "Check highlight @targetname"); + is(infoPanel.getAttribute("targetName"), "loop-newRoom", "Check info panel @targetname"); + + info("Close the loop menu and make sure the annotations inside disappear"); + let hiddenPromises = [promisePanelElementHidden(window, infoPanel), + promisePanelElementHidden(window, highlightPanel)]; + gContentAPI.hideMenu("loop"); + yield Promise.all(hiddenPromises); + isnot(infoPanel.state, "open", "Info panel should have automatically hid"); + isnot(highlightPanel.state, "open", "Highlight panel should have automatically hid"); + done(); + }), "Info panel should be anchored to the new room button"); + }); + }); + }, ]; function checkLoopPanelIsHidden() { ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up"); ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen"); isnot(loopPanel.state, "open", "The panel shouldn't be open"); is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed"); } if (Services.prefs.getBoolPref("loop.enabled")) { loopButton = window.LoopUI.toolbarButton.node; + // The targets to highlight only appear after getting started is launched. + Services.prefs.setBoolPref("loop.gettingStarted.seen", true); + Services.prefs.setCharPref("loop.server", "http://localhost"); + Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/"); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("loop.gettingStarted.seen"); + Services.prefs.clearUserPref("loop.server"); + Services.prefs.clearUserPref("services.push.serverURL"); + // Copied from browser/components/loop/test/mochitest/head.js // Remove the iframe after each test. This also avoids mochitest complaining // about leaks on shutdown as we intentionally hold the iframe open for the // life of the application. let frameId = loopButton.getAttribute("notificationFrameId"); let frame = document.getElementById(frameId); if (frame) { frame.remove();