Bug 1550526 - Create deck with tabs for HTML about:addons details r=robwu,jaws

Differential Revision: https://phabricator.services.mozilla.com/D29895

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Striemer 2019-05-13 19:05:26 +00:00
Родитель f71d78cf3c
Коммит 4bd67d819d
5 изменённых файлов: 411 добавлений и 0 удалений

Просмотреть файл

@ -8,6 +8,7 @@
<link rel="localization" href="toolkit/about/aboutAddons.ftl">
<link rel="localization" href="toolkit/about/abuseReports.ftl">
<script src="chrome://mozapps/content/extensions/named-deck.js"></script>
<script src="chrome://mozapps/content/extensions/aboutaddonsCommon.js"></script>
<script src="chrome://mozapps/content/extensions/message-bar.js"></script>
<script src="chrome://mozapps/content/extensions/abuse-reports.js"></script>

Просмотреть файл

@ -0,0 +1,221 @@
/* 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/. */
/* eslint max-len: ["error", 80] */
"use strict";
/**
* This element is for use with the <named-deck> element. Set the target
* <named-deck>'s ID in the "deck" attribute and the button's selected state
* will reflect the deck's state. When the button is clicked, it will set the
* view in the <named-deck> to the button's "name" attribute.
*
* NOTE: This does not observe changes to the "deck" or "name" attributes, so
* changing them likely won't work properly.
*
* <named-deck-button deck="pet-deck" name="dogs">Dogs</named-deck-button>
* <named-deck id="pet-deck">
* <p name="cats">I like cats.</p>
* <p name="dogs">I like dogs.</p>
* </named-deck>
*
* let btn = document.querySelector("named-deck-button");
* let deck = document.querySelector("named-deck");
* deck.selectedViewName == "cats";
* btn.selected == false; // Selected was pulled from the related deck.
* btn.click();
* deck.selectedViewName == "dogs";
* btn.selected == true; // Selected updated when view changed.
*/
class NamedDeckButton extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
// Include styles inline to avoid a FOUC.
let style = document.createElement("style");
style.textContent = `
button {
-moz-appearance: none;
border: none;
border-top: 2px solid transparent;
border-bottom: 2px solid transparent;
background: white;
font-size: 14px;
line-height: 20px;
padding: 4px 16px;
color: var(--grey-90);
}
button:hover {
background-color: var(--grey-90-a10);
border-top-color: var(--grey-90-a20);
}
button:hover:active {
background-color: var(--grey-90-a20);
}
:host([selected]) button {
border-top-color: var(--blue-60);
color: var(--blue-60);
}
`;
this.shadowRoot.appendChild(style);
let button = document.createElement("button");
button.appendChild(document.createElement("slot"));
this.shadowRoot.appendChild(button);
this.addEventListener("click", this);
}
connectedCallback() {
this.setSelectedFromDeck();
document.addEventListener("view-changed", this, {capture: true});
}
disconnectedCallback() {
document.removeEventListener("view-changed", this, {capture: true});
}
get deckId() {
return this.getAttribute("deck");
}
set deckId(val) {
this.setAttribute("deck", val);
}
get deck() {
return document.getElementById(this.deckId);
}
handleEvent(e) {
if (e.type == "view-changed" && e.target.id == this.deckId) {
this.setSelectedFromDeck();
} else if (e.type == "click") {
let {deck} = this;
if (deck) {
deck.selectedViewName = this.name;
}
}
}
get name() {
return this.getAttribute("name");
}
get selected() {
return this.hasAttribute("selected");
}
set selected(val) {
this.toggleAttribute("selected", !!val);
}
setSelectedFromDeck() {
let {deck} = this;
this.selected = deck && deck.selectedViewName == this.name;
}
}
customElements.define("named-deck-button", NamedDeckButton);
/**
* A deck that is indexed by the "name" attribute of its children. The
* <named-deck-button> element is a companion element that can update its state
* and change the view of a <named-deck>.
*
* When the deck is connected it will set the first child as the selected view
* if a view is not already selected.
*
* The deck is implemented using a named slot. Setting a slot directly on a
* child element of the deck is not supported.
*
* You can get or set the selected view by name with the `selectedViewName`
* property or by setting the "selected-view" attribute.
*
* <named-deck>
* <section name="cats">Some info about cats.</section>
* <section name="dogs">Some dog stuff.</section>
* </named-deck>
*
* let deck = document.querySelector("named-deck");
* deck.selectedViewName == "cats"; // Cat info is shown.
* deck.selectedViewName = "dogs";
* deck.selectedViewName == "dogs"; // Dog stuff is shown.
* deck.setAttribute("selected-view", "cats");
* deck.selectedViewName == "cats"; // Cat info is shown.
*/
class NamedDeck extends HTMLElement {
static get observedAttributes() {
return ["selected-view"];
}
constructor() {
super();
this.attachShadow({mode: "open"});
// Create a slot for the visible content.
let selectedSlot = document.createElement("slot");
selectedSlot.setAttribute("name", "selected");
this.shadowRoot.appendChild(selectedSlot);
this.observer = new MutationObserver(() => {
this._setSelectedViewAttributes();
});
}
connectedCallback() {
if (this.selectedViewName) {
// Make sure the selected view is shown.
this._setSelectedViewAttributes();
} else {
// If there's no selected view, default to the first.
let firstView = this.firstElementChild;
if (firstView) {
// This will trigger showing the first view.
this.selectedViewName = firstView.getAttribute("name");
}
}
this.observer.observe(this, {childList: true});
}
disconnectedCallback() {
this.observer.disconnect();
}
attributeChangedCallback(attr, oldVal, newVal) {
if (attr == "selected-view" && oldVal != newVal) {
// Update the slot attribute on the views.
this._setSelectedViewAttributes();
// Notify that the selected view changed.
this.dispatchEvent(new CustomEvent("view-changed"));
}
}
get selectedViewName() {
return this.getAttribute("selected-view");
}
set selectedViewName(name) {
this.setAttribute("selected-view", name);
}
/**
* Set the slot attribute on all of the views to ensure only the selected view
* is shown.
*/
_setSelectedViewAttributes() {
let {selectedViewName} = this;
for (let view of this.children) {
if (view.getAttribute("name") == selectedViewName) {
view.slot = "selected";
} else {
view.slot = "";
}
}
}
}
customElements.define("named-deck", NamedDeck);

