зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f71d78cf3c
Коммит
4bd67d819d
|
@ -8,6 +8,7 @@
|
||||||
<link rel="localization" href="toolkit/about/aboutAddons.ftl">
|
<link rel="localization" href="toolkit/about/aboutAddons.ftl">
|
||||||
<link rel="localization" href="toolkit/about/abuseReports.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/aboutaddonsCommon.js"></script>
|
||||||
<script src="chrome://mozapps/content/extensions/message-bar.js"></script>
|
<script src="chrome://mozapps/content/extensions/message-bar.js"></script>
|
||||||
<script src="chrome://mozapps/content/extensions/abuse-reports.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/abuse-report-panel.js (content/abuse-report-panel.js)
|
||||||
content/mozapps/extensions/message-bar.css (content/message-bar.css)
|
content/mozapps/extensions/message-bar.css (content/message-bar.css)
|
||||||
content/mozapps/extensions/message-bar.js (content/message-bar.js)
|
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-list.css (content/panel-list.css)
|
||||||
content/mozapps/extensions/panel-item.css (content/panel-item.css)
|
content/mozapps/extensions/panel-item.css (content/panel-item.css)
|
||||||
content/mozapps/extensions/rating-star.css (content/rating-star.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_discover_view_prefs.js]
|
||||||
[browser_html_list_view.js]
|
[browser_html_list_view.js]
|
||||||
[browser_html_message_bar.js]
|
[browser_html_message_bar.js]
|
||||||
|
[browser_html_named_deck.js]
|
||||||
[browser_html_options_ui_in_tab.js]
|
[browser_html_options_ui_in_tab.js]
|
||||||
[browser_html_plugins.js]
|
[browser_html_plugins.js]
|
||||||
skip-if = (os == 'win' && processor == 'aarch64') # aarch64 has no plugin support, bug 1525174 and 1547495
|
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);
|
||||||
|
});
|
Загрузка…
Ссылка в новой задаче