Bug 1401991 - Ensure that we don't hide panelviews that are already reparented to another multi-view and ensure to hide other panels consistently. r?Gijs
* Harden the new `hideAllViewsExcept()` to not do erroneous things if called when
the binding is already gone.
* Generalize things into `hideAllViewsExcept(thisOne)`:
- Clear `_viewShowing` in there and do the descriptionHeightWorkaround thing
in there too,
- For Photon panels, do all the 'current' attribute setting in there. To show
a panel during transition, I introduced the 'in-transition' attribute.
* I had to make sure not to over-eagerly dispatch 'ViewShowing' events, because
that confuses some,
* Move the temporary panel handling, which contains an ephemeral panelmultiview
instance, internally. This cleans up the hacky, duplicate PanelUI.js code nicely.
* Keep a local copy of `_transitionDetails` to ensure it's still there after transition,
* Harden `_cleanupTransitionPhase()` to only clear the phase that belongs to a
specific transition, _if_ that's passed in as an argument. This resolves any
potential raciness that might occur when `showSubView()` is called again mid-transition.
* Skip the UITour element visibility check when it's inside a panelview, because
too many things need to happen and that check is too simple to be useful in
that case.
MozReview-Commit-ID: 5HpJKs1Ny5j
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -85,22 +85,22 @@ panel[hidden] photonpanelmultiview,
panel[hidden] panelview {
-moz-binding: none;
}
.panel-mainview {
transition: transform var(--panelui-subview-transition-duration);
}
-panelview:not([mainview]):not([current]) {
+panelview:not([mainview]):not([current]):not([in-transition]) {
transition: visibility 0s linear var(--panelui-subview-transition-duration);
visibility: collapse;
}
-photonpanelmultiview panelview:not([current]) {
+photonpanelmultiview panelview:not([current]):not([in-transition]) {
transition: none;
visibility: collapse;
}
panelview[mainview] > .panel-header,
panelview:not([title]) > .panel-header {
display: none;
}
--- a/browser/base/content/test/performance/browser_appmenu_reflows.js
+++ b/browser/base/content/test/performance/browser_appmenu_reflows.js
@@ -63,17 +63,17 @@ const EXPECTED_APPMENU_SUBVIEW_REFLOWS =
* correct. Unfortunately this requires 2 sync reflows.
*
* If we add more views where this is necessary, we may need to duplicate
* these expected reflows further.
*/
{
stack: [
"descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
- "_cleanupTransitionPhase@resource:///modules/PanelMultiView.jsm",
+ "hideAllViewsExcept@resource:///modules/PanelMultiView.jsm",
],
times: 2, // This number should only ever go down - never up.
},
/**
* Please don't add anything new!
*/
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -186,16 +186,25 @@ this.PanelMultiView = class {
this.__transitioning = val;
if (val) {
this.node.setAttribute("transitioning", "true");
} else {
this.node.removeAttribute("transitioning");
}
}
+ /**
+ * @return {Boolean} |true| when the 'ephemeral' attribute is set, which means
+ * that this instance should be ready to be thrown away at
+ * any time.
+ */
+ get _ephemeral() {
+ return this.node.hasAttribute("ephemeral");
+ }
+
get panelViews() {
// If there's a dedicated subViews container, we're not in the right binding
// to use SlidingPanelViews.
if (this._subViews)
return null;
if (this._panelViews)
return this._panelViews;
@@ -212,34 +221,40 @@ this.PanelMultiView = class {
}
get _screenManager() {
if (this.__screenManager)
return this.__screenManager;
return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
.getService(Ci.nsIScreenManager);
}
/**
- * Getter that returns the currently visible subview OR the subview that is
- * about to be shown whilst a 'ViewShowing' event is being dispatched.
- *
- * @return {panelview}
+ * @return {panelview} the currently visible subview OR the subview that is
+ * about to be shown whilst a 'ViewShowing' event is being
+ * dispatched.
*/
get current() {
return this._viewShowing || this._currentSubView
}
get _currentSubView() {
return this.panelViews ? this.panelViews.currentView : this.__currentSubView;
}
set _currentSubView(panel) {
if (this.panelViews)
this.panelViews.currentView = panel;
else
this.__currentSubView = panel;
return panel;
}
+ /**
+ * @return {Promise} showSubView() returns a promise, which is kept here for
+ * random access.
+ */
+ get currentShowPromise() {
+ return this._currentShowPromise || Promise.resolve();
+ }
get _keyNavigationMap() {
if (!this.__keyNavigationMap)
this.__keyNavigationMap = new Map();
return this.__keyNavigationMap;
}
get _multiLineElementsMap() {
if (!this.__multiLineElementsMap)
this.__multiLineElementsMap = new WeakMap();
@@ -311,29 +326,33 @@ this.PanelMultiView = class {
});
["goBack", "descriptionHeightWorkaround", "setMainView", "showMainView",
"showSubView"].forEach(method => {
Object.defineProperty(this.node, method, {
enumerable: true,
value: (...args) => this[method](...args)
});
});
- Object.defineProperty(this.node, "current", {
- enumerable: true,
- get: () => this.current
+ ["current", "currentShowPromise"].forEach(property => {
+ Object.defineProperty(this.node, property, {
+ enumerable: true,
+ get: () => this[property]
+ });
});
}
destructor() {
// Guard against re-entrancy.
if (!this.node)
return;
- if (this._mainView) {
- let mainView = this._mainView;
+ if (this._ephemeral)
+ this.hideAllViewsExcept(null);
+ let mainView = this._mainView;
+ if (mainView) {
if (this._panelViewCache)
this._panelViewCache.appendChild(mainView);
mainView.removeAttribute("mainview");
}
if (this._subViews)
this._moveOutKids(this._subViews);
if (this.panelViews) {
@@ -390,20 +409,23 @@ this.PanelMultiView = class {
/**
* Checks whether it is possible to navigate backwards currently. Returns
* false if this is the panelmultiview's mainview, true otherwise.
*
* @param {panelview} view View to check, defaults to the currently active view.
* @return {Boolean}
*/
_canGoBack(view = this._currentSubView) {
- return view != this._mainView;
+ return view.id != this._mainViewId;
}
setMainView(aNewMainView) {
+ if (!aNewMainView)
+ return;
+
if (this._mainView) {
if (!this.panelViews)
this._subViews.appendChild(this._mainView);
this._mainView.removeAttribute("mainview");
}
this._mainViewId = aNewMainView.id;
aNewMainView.setAttribute("mainview", "true");
if (this.panelViews) {
@@ -413,65 +435,88 @@ this.PanelMultiView = class {
this._viewStack.insertBefore(aNewMainView, this._viewStack.firstChild);
}
} else {
this._mainViewContainer.appendChild(aNewMainView);
}
}
showMainView() {
+ if (!this._mainViewId)
+ return Promise.resolve();
+
+ if (this.panelViews)
+ return this.showSubView(this._mainView);
+
if (this.showingSubView) {
let viewNode = this._currentSubView;
this._dispatchViewEvent(viewNode, "ViewHiding");
- if (this.panelViews) {
+ this._transitionHeight(() => {
viewNode.removeAttribute("current");
- this.showSubView(this._mainViewId);
- } else {
- this._transitionHeight(() => {
- viewNode.removeAttribute("current");
- this._currentSubView = null;
- this.node.setAttribute("viewtype", "main");
- });
- }
- } else if (this.panelViews) {
- // Make sure to hide all subviews, except for the mainView.
- let mainView = this._mainView;
- for (let panelview of this._panelViews) {
- if (panelview == mainView)
- panelview.setAttribute("current", true);
- else
- panelview.removeAttribute("current");
- }
- this.node.setAttribute("viewtype", "main");
+ this._currentSubView = null;
+ this.node.setAttribute("viewtype", "main");
+ });
}
- if (!this.panelViews) {
- this._shiftMainView();
+ this._shiftMainView();
+ return Promise.resolve();
+ }
+
+ /**
+ * Ensures that all the panelviews, that are currently part of this instance,
+ * are hidden, except one specifically.
+ *
+ * @param {panelview} [theOne] The panelview DOM node to ensure is visible.
+ * Optional.
+ */
+ hideAllViewsExcept(theOne = null) {
+ for (let panelview of this._panelViews) {
+ // When the panelview was already reparented, don't interfere any more.
+ if (panelview == theOne || !this.node || panelview.panelMultiView != this.node)
+ continue;
+ if (panelview.hasAttribute("current"))
+ this._dispatchViewEvent(panelview, "ViewHiding");
+ panelview.removeAttribute("current");
}
+
+ this._viewShowing = null;
+
+ if (!this.node || !theOne)
+ return;
+
+ this._currentSubView = theOne;
+ if (!theOne.hasAttribute("current")) {
+ theOne.setAttribute("current", true);
+ this.descriptionHeightWorkaround(theOne);
+ this._dispatchViewEvent(theOne, "ViewShown");
+ }
+ this.node.setAttribute("viewtype", (theOne.id == this._mainViewId) ? "main" : "subview");
}
showSubView(aViewId, aAnchor, aPreviousView) {
- return (async () => {
+ this._currentShowPromise = (async () => {
// Support passing in the node directly.
let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
if (!viewNode) {
viewNode = this.document.getElementById(aViewId);
if (viewNode) {
this._placeSubView(viewNode);
} else {
throw new Error(`Subview ${aViewId} doesn't exist!`);
}
} else if (viewNode.parentNode == this._panelViewCache) {
this._placeSubView(viewNode);
}
let reverse = !!aPreviousView;
let previousViewNode = aPreviousView || this._currentSubView;
- let playTransition = (!!previousViewNode && previousViewNode != viewNode &&
- this._panel.state == "open");
+ // If the panelview to show is the same as the previous one, the 'ViewShowing'
+ // event has already been dispatched. Don't do it twice.
+ let showingSameView = viewNode == previousViewNode;
+ let playTransition = (!!previousViewNode && !showingSameView && this._panel.state == "open");
let dwu, previousRect;
if (playTransition || this.panelViews) {
dwu = this._dwu;
previousRect = previousViewNode.__lastKnownBoundingRect =
dwu.getBoundsWithoutFlushing(previousViewNode);
if (this.panelViews) {
// Here go the measures that have the same caching lifetime as the width
@@ -487,88 +532,88 @@ this.PanelMultiView = class {
if (!this._mainViewHeight) {
this._mainViewHeight = previousRect.height;
this._viewContainer.style.minHeight = this._mainViewHeight + "px";
}
}
}
this._viewShowing = viewNode;
+ // Because the 'mainview' attribute may be out-of-sync, due to view node
+ // reparenting in combination with ephemeral PanelMultiView instances,
+ // this is the best place to correct it (just before showing).
+ if (viewNode.id == this._mainViewId)
+ viewNode.setAttribute("mainview", true);
+ else
+ viewNode.removeAttribute("mainview");
// Make sure that new panels always have a title set.
if (this.panelViews && aAnchor) {
if (!viewNode.hasAttribute("title"))
viewNode.setAttribute("title", aAnchor.getAttribute("label"));
viewNode.classList.add("PanelUI-subView");
}
if (this.panelViews && this._mainViewWidth)
viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
- // Emit the ViewShowing event so that the widget definition has a chance
- // to lazily populate the subview with things or perhaps even cancel this
- // whole operation.
- let detail = {
- blockers: new Set(),
- addBlocker(promise) {
- this.blockers.add(promise);
- }
- };
- let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
- if (detail.blockers.size) {
- try {
- let results = await Promise.all(detail.blockers);
- cancel = cancel || results.some(val => val === false);
- } catch (e) {
- Cu.reportError(e);
- cancel = true;
+ if (!showingSameView || !viewNode.hasAttribute("current")) {
+ // Emit the ViewShowing event so that the widget definition has a chance
+ // to lazily populate the subview with things or perhaps even cancel this
+ // whole operation.
+ let detail = {
+ blockers: new Set(),
+ addBlocker(promise) {
+ this.blockers.add(promise);
+ }
+ };
+ let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
+ if (detail.blockers.size) {
+ try {
+ let results = await Promise.all(detail.blockers);
+ cancel = cancel || results.some(val => val === false);
+ } catch (e) {
+ Cu.reportError(e);
+ cancel = true;
+ }
}
- }
- this._viewShowing = null;
- if (cancel) {
- return;
- }
-
- this._currentSubView = viewNode;
- if (this.panelViews) {
- if (viewNode.id == this._mainViewId) {
- this.node.setAttribute("viewtype", "main");
- } else {
- this.node.setAttribute("viewtype", "subview");
- }
- // If we've got an older transition still running, make sure to clean it up.
- await this._cleanupTransitionPhase();
- if (!playTransition) {
- viewNode.setAttribute("current", true);
- this.descriptionHeightWorkaround(viewNode);
+ if (cancel) {
+ this._viewShowing = null;
+ return;
}
}
// Now we have to transition the panel.
- if (this.panelViews && playTransition) {
- await this._transitionViews(previousViewNode, viewNode, reverse, previousRect, aAnchor);
-
- this._dispatchViewEvent(viewNode, "ViewShown");
- this._updateKeyboardFocus(viewNode);
- } else if (!this.panelViews) {
+ if (this.panelViews) {
+ // If we've got an older transition still running, make sure to clean it up.
+ await this._cleanupTransitionPhase();
+ if (playTransition) {
+ await this._transitionViews(previousViewNode, viewNode, reverse, previousRect, aAnchor);
+ this._updateKeyboardFocus(viewNode);
+ } else {
+ this.hideAllViewsExcept(viewNode);
+ }
+ } else {
+ this._currentSubView = viewNode;
this._transitionHeight(() => {
viewNode.setAttribute("current", true);
if (viewNode.id == this._mainViewId) {
this.node.setAttribute("viewtype", "main");
} else {
this.node.setAttribute("viewtype", "subview");
}
// Now that the subview is visible, we can check the height of the
// description elements it contains.
this.descriptionHeightWorkaround(viewNode);
this._dispatchViewEvent(viewNode, "ViewShown");
});
this._shiftMainView(aAnchor);
}
})().catch(e => Cu.reportError(e));
+ return this._currentShowPromise;
}
/**
* Apply a transition to 'slide' from the currently active view to the next
* one.
* Sliding the next subview in means that the previous panelview stays where it
* is and the active panelview slides in from the left in LTR mode, right in
* RTL mode.
@@ -591,65 +636,69 @@ this.PanelMultiView = class {
return;
}
const {window, document} = this;
if (this._autoResizeWorkaroundTimer)
window.clearTimeout(this._autoResizeWorkaroundTimer);
- this._transitionDetails = {
+ let details = this._transitionDetails = {
phase: TRANSITION_PHASES.START,
previousViewNode, viewNode, reverse, anchor
};
if (anchor)
anchor.setAttribute("open", "true");
+ // Since we're going to show two subview at the same time, don't abuse the
+ // 'current' attribute, since it's needed for other state-keeping, but use
+ // a separate 'in-transition' attribute instead.
+ previousViewNode.setAttribute("in-transition", true);
// Set the viewContainer dimensions to make sure only the current view is
// visible.
this._viewContainer.style.height = Math.max(previousRect.height, this._mainViewHeight) + "px";
this._viewContainer.style.width = previousRect.width + "px";
// Lock the dimensions of the window that hosts the popup panel.
let rect = this._panel.popupBoxObject.getOuterScreenRect();
this._panel.setAttribute("width", rect.width);
this._panel.setAttribute("height", rect.height);
let viewRect;
if (viewNode.__lastKnownBoundingRect) {
viewRect = viewNode.__lastKnownBoundingRect;
- viewNode.setAttribute("current", true);
+ viewNode.setAttribute("in-transition", true);
} else if (viewNode.customRectGetter) {
// Can't use Object.assign directly with a DOM Rect object because its properties
// aren't enumerable.
let {height, width} = previousRect;
viewRect = Object.assign({height, width}, viewNode.customRectGetter());
let {header} = viewNode;
if (header) {
viewRect.height += this._dwu.getBoundsWithoutFlushing(header).height;
}
- viewNode.setAttribute("current", true);
+ viewNode.setAttribute("in-transition", true);
} else {
let oldSibling = viewNode.nextSibling || null;
this._offscreenViewStack.appendChild(viewNode);
- viewNode.setAttribute("current", true);
+ viewNode.setAttribute("in-transition", true);
viewRect = await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
return this._dwu.getBoundsWithoutFlushing(viewNode);
});
try {
this._viewStack.insertBefore(viewNode, oldSibling);
} catch (ex) {
this._viewStack.appendChild(viewNode);
}
}
this._transitioning = true;
- this._transitionDetails.phase = TRANSITION_PHASES.PREPARE;
+ details.phase = TRANSITION_PHASES.PREPARE;
// The 'magic' part: build up the amount of pixels to move right or left.
let moveToLeft = (this._dir == "rtl" && !reverse) || (this._dir == "ltr" && reverse);
let deltaX = previousRect.width;
let deepestNode = reverse ? previousViewNode : viewNode;
// With a transition when navigating backwards - user hits the 'back'
// button - we need to make sure that the views are positioned in a way
@@ -674,57 +723,58 @@ this.PanelMultiView = class {
this._panel.removeAttribute("height");
// We're setting the width property to prevent flickering during the
// sliding animation with smaller views.
viewNode.style.width = viewRect.width + "px";
await BrowserUtils.promiseLayoutFlushed(document, "layout", () => {});
// Kick off the transition!
- this._transitionDetails.phase = TRANSITION_PHASES.TRANSITION;
+ details.phase = TRANSITION_PHASES.TRANSITION;
this._viewStack.style.transform = "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
await new Promise(resolve => {
- this._transitionDetails.resolve = resolve;
- this._viewContainer.addEventListener("transitionend", this._transitionDetails.listener = ev => {
+ details.resolve = resolve;
+ this._viewContainer.addEventListener("transitionend", details.listener = ev => {
// It's quite common that `height` on the view container doesn't need
// to transition, so we make sure to do all the work on the transform
// transition-end, because that is guaranteed to happen.
if (ev.target != this._viewStack || ev.propertyName != "transform")
return;
- this._viewContainer.removeEventListener("transitionend", this._transitionDetails.listener);
- delete this._transitionDetails.listener;
+ this._viewContainer.removeEventListener("transitionend", details.listener);
+ delete details.listener;
resolve();
});
});
- this._transitionDetails.phase = TRANSITION_PHASES.END;
+ details.phase = TRANSITION_PHASES.END;
- await this._cleanupTransitionPhase();
+ await this._cleanupTransitionPhase(details);
}
/**
* Attempt to clean up the attributes and properties set by `_transitionViews`
* above. Which attributes and properties depends on the phase the transition
* was left from - normally that'd be `TRANSITION_PHASES.END`.
*/
- async _cleanupTransitionPhase() {
- if (!this._transitionDetails)
+ async _cleanupTransitionPhase(details = this._transitionDetails) {
+ // Make sure to only clean up a phase from the most recent transition.
+ if (!this._transitionDetails || details != this._transitionDetails)
return;
let {phase, previousViewNode, viewNode, reverse, resolve, listener, anchor} = this._transitionDetails;
this._transitionDetails = null;
// Do the things we _always_ need to do whenever the transition ends or is
// interrupted.
- this._dispatchViewEvent(previousViewNode, "ViewHiding");
- previousViewNode.removeAttribute("current");
+ this.hideAllViewsExcept(viewNode);
+ previousViewNode.removeAttribute("in-transition");
+ viewNode.removeAttribute("in-transition");
if (reverse)
this._resetKeyNavigation(previousViewNode);
- this.descriptionHeightWorkaround(viewNode);
if (anchor)
anchor.removeAttribute("open");
if (phase >= TRANSITION_PHASES.START) {
this._panel.removeAttribute("width");
this._panel.removeAttribute("height");
// Myeah, panel layout auto-resizing is a funky thing. We'll wait
@@ -910,17 +960,17 @@ this.PanelMultiView = class {
}
break;
case "keydown":
this._keyNavigation(aEvent);
break;
case "mousemove":
this._resetKeyNavigation();
break;
- case "popupshowing":
+ case "popupshowing": {
this.node.setAttribute("panelopen", "true");
// Bug 941196 - The panel can get taller when opening a subview. Disabling
// autoPositioning means that the panel won't jump around if an opened
// subview causes the panel to exceed the dimensions of the screen in the
// direction that the panel originally opened in. This property resets
// every time the popup closes, which is why we have to set it each time.
this._panel.autoPosition = false;
if (this.panelViews && !this.node.hasAttribute("disablekeynav")) {
@@ -966,44 +1016,47 @@ this.PanelMultiView = class {
this._viewStack.style.maxHeight = maxHeight + "px";
// When using block-in-box layout inside a scrollable frame, like in the
// main menu contents scroller, if we allow the contents to scroll then
// it will not cause its container to expand. Thus, we layout first
// without any scrolling (using "display: flex;"), and only if the view
// exceeds the available space we set the height explicitly and enable
// scrolling.
- if (this._mainView.hasAttribute("blockinboxworkaround")) {
+ let mainView = this._mainView;
+ if (mainView && mainView.hasAttribute("blockinboxworkaround")) {
let blockInBoxWorkaround = () => {
let mainViewHeight =
- this._dwu.getBoundsWithoutFlushing(this._mainView).height;
+ this._dwu.getBoundsWithoutFlushing(mainView).height;
if (mainViewHeight > maxHeight) {
- this._mainView.style.height = maxHeight + "px";
- this._mainView.setAttribute("exceeding", "true");
+ mainView.style.height = maxHeight + "px";
+ mainView.setAttribute("exceeding", "true");
}
};
// On Windows, we cannot measure the full height of the main view
// until it is visible. Unfortunately, this causes a visible jump when
// the view needs to scroll, but there is no easy way around this.
if (AppConstants.platform == "win") {
// We register a "once" listener so we don't need to store the value
// of maxHeight elsewhere on the object.
this._panel.addEventListener("popupshown", blockInBoxWorkaround,
{ once: true });
} else {
blockInBoxWorkaround();
}
}
break;
+ }
case "popupshown":
// Now that the main view is visible, we can check the height of the
// description elements it contains.
- this.descriptionHeightWorkaround();
+ if (!this.panelViews)
+ this.descriptionHeightWorkaround();
break;
- case "popuphidden":
+ case "popuphidden": {
// WebExtensions consumers can hide the popup from viewshowing, or
// mid-transition, which disrupts our state:
this._viewShowing = null;
this._transitioning = false;
this.node.removeAttribute("panelopen");
this.showMainView();
if (this.panelViews) {
for (let panelView of this._viewStack.children) {
@@ -1024,22 +1077,24 @@ this.PanelMultiView = class {
this._viewContainer.style.removeProperty("min-height");
this._viewStack.style.removeProperty("max-height");
this._viewContainer.style.removeProperty("min-width");
this._viewContainer.style.removeProperty("max-width");
}
// Always try to layout the panel normally when reopening it. This is
// also the layout that will be used in customize mode.
- if (this._mainView.hasAttribute("blockinboxworkaround")) {
- this._mainView.style.removeProperty("height");
- this._mainView.removeAttribute("exceeding");
+ let mainView = this._mainView;
+ if (mainView && mainView.hasAttribute("blockinboxworkaround")) {
+ mainView.style.removeProperty("height");
+ mainView.removeAttribute("exceeding");
}
this._dispatchViewEvent(this.node, "PanelMultiViewHidden");
break;
+ }
}
}
/**
* Based on going up or down, select the previous or next focusable button
* in the current view.
*
* @param {Object} navMap the navigation keyboard map object for the view
@@ -1252,17 +1307,17 @@ this.PanelMultiView = class {
*
* This may trigger a synchronous layout.
*
* @param viewNode
* Indicates the node to scan for descendant elements. This is the main
* view if omitted.
*/
descriptionHeightWorkaround(viewNode = this._mainView) {
- if (!viewNode.hasAttribute("descriptionheightworkaround")) {
+ if (!viewNode || !viewNode.hasAttribute("descriptionheightworkaround")) {
// This view does not require the workaround.
return;
}
// We batch DOM changes together in order to reduce synchronous layouts.
// First we reset any change we may have made previously. The first time
// this is called, and in the best case scenario, this has no effect.
let items = [];
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -414,79 +414,65 @@ const PanelUI = {
tempPanel.setAttribute("viewId", aViewId);
if (aAnchor.getAttribute("tabspecific")) {
tempPanel.setAttribute("tabspecific", true);
}
if (this._disableAnimations) {
tempPanel.setAttribute("animate", "false");
}
tempPanel.setAttribute("context", "");
+ tempPanel.setAttribute("photon", true);
document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
// If the view has a footer, set a convenience class on the panel.
tempPanel.classList.toggle("cui-widget-panelWithFooter",
viewNode.querySelector(".panel-subview-footer"));
+ // If the panelview is already selected in another PanelMultiView instance
+ // as a subview, make sure to properly hide it there.
+ let oldMultiView = viewNode.panelMultiView;
+ if (oldMultiView && oldMultiView.current == viewNode) {
+ await oldMultiView.showMainView();
+ }
+
+ let viewShown = false;
+ let listener = () => viewShown = true;
+ viewNode.addEventListener("ViewShown", listener, {once: true});
+
let multiView = document.createElement("photonpanelmultiview");
multiView.setAttribute("id", "customizationui-widget-multiview");
- multiView.setAttribute("nosubviews", "true");
multiView.setAttribute("viewCacheId", "appMenu-viewCache");
- tempPanel.setAttribute("photon", true);
multiView.setAttribute("mainViewId", viewNode.id);
- multiView.appendChild(viewNode);
+ multiView.setAttribute("ephemeral", true);
+ document.getElementById("appMenu-viewCache").appendChild(viewNode);
tempPanel.appendChild(multiView);
viewNode.classList.add("cui-widget-panelview");
- let viewShown = false;
let panelRemover = () => {
viewNode.classList.remove("cui-widget-panelview");
if (viewShown) {
CustomizableUI.removePanelCloseListeners(tempPanel);
tempPanel.removeEventListener("popuphidden", panelRemover);
-
- let currentView = multiView.current || viewNode;
- let evt = new CustomEvent("ViewHiding", {detail: currentView});
- currentView.dispatchEvent(evt);
}
aAnchor.open = false;
// Ensure we run the destructor:
multiView.instance.destructor();
tempPanel.remove();
};
- // Emit the ViewShowing event so that the widget definition has a chance
- // to lazily populate the subview with things.
- let detail = {
- blockers: new Set(),
- addBlocker(aPromise) {
- this.blockers.add(aPromise);
- },
- };
-
- let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
- viewNode.dispatchEvent(evt);
+ // Wait until all the tasks needed to show a view are done.
+ await multiView.currentShowPromise;
- let cancel = evt.defaultPrevented;
- if (detail.blockers.size) {
- try {
- let results = await Promise.all(detail.blockers);
- cancel = cancel || results.some(val => val === false);
- } catch (e) {
- Components.utils.reportError(e);
- cancel = true;
- }
- }
-
- if (cancel) {
+ if (!viewShown) {
+ viewNode.removeEventListener("ViewShown", listener);
panelRemover();
return;
}
- viewShown = true;
CustomizableUI.addPanelCloseListeners(tempPanel);
tempPanel.addEventListener("popuphidden", panelRemover);
let anchor = this._getPanelAnchor(aAnchor);
if (aAnchor != anchor && aAnchor.id) {
anchor.setAttribute("consumeanchor", aAnchor.id);
}
@@ -534,17 +520,18 @@ const PanelUI = {
let highlights = await NewTabUtils.activityStreamLinks.getHighlights({
// As per bug 1402023, hard-coded limit, until Activity Stream develops a
// richer list.
numItems: 6,
withFavicons: true
});
// If there's nothing to display, or the panel is already hidden, get out.
- if (!highlights.length || viewNode.panelMultiView.getAttribute("panelopen") != "true") {
+ let multiView = viewNode.panelMultiView;
+ if (!highlights.length || (multiView && multiView.getAttribute("panelopen") != "true")) {
this._loadingRecentHighlights = false;
return;
}
container.hidden = container.previousSibling.hidden =
container.previousSibling.previousSibling.hidden = false;
let fragment = document.createDocumentFragment();
for (let highlight of highlights) {
--- a/browser/components/customizableui/test/browser_981305_separator_insertion.js
+++ b/browser/components/customizableui/test/browser_981305_separator_insertion.js
@@ -30,39 +30,40 @@ function checkSeparatorInsertion(menuId,
insertTempItemsIntoMenu(menu);
CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
await waitForOverflowButtonShown();
await document.getElementById("nav-bar").overflowable.show();
+ let subview = document.getElementById(subviewId);
let button = document.getElementById(buttonId);
button.click();
+ await BrowserTestUtils.waitForEvent(subview, "ViewShown");
- await BrowserTestUtils.waitForEvent(PanelUI.overflowPanel, "ViewShown");
- let subview = document.getElementById(subviewId);
- ok(subview.firstChild, "Subview should have a kid");
- is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
+ let subviewBody = subview.firstChild;
+ ok(subviewBody.firstChild, "Subview should have a kid");
+ is(subviewBody.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
- for (let kid of subview.children) {
+ for (let kid of subviewBody.children) {
if (kid.localName == "menuseparator") {
ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator",
"Separators should never have another separator next to them, and should never be the first node.");
}
}
let panelHiddenPromise = promiseOverflowHidden(window);
PanelUI.overflowPanel.hidePopup();
await panelHiddenPromise;
CustomizableUI.reset();
};
}
-add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developerItems"));
+add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developer"));
registerCleanupFunction(function() {
for (let el of tempElements) {
el.remove();
}
tempElements = null;
});
--- a/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js
+++ b/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js
@@ -3,89 +3,62 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
const kWidgetId = "test-981418-widget-onbeforecreated";
// Should be able to add broken view widget
add_task(async function testAddOnBeforeCreatedWidget() {
- let viewShownDeferred = Promise.defer();
let onBeforeCreatedCalled = false;
let widgetSpec = {
id: kWidgetId,
type: "view",
viewId: kWidgetId + "idontexistyet",
onBeforeCreated(doc) {
let view = doc.createElement("panelview");
view.id = kWidgetId + "idontexistyet";
- document.getElementById("PanelUI-multiView").appendChild(view);
+ document.getElementById("appMenu-viewCache").appendChild(view);
onBeforeCreatedCalled = true;
- },
- onViewShowing() {
- viewShownDeferred.resolve();
}
};
- let noError = true;
- try {
- CustomizableUI.createWidget(widgetSpec);
- CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
- } catch (ex) {
- Cu.reportError(ex);
- noError = false;
- }
- ok(noError, "Should not throw an exception trying to add the widget.");
+ CustomizableUI.createWidget(widgetSpec);
+ CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
+
ok(onBeforeCreatedCalled, "onBeforeCreated should have been called");
let widgetNode = document.getElementById(kWidgetId);
+ let viewNode = document.getElementById(kWidgetId + "idontexistyet");
ok(widgetNode, "Widget should exist");
- if (widgetNode) {
- try {
- widgetNode.click();
-
- let tempPanel = document.getElementById("customizationui-widget-panel");
- let panelShownPromise = promisePanelElementShown(window, tempPanel);
+ ok(viewNode, "Panelview should exist");
+ widgetNode.click();
- let shownTimeout = setTimeout(() => viewShownDeferred.reject("Panel not shown within 20s"), 20000);
- await viewShownDeferred.promise;
- await panelShownPromise;
- clearTimeout(shownTimeout);
- ok(true, "Found view shown");
+ let tempPanel = document.getElementById("customizationui-widget-panel");
+ let panelShownPromise = promisePanelElementShown(window, tempPanel);
- let panelHiddenPromise = promisePanelElementHidden(window, tempPanel);
- tempPanel.hidePopup();
- await panelHiddenPromise;
-
- CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
- await waitForOverflowButtonShown();
- await document.getElementById("nav-bar").overflowable.show();
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(viewNode, "ViewShown"),
+ panelShownPromise
+ ]);
- viewShownDeferred = Promise.defer();
- widgetNode.click();
+ let panelHiddenPromise = promisePanelElementHidden(window, tempPanel);
+ tempPanel.hidePopup();
+ await panelHiddenPromise;
- shownTimeout = setTimeout(() => viewShownDeferred.reject("Panel not shown within 20s"), 20000);
- await viewShownDeferred.promise;
- clearTimeout(shownTimeout);
- ok(true, "Found view shown");
+ CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
+ await waitForOverflowButtonShown();
+ await document.getElementById("nav-bar").overflowable.show();
- let panelHidden = promiseOverflowHidden(window);
- PanelUI.overflowPanel.hidePopup();
- await panelHidden;
- } catch (ex) {
- ok(false, "Unexpected exception (like a timeout for one of the yields) " +
- "when testing view widget.");
- }
- }
+ widgetNode.click();
+
+ await BrowserTestUtils.waitForEvent(viewNode, "ViewShown");
- noError = true;
- try {
- CustomizableUI.destroyWidget(kWidgetId);
- } catch (ex) {
- Cu.reportError(ex);
- noError = false;
- }
- ok(noError, "Should not throw an exception trying to remove the broken view widget.");
+ let panelHidden = promiseOverflowHidden(window);
+ PanelUI.overflowPanel.hidePopup();
+ await panelHidden;
+
+ CustomizableUI.destroyWidget(kWidgetId);
});
add_task(async function asyncCleanup() {
await resetCustomization();
});
--- a/browser/components/customizableui/test/browser_remote_tabs_button.js
+++ b/browser/components/customizableui/test/browser_remote_tabs_button.js
@@ -2,17 +2,17 @@
* 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/.
*/
"use strict";
let syncService = {};
Components.utils.import("resource://services-sync/service.js", syncService);
const service = syncService.Service;
-Components.utils.import("resource://services-sync/UIState.jsm");
+const {UIState} = Components.utils.import("resource://services-sync/UIState.jsm", {});
let getState;
let originalSync;
let syncWasCalled = false;
// TODO: This test should probably be re-written, we don't really test much here.
add_task(async function testSyncRemoteTabsButtonFunctionality() {
info("Test the Sync Remote Tabs button in the panel");
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -311,22 +311,22 @@ function isOverflowOpen() {
}
function subviewShown(aSubview) {
return new Promise((resolve, reject) => {
let win = aSubview.ownerGlobal;
let timeoutId = win.setTimeout(() => {
reject("Subview (" + aSubview.id + ") did not show within 20 seconds.");
}, 20000);
- function onViewShowing(e) {
- aSubview.removeEventListener("ViewShowing", onViewShowing);
+ function onViewShown(e) {
+ aSubview.removeEventListener("ViewShown", onViewShown);
win.clearTimeout(timeoutId);
resolve();
}
- aSubview.addEventListener("ViewShowing", onViewShowing);
+ aSubview.addEventListener("ViewShown", onViewShown);
});
}
function subviewHidden(aSubview) {
return new Promise((resolve, reject) => {
let win = aSubview.ownerGlobal;
let timeoutId = win.setTimeout(() => {
reject("Subview (" + aSubview.id + ") did not hide within 20 seconds.");
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -145,17 +145,17 @@ this.browserAction = class extends Exten
localized: false,
onBeforeCreated: document => {
let view = document.createElementNS(XUL_NS, "panelview");
view.id = this.viewId;
view.setAttribute("flex", "1");
view.setAttribute("extension", true);
- document.getElementById("PanelUI-multiView").appendChild(view);
+ document.getElementById("appMenu-viewCache").appendChild(view);
if (this.extension.hasPermission("menus") ||
this.extension.hasPermission("contextMenus")) {
document.addEventListener("popupshowing", this);
}
},
onDestroyed: document => {
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -1073,25 +1073,26 @@ this.UITour = {
*/
async _ensureTarget(aChromeWindow, aTarget) {
let shouldOpenAppMenu = false;
let shouldOpenPageActionPanel = false;
if (this.targetIsInAppMenu(aTarget)) {
shouldOpenAppMenu = true;
} else if (this.targetIsInPageActionPanel(aTarget)) {
shouldOpenPageActionPanel = true;
- // Ensure the panel visibility so as to ensure the visibility of
- // the target element inside the panel otherwise
- // we would be rejected in the below `isElementVisible` checking.
+ // Ensure the panel visibility so as to ensure the visibility of the target
+ // element inside the panel otherwise we would be rejected in the below
+ // `isElementVisible` checking.
aChromeWindow.BrowserPageActions.panelNode.hidden = false;
}
- // Prevent showing a panel at an undefined position.
- if (!this.isElementVisible(aTarget.node)) {
- return Promise.reject(`_ensureTarget: Reject the ${aTarget.name} target since it isn't visible.`);
+ // Prevent showing a panel at an undefined position, but when it's tucked
+ // away inside a panel, we skip this check.
+ if (!aTarget.node.closest("panelview") && !this.isElementVisible(aTarget.node)) {
+ return Promise.reject(`_ensureTarget: Reject the ${aTarget.name || aTarget.targetName} target since it isn't visible.`);
}
let menuToOpen = null;
let menuClosePromises = [];
if (shouldOpenAppMenu) {
menuToOpen = "appMenu";
menuClosePromises.push(this._setMenuStateForAnnotation(aChromeWindow, false, "pageActionPanel"));
} else if (shouldOpenPageActionPanel) {
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -189,20 +189,16 @@
.panel-viewstack[viewtype="main"] > .panel-subviews {
transform: translateX(@menuPanelWidth@);
}
.panel-viewstack[viewtype="main"] > .panel-subviews:-moz-locale-dir(rtl) {
transform: translateX(-@menuPanelWidth@);
}
-panelmultiview[nosubviews=true] > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
- display: none;
-}
-
panelview {
-moz-box-orient: vertical;
-moz-box-flex: 1;
}
.panel-subview-body {
overflow-y: auto;
overflow-x: hidden;