Просмотреть файл

@ -29,6 +29,7 @@ toolkit.jar:
content/mozapps/extensions/abuse-report-panel.js (content/abuse-report-panel.js)
content/mozapps/extensions/message-bar.css (content/message-bar.css)
content/mozapps/extensions/message-bar.js (content/message-bar.js)
content/mozapps/extensions/named-deck.js (content/named-deck.js)
content/mozapps/extensions/panel-list.css (content/panel-list.css)
content/mozapps/extensions/panel-item.css (content/panel-item.css)
content/mozapps/extensions/rating-star.css (content/rating-star.css)

Просмотреть файл

@ -84,6 +84,7 @@ skip-if = os == 'linux' && !debug # Bug 1398766
[browser_html_discover_view_prefs.js]
[browser_html_list_view.js]
[browser_html_message_bar.js]
[browser_html_named_deck.js]
[browser_html_options_ui_in_tab.js]
[browser_html_plugins.js]
skip-if = (os == 'win' && processor == 'aarch64') # aarch64 has no plugin support, bug 1525174 and 1547495

Просмотреть файл

@ -0,0 +1,187 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint max-len: ["error", 80] */
"use strict";
add_task(async function enableHtmlViews() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.htmlaboutaddons.enabled", true]],
});
});
const DEFAULT_SECTION_NAMES = ["one", "two", "three"];
function makeButton({doc, name, deckId}) {
let button = doc.createElement("named-deck-button");
button.setAttribute("name", name);
button.deckId = deckId;
button.textContent = name.toUpperCase();
return button;
}
function makeSection({doc, name}) {
let view = doc.createElement("section");
view.setAttribute("name", name);
view.textContent = name + name;
return view;
}
function addSection({name, deck, buttons}) {
let doc = deck.ownerDocument;
let button = makeButton({doc, name, deckId: deck.id});
buttons.appendChild(button);
let view = makeSection({doc, name});
deck.appendChild(view);
return {button, view};
}
async function runTests({deck, buttons}) {
const selectedSlot = deck.shadowRoot.querySelector('slot[name="selected"]');
const getButtonByName = name => buttons.querySelector(`[name="${name}"]`);
function checkState(name, count, empty = false) {
// Check that the right view is selected.
is(deck.selectedViewName, name, "The right view is selected");
// Verify there's one element in the slot.
let slottedEls = selectedSlot.assignedElements();
if (empty) {
is(slottedEls.length, 0, "The deck is empty");
} else {
is(slottedEls.length, 1, "There's one visible view");
is(slottedEls[0].getAttribute("name"), name,
"The correct view is in the slot");
}
// Check that the hidden properties are set.
let sections = deck.querySelectorAll("section");
is(sections.length, count, "There are the right number of sections");
for (let section of sections) {
let sectionName = section.getAttribute("name");
if (sectionName == name) {
is(section.slot, "selected", `${sectionName} is visible`);
} else {
is(section.slot, "", `${sectionName} is hidden`);
}
}
// Check the right button is selected.
is(buttons.children.length, count, "There are the right number of buttons");
for (let button of buttons.children) {
let buttonName = button.getAttribute("name");
let selected = buttonName == name;
is(button.hasAttribute("selected"), selected,
`${buttonName} is ${selected ? "selected" : "not selected"}`);
}
}
// Check that the first view is selected by default.
checkState("one", 3);
// Switch to the third view.
info("Switch to section three");
getButtonByName("three").click();
checkState("three", 3);
// Add a new section, nothing changes.
info("Add section last");
let last = addSection({name: "last", deck, buttons});
checkState("three", 4);
// We can switch to the new section.
last.button.click();
info("Switch to section last");
checkState("last", 4);
info("Switch view with selectedViewName");
let shown = BrowserTestUtils.waitForEvent(deck, "view-changed");
deck.selectedViewName = "two";
await shown;
checkState("two", 4);
info("Switch back to the last view to test removing selected view");
shown = BrowserTestUtils.waitForEvent(deck, "view-changed");
deck.setAttribute("selected-view", "last");
await shown;
checkState("last", 4);
// Removing the selected section leaves the selected slot empty.
info("Remove section last");
last.button.remove();
last.view.remove();
info("Should not have any selected views");
checkState("last", 3, true);
// Setting a missing view will give a "view-changed" event.
info("Set view to a missing name");
let hidden = BrowserTestUtils.waitForEvent(deck, "view-changed");
deck.selectedViewName = "missing";
await hidden;
checkState("missing", 3, true);
// Adding the view won't trigger "view-changed", but the view will slotted.
info("Add the missing view, it should be shown");
shown = BrowserTestUtils.waitForEvent(selectedSlot, "slotchange");
let viewChangedEvent = false;
let viewChangedFn = () => { viewChangedEvent = true; };
deck.addEventListener("view-changed", viewChangedFn);
addSection({name: "missing", deck, buttons});
await shown;
deck.removeEventListener("view-changed", viewChangedFn);
ok(!viewChangedEvent, "The view-changed event didn't fire");
checkState("missing", 4);
}
async function setup({doc, beAsync, first}) {
const deckId = `${first}-first-${beAsync}`;
// Make the deck and buttons.
const deck = doc.createElement("named-deck");
deck.id = deckId;
for (let name of DEFAULT_SECTION_NAMES) {
deck.appendChild(makeSection({doc, name}));
}
const buttons = doc.createElement("div");
for (let name of DEFAULT_SECTION_NAMES) {
buttons.appendChild(makeButton({doc, name, deckId}));
}
let ordered;
if (first == "deck") {
ordered = [deck, buttons];
} else if (first == "buttons") {
ordered = [buttons, deck];
} else {
throw new Error("Invalid order");
}
// Insert them in the specified order, possibly async.
doc.body.appendChild(ordered.shift());
if (beAsync) {
await new Promise(resolve => requestAnimationFrame(resolve));
}
doc.body.appendChild(ordered.shift());
return {deck, buttons};
}
add_task(async function testNamedDeckAndButtons() {
const win = await loadInitialView("extension");
const doc = win.document;
// Check adding the deck first.
dump("Running deck first tests synchronously");
await runTests(await setup({doc, beAsync: false, first: "deck"}));
dump("Running deck first tests asynchronously");
await runTests(await setup({doc, beAsync: true, first: "deck"}));
// Check adding the buttons first.
dump("Running buttons first tests synchronously");
await runTests(await setup({doc, beAsync: false, first: "buttons"}));
dump("Running buttons first tests asynchronously");
await runTests(await setup({doc, beAsync: true, first: "buttons"}));
await closeView(win);
});