Bug 1386316 - Resize Android WebExtension Options UI iframe to match its content size.
MozReview-Commit-ID: 17a240drasZ
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -140,16 +140,20 @@ function onPopState(aEvent) {
function showAddons() {
// Hide the addon options and show the addons list
let details = document.querySelector("#addons-details");
details.classList.add("hidden");
let list = document.querySelector("#addons-list");
list.classList.remove("hidden");
document.documentElement.removeAttribute("details");
+
+ // Clean the optionsBox content when switching to the add-ons list view.
+ let optionsBox = document.querySelector("#addons-details > .addon-item .options-box");
+ optionsBox.innerHTML = "";
}
function showAddonOptions() {
// Hide the addon list and show the addon options
let list = document.querySelector("#addons-list");
list.classList.add("hidden");
let details = document.querySelector("#addons-details");
details.classList.remove("hidden");
@@ -354,35 +358,70 @@ var Addons = {
let optionsURL = aListItem.getAttribute("optionsURL");
// Always clean the options content before rendering the options of the
// newly selected extension.
optionsBox.innerHTML = "";
switch (parseInt(addon.optionsType)) {
case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
+ // Allow the options to use all the available width space.
+ optionsBox.classList.remove("inner");
+
// WebExtensions are loaded asynchronously and the optionsURL
// may not be available via listitem when the add-on has just been
// installed, but it is available on the addon if one is set.
detailItem.setAttribute("optionsURL", addon.optionsURL);
this.createWebExtensionOptions(optionsBox, addon.optionsURL, addon.optionsBrowserStyle);
break;
case AddonManager.OPTIONS_TYPE_INLINE:
+ // Keep the usual layout for any options related the legacy (or system) add-ons.
+ optionsBox.classList.add("inner");
+
this.createInlineOptions(optionsBox, optionsURL, aListItem);
break;
}
showAddonOptions();
},
createWebExtensionOptions: async function(destination, optionsURL, browserStyle) {
+ let originalHeight;
let frame = document.createElement("iframe");
frame.setAttribute("id", "addon-options");
frame.setAttribute("mozbrowser", "true");
+ frame.setAttribute("style", "width: 100%; overflow: hidden;");
+
+ // Adjust iframe height to the iframe content (also between navigation of multiple options
+ // files).
+ frame.onload = (evt) => {
+ if (evt.target !== frame) {
+ return;
+ }
+
+ const {document} = frame.contentWindow;
+ const bodyScrollHeight = document.body && document.body.scrollHeight;
+ const documentScrollHeight = document.documentElement.scrollHeight;
+
+ // Set the iframe height to the maximum between the body and the document
+ // scrollHeight values.
+ frame.style.height = Math.max(bodyScrollHeight, documentScrollHeight) + "px";
+
+ // Restore the original iframe height between option page loads,
+ // so that we don't force the new document to have the same size
+ // of the previosuly loaded option page.
+ frame.contentWindow.addEventListener("unload", () => {
+ frame.style.height = originalHeight + "px";
+ }, {once: true});
+ };
+
destination.appendChild(frame);
+
+ originalHeight = frame.getBoundingClientRect().height;
+
// Loading the URL this way prevents the native back
// button from applying to the iframe.
frame.contentWindow.location.replace(optionsURL);
},
createInlineOptions(destination, optionsURL, aListItem) {
// This function removes and returns the text content of aNode without
// removing any child elements. Removing the text nodes ensures any XBL
--- a/mobile/android/chrome/content/aboutAddons.xhtml
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -39,19 +39,19 @@
<div id="addons-details" class="list hidden">
<div class="addon-item list-item">
<img class="icon"/>
<div class="inner">
<div class="details">
<div class="title"></div><div class="version"></div>
</div>
<div class="description-full"></div>
- <div class="options-box"></div>
</div>
<div class="warn-unsigned">&addonUnsigned.message; <a id="unsigned-learn-more">&addonUnsigned.learnMore;</a></div>
+ <div class="options-box"></div>
<div class="status status-uninstalled show-on-uninstall"></div>
<div class="buttons">
<button id="enable-btn" class="show-on-disable hide-on-enable hide-on-uninstall" >&addonAction.enable;</button>
<button id="disable-btn" class="show-on-enable hide-on-disable hide-on-uninstall" >&addonAction.disable;</button>
<button id="uninstall-btn" class="hide-on-uninstall" >&addonAction.uninstall;</button>
<button id="cancel-btn" class="show-on-uninstall" >&addonAction.undo;</button>
</div>
</div>
--- a/mobile/android/components/extensions/test/mochitest/chrome.ini
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -3,11 +3,12 @@ support-files =
head.js
../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js
tags = webextensions
[test_ext_browserAction_getTitle_setTitle.html]
[test_ext_browserAction_onClicked.html]
[test_ext_browsingData_cookies_cache.html]
[test_ext_browsingData_settings.html]
+[test_ext_options_ui.html]
[test_ext_pageAction_show_hide.html]
[test_ext_pageAction_getPopup_setPopup.html]
skip-if = os == 'android' # bug 1373170
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
@@ -0,0 +1,196 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PageAction Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://testing-common/ContentTask.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+async function waitAboutAddonsRendered(addonId) {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document.querySelector(`div.addon-item[addonID="${addonId}"]`);
+ }, `wait Addon Item for ${addonId} to be rendered`);
+}
+
+async function navigateToAddonDetails(addonId) {
+ const item = content.document.querySelector(`div.addon-item[addonID="${addonId}"]`);
+ let rect = item.getBoundingClientRect();
+ const x = rect.left + rect.width / 2;
+ const y = rect.top + rect.height / 2;
+ let domWinUtils = content.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ domWinUtils.sendMouseEventToWindow("mousedown", x, y, 0, 1, 0);
+ domWinUtils.sendMouseEventToWindow("mouseup", x, y, 0, 1, 0);
+}
+
+async function waitAddonOptionsPage([addonId, expectedText]) {
+ await ContentTaskUtils.waitForCondition(() => {
+ const optionsIframe = content.document.querySelector(`#addon-options`);
+ return optionsIframe && optionsIframe.contentDocument.readyState === "complete" &&
+ optionsIframe.contentDocument.body.innerText.includes(expectedText);
+ }, `wait Addon Options ${expectedText} for ${addonId} to be loaded`);
+
+ const optionsIframe = content.document.querySelector(`#addon-options`);
+
+ return {
+ iframeHeight: optionsIframe.style.height,
+ documentHeight: optionsIframe.contentDocument.documentElement.scrollHeight,
+ bodyHeight: optionsIframe.contentDocument.body.scrollHeight,
+ };
+}
+
+async function clickOnLinkInOptionsPage(selector) {
+ const optionsIframe = content.document.querySelector(`#addon-options`);
+ optionsIframe.contentDocument.querySelector(selector).click();
+}
+
+async function navigateBack() {
+ content.window.history.back();
+}
+
+add_task(async function test_options_ui_iframe_height() {
+ let addonID = "test-options-ui@mozilla.org";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {id: addonID},
+ },
+ name: "Options UI Extension",
+ description: "Longer addon description",
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ // An option page with the document element bigger than the body.
+ "options.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ html { height: 500px; border: 1px solid black; }
+ body { height: 200px; }
+ </style>
+ </head>
+ <body>
+ <h1>Options page 1</h1>
+ <a href="options2.html">go to page 2</a>
+ </body>
+ </html>
+ `,
+ // A second option page with the body element bigger than the document.
+ "options2.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ html { height: 200px; border: 1px solid black; }
+ body { height: 350px; }
+ </style>
+ </head>
+ <body>
+ <h1>Options page 2</h1>
+ </body>
+ </html>
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ let waitAboutAddonsLoaded = new Promise(resolve => {
+ let listener = (event) => {
+ if (event.target.defaultView.location.href === "about:addons") {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", listener);
+ resolve();
+ }
+ };
+
+ BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
+ });
+
+ BrowserApp.selectOrAddTab("about:addons", {
+ selected: true,
+ parentId: BrowserApp.selectedTab.id,
+ });
+
+ await waitAboutAddonsLoaded;
+
+ is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
+ "about:addons is the currently selected tab");
+
+ let aboutAddonsWindow = BrowserApp.selectedTab.browser.contentWindow;
+
+ is(aboutAddonsWindow.location.href, "about:addons");
+
+ await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, waitAboutAddonsRendered);
+
+ await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, navigateToAddonDetails);
+
+ const optionsSizes = await ContentTask.spawn(
+ BrowserApp.selectedTab.browser, [addonID, "Options page 1"], waitAddonOptionsPage
+ );
+
+ ok(parseInt(optionsSizes.iframeHeight, 10) >= 500,
+ "The addon options iframe is at least 500px");
+
+ is(optionsSizes.iframeHeight, optionsSizes.documentHeight + "px",
+ "The addon options iframe has the expected height");
+
+ await ContentTask.spawn(BrowserApp.selectedTab.browser, "a", clickOnLinkInOptionsPage);
+
+ const options2Sizes = await ContentTask.spawn(
+ BrowserApp.selectedTab.browser, [addonID, "Options page 2"], waitAddonOptionsPage
+ );
+
+ // The second option page has a body bigger than the document element
+ // and we expect the iframe to be bigger than that.
+ ok(parseInt(options2Sizes.iframeHeight, 10) > 200,
+ `The iframe is bigger then 200px (${options2Sizes.iframeHeight})`);
+
+ // The second option page has a body smaller than the document element of the first
+ // page and we expect the iframe to be smaller than for the previous options page.
+ ok(parseInt(options2Sizes.iframeHeight, 10) < 500,
+ `The iframe is smaller then 500px (${options2Sizes.iframeHeight})`);
+
+ is(options2Sizes.iframeHeight, options2Sizes.documentHeight + "px",
+ "The second addon options page has the expected height");
+
+ await ContentTask.spawn(BrowserApp.selectedTab.browser, null, navigateBack);
+
+ const backToOptionsSizes = await ContentTask.spawn(
+ BrowserApp.selectedTab.browser, [addonID, "Options page 1"], waitAddonOptionsPage
+ );
+
+ // After going back to the first options page,
+ // we expect the iframe to have the same size of the previous load.
+ is(backToOptionsSizes.iframeHeight, optionsSizes.iframeHeight,
+ `When navigating back, the old iframe size is restored (${backToOptionsSizes.iframeHeight})`);
+
+ BrowserApp.closeTab(BrowserApp.selectedTab);
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
--- a/mobile/android/themes/core/aboutAddons.css
+++ b/mobile/android/themes/core/aboutAddons.css
@@ -44,16 +44,19 @@ a:active {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.warn-unsigned {
border-top: 1px solid var(--color_about_item_border);
+ border-bottom: 1px solid var(--color_about_item_border);
+ margin-top: 1em;
+ margin-bottom: 1em;
padding: 1em;
padding-inline-start: calc(var(--icon-size) + var(--icon-margin) * 2);
background-image: url("chrome://browser/skin/images/grey-caution.svg");
background-size: var(--icon-size);
background-position: var(--icon-margin);
background-repeat: no-repeat;
display: none;
}