Bug 1850591 - Convert Firefox View's domain-specific navigation component to a global component r=desktop-theme-reviewers,fxview-reviewers,reusable-components-reviewers,sclements,jules,hjones,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D193593
This commit is contained in:
Kelly Cochrane 2024-02-09 22:01:30 +00:00
parent c056a64e2e
commit fa0e1f01ae
39 changed files with 871 additions and 899 deletions

View file

@ -10,12 +10,21 @@ const {
"resource://testing-common/FirefoxViewTestUtils.sys.mjs"
);
/**
* Bug 1856572 - This test is to ensure that fluent strings in Firefox View
* can be updated after opening Customize Mode
* **/
add_task(async function test_data_l10n_customize_mode() {
FirefoxViewTestUtilsInit(this);
await withFirefoxView({ win: window }, async function (browser) {
/**
* Bug 1856572, Bug 1857622: Without requesting two animation frames
* the "missing Fluent strings" issue will not reproduce.
*
* Given the precondition that we open Firefox View, then open Customize Mode, then
* navigate back to Firefox View in a quick succession, as long as Fluent
* controlled strings can be updated by changing their Fluent IDs then this
* test is valid.
*/
await new Promise(r =>
requestAnimationFrame(() => requestAnimationFrame(r))
@ -23,39 +32,47 @@ add_task(async function test_data_l10n_customize_mode() {
await startCustomizing();
await endCustomizing();
await openFirefoxViewTab(window);
const { document } = browser.contentWindow;
let header = document.querySelector("h1");
document.l10n.setAttributes(header, "firefoxview-overview-header");
let secondPageNavButton = document.querySelectorAll(
"moz-page-nav-button"
)[0];
document.l10n.setAttributes(
secondPageNavButton,
"firefoxview-overview-header"
);
let previousText = await document.l10n.formatValue(
"firefoxview-page-title"
"firefoxview-opentabs-nav"
);
let translatedText = await window.content.document.l10n.formatValue(
"firefoxview-overview-header"
);
/**
* This should be replaced with
* BrowserTestUtils.waitForMutationCondition(header, {characterData: true}, ...)
* BrowserTestUtils.waitForMutationCondition(secondPageNavButton, {characterData: true}, ...)
* but apparently Fluent manipulation of textContent doesn't result
* in a characterData mutation occurring.
*/
await BrowserTestUtils.waitForCondition(() => {
return header.textContent != previousText;
return (
secondPageNavButton.textContent !== previousText &&
secondPageNavButton.textContent === translatedText
);
}, "waiting for text content to change");
Assert.equal(
header.getAttribute("data-l10n-id"),
secondPageNavButton.getAttribute("data-l10n-id"),
"firefoxview-overview-header",
"data-l10n-id should be updated"
);
Assert.notEqual(
previousText,
header.textContent,
"The header's text content should be updated"
);
let translatedText = await window.content.document.l10n.formatValue(
"firefoxview-overview-header"
secondPageNavButton.textContent,
"The second page-nav button text content should be updated"
);
Assert.equal(
translatedText,
header.textContent,
secondPageNavButton.textContent,
"The changed text should be the translated value of 'firefoxview-overview-header"
);
});

View file

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 655 B

View file

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 558 B

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 658 B

View file

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 743 B

View file

@ -69,6 +69,10 @@ body {
grid-template-columns: var(--fxview-sidebar-width) 1fr;
background-color: var(--fxview-background-color);
color: var(--fxview-text-primary-color);
@media (max-width: 52rem) {
display: flex;
}
}
.main-container {
@ -88,26 +92,6 @@ body {
margin: 0;
}
fxview-category-button:focus-visible {
outline-offset: var(--in-content-focus-outline-inset);
}
fxview-category-button[name="recentbrowsing"]::part(icon) {
background-image: url("chrome://browser/content/firefoxview/category-recentbrowsing.svg");
}
fxview-category-button[name="opentabs"]::part(icon) {
background-image: url("chrome://browser/content/firefoxview/category-opentabs.svg");
}
fxview-category-button[name="recentlyclosed"]::part(icon) {
background-image: url("chrome://browser/content/firefoxview/category-recentlyclosed.svg");
}
fxview-category-button[name="syncedtabs"]::part(icon) {
background-image: url("chrome://browser/content/firefoxview/category-syncedtabs.svg");
}
fxview-category-button[name="history"]::part(icon) {
background-image: url("chrome://browser/content/firefoxview/category-history.svg");
}
fxview-tab-list.with-dismiss-button::part(secondary-button) {
background-image: url("chrome://global/skin/icons/close.svg");
}
@ -174,14 +158,6 @@ panel-list {
overflow-y: visible;
}
fxview-category-navigation {
overflow-y: auto;
}
fxview-category-navigation h1 {
margin-block: 0;
}
fxview-empty-state:not([isSelectedTab]) button[slot="primary-action"] {
margin-inline-start: 0;
}

View file

@ -41,54 +41,51 @@
></script>
<script
type="module"
src="chrome://browser/content/firefoxview/fxview-category-navigation.mjs"
src="chrome://browser/content/firefoxview/syncedtabs.mjs"
></script>
<script
type="module"
src="chrome://browser/content/firefoxview/syncedtabs.mjs"
src="chrome://global/content/elements/moz-page-nav.mjs"
></script>
<script src="chrome://browser/content/contentTheme.js"></script>
</head>
<body>
<fxview-category-navigation>
<h1 slot="category-nav-header" data-l10n-id="firefoxview-page-title"></h1>
<fxview-category-button
class="category"
slot="category-button"
name="recentbrowsing"
<moz-page-nav
data-l10n-id="firefoxview-page-heading"
data-l10n-attrs="heading"
>
<moz-page-nav-button
view="recentbrowsing"
data-l10n-id="firefoxview-overview-nav"
iconSrc="chrome://browser/content/firefoxview/view-recentbrowsing.svg"
>
</fxview-category-button>
<fxview-category-button
class="category"
slot="category-button"
name="opentabs"
</moz-page-nav-button>
<moz-page-nav-button
view="opentabs"
data-l10n-id="firefoxview-opentabs-nav"
iconSrc="chrome://browser/content/firefoxview/view-opentabs.svg"
>
</fxview-category-button>
<fxview-category-button
class="category"
slot="category-button"
name="recentlyclosed"
</moz-page-nav-button>
<moz-page-nav-button
view="recentlyclosed"
data-l10n-id="firefoxview-recently-closed-nav"
iconSrc="chrome://browser/content/firefoxview/view-recentlyclosed.svg"
>
</fxview-category-button>
<fxview-category-button
class="category"
slot="category-button"
name="syncedtabs"
</moz-page-nav-button>
<moz-page-nav-button
view="syncedtabs"
data-l10n-id="firefoxview-synced-tabs-nav"
iconSrc="chrome://browser/content/firefoxview/view-syncedtabs.svg"
>
</fxview-category-button>
<fxview-category-button
class="category"
slot="category-button"
name="history"
</moz-page-nav-button>
<moz-page-nav-button
view="history"
data-l10n-id="firefoxview-history-nav"
iconSrc="chrome://browser/content/firefoxview/view-history.svg"
>
</fxview-category-button>
</fxview-category-navigation>
</moz-page-nav-button>
</moz-page-nav>
<main id="pages" role="application" data-l10n-id="firefoxview-page-label">
<div class="main-container">
<named-deck>

View file

@ -3,35 +3,35 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
let pageList = [];
let categoryPagesDeck = null;
let categoryNavigation = null;
let viewsDeck = null;
let pageNav = null;
let activeComponent = null;
let searchKeyboardShortcut = null;
const { topChromeWindow } = window.browsingContext;
function onHashChange() {
let page = document.location?.hash.substring(1);
if (!page || !pageList.includes(page)) {
page = "recentbrowsing";
let view = document.location?.hash.substring(1);
if (!view || !pageList.includes(view)) {
view = "recentbrowsing";
}
changePage(page);
changeView(view);
}
function changePage(page) {
categoryPagesDeck.selectedViewName = page;
categoryNavigation.currentCategory = page;
if (categoryNavigation.categoryButtons.includes(document.activeElement)) {
let currentCategoryButton = categoryNavigation.categoryButtons.find(
categoryButton => categoryButton.name === page
function changeView(view) {
viewsDeck.selectedViewName = view;
pageNav.currentPage = view;
if (pageNav.pageNavButtons.includes(document.activeElement)) {
let currentPageButton = pageNav.pageNavButtons.find(
pageButton => pageButton.view === view
);
(currentCategoryButton || categoryNavigation.categoryButtons[0]).focus();
(currentPageButton || pageNav.pageNavButtons[0]).focus();
}
}
function onPagesDeckViewChange() {
for (const child of categoryPagesDeck.children) {
if (child.getAttribute("name") == categoryPagesDeck.selectedViewName) {
function onViewsDeckViewChange() {
for (const child of viewsDeck.children) {
if (child.getAttribute("name") == viewsDeck.selectedViewName) {
child.enter();
activeComponent = child;
} else {
@ -41,11 +41,11 @@ function onPagesDeckViewChange() {
}
function recordNavigationTelemetry(source, eventTarget) {
let page = "recentbrowsing";
let view = "recentbrowsing";
if (source === "category-navigation") {
page = eventTarget.parentNode.currentCategory;
view = eventTarget.parentNode.currentView;
} else if (source === "view-all") {
page = eventTarget.shortPageName;
view = eventTarget.shortPageName;
}
// Record telemetry
Services.telemetry.recordEvent(
@ -54,7 +54,7 @@ function recordNavigationTelemetry(source, eventTarget) {
"navigation",
null,
{
page,
page: view,
source,
}
);
@ -73,7 +73,7 @@ async function updateSearchTextboxSize() {
const placeholder = msg.attributes[0].value;
maxLength = Math.max(maxLength, placeholder.length);
}
for (const child of categoryPagesDeck.children) {
for (const child of viewsDeck.children) {
child.searchTextboxSize = maxLength;
}
}
@ -89,15 +89,15 @@ async function updateSearchKeyboardShortcut() {
window.addEventListener("DOMContentLoaded", async () => {
recordEnteredTelemetry();
categoryNavigation = document.querySelector("fxview-category-navigation");
categoryPagesDeck = document.querySelector("named-deck");
pageNav = document.querySelector("moz-page-nav");
viewsDeck = document.querySelector("named-deck");
for (const item of categoryNavigation.categoryButtons) {
pageList.push(item.getAttribute("name"));
for (const item of pageNav.pageNavButtons) {
pageList.push(item.getAttribute("view"));
}
window.addEventListener("hashchange", onHashChange);
window.addEventListener("change-category", function (event) {
location.hash = event.target.getAttribute("name");
window.addEventListener("change-view", function (event) {
location.hash = event.target.getAttribute("view");
window.scrollTo(0, 0);
recordNavigationTelemetry("category-navigation", event.target);
});
@ -105,11 +105,11 @@ window.addEventListener("DOMContentLoaded", async () => {
recordNavigationTelemetry("view-all", event.originalTarget);
});
categoryPagesDeck.addEventListener("view-changed", onPagesDeckViewChange);
viewsDeck.addEventListener("view-changed", onViewsDeckViewChange);
// set the initial state
onHashChange();
onPagesDeckViewChange();
onViewsDeckViewChange();
await updateSearchTextboxSize();
await updateSearchKeyboardShortcut();

View file

@ -1,60 +0,0 @@
/* 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/. */
:host {
--fxviewcategorynav-button-padding: 8px;
margin-inline-start: 42px;
position: sticky;
top: 0;
height: 100vh;
}
nav {
display: grid;
grid-template-rows: min-content 1fr auto;
gap: 25px;
margin-block-start: var(--fxview-margin-top);
}
.category-nav-header {
/* Align the header text/icon with the category button icons */
margin-inline-start: var(--fxviewcategorynav-button-padding);
}
.category-nav-buttons,
::slotted(.category-nav-footer) {
display: grid;
grid-template-columns: 1fr;
grid-auto-rows: min-content;
gap: 4px;
}
@media (prefers-contrast) {
.category-nav-buttons {
gap: 8px;
}
}
@media (prefers-reduced-motion) {
/* (See Bug 1610081) Setting border-inline-end to add clear differentiation between side navigation and main content area */
:host {
border-inline-end: 1px solid var(--in-content-border-color);
}
}
@media (max-width: 52rem) {
:host {
grid-template-rows: 1fr auto;
}
.category-nav-header {
display: none;
}
.category-nav-buttons,
::slotted(.category-nav-footer) {
justify-content: center;
grid-template-columns: min-content;
}
}

View file

@ -1,150 +0,0 @@
/* 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/. */
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
export default class FxviewCategoryNavigation extends MozLitElement {
static properties = {
currentCategory: { type: String },
};
static queries = {
categoryButtonsSlot: "slot[name=category-button]",
};
get categoryButtons() {
return this.categoryButtonsSlot
.assignedNodes()
.filter(node => !node.hidden);
}
onChangeCategory(e) {
this.currentCategory = e.target.name;
}
handleFocus(e) {
if (e.key == "ArrowDown" || e.key == "ArrowRight") {
e.preventDefault();
this.focusNextCategory();
} else if (e.key == "ArrowUp" || e.key == "ArrowLeft") {
e.preventDefault();
this.focusPreviousCategory();
}
}
focusPreviousCategory() {
let categoryButtons = this.categoryButtons;
let currentIndex = categoryButtons.findIndex(b => b.selected);
let prev = categoryButtons[currentIndex - 1];
if (prev) {
prev.activate();
prev.focus();
}
}
focusNextCategory() {
let categoryButtons = this.categoryButtons;
let currentIndex = categoryButtons.findIndex(b => b.selected);
let next = categoryButtons[currentIndex + 1];
if (next) {
next.activate();
next.focus();
}
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://browser/content/firefoxview/fxview-category-navigation.css"
/>
<nav>
<div class="category-nav-header">
<slot name="category-nav-header"></slot>
</div>
<div
class="category-nav-buttons"
role="tablist"
aria-orientation="vertical"
>
<slot
name="category-button"
@change-category=${this.onChangeCategory}
@keydown=${this.handleFocus}
></slot>
</div>
<div class="category-nav-footer">
<slot name="category-nav-footer"></slot>
</div>
</nav>
`;
}
updated() {
let categorySelected = false;
let assignedCategories = this.categoryButtons;
for (let button of assignedCategories) {
button.selected = button.name == this.currentCategory;
categorySelected = categorySelected || button.selected;
}
if (!categorySelected && assignedCategories.length) {
// Current category has no matching category, reset to the first category.
assignedCategories[0].activate();
}
}
}
customElements.define("fxview-category-navigation", FxviewCategoryNavigation);
export class FxviewCategoryButton extends MozLitElement {
static properties = {
selected: { type: Boolean },
};
static queries = {
buttonEl: "button",
};
connectedCallback() {
super.connectedCallback();
this.setAttribute("role", "tab");
}
get name() {
return this.getAttribute("name");
}
activate() {
this.dispatchEvent(
new CustomEvent("change-category", {
bubbles: true,
composed: true,
})
);
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://browser/content/firefoxview/fxview-category-button.css"
/>
<button
aria-hidden="true"
tabindex="-1"
?selected=${this.selected}
@click=${this.activate}
>
<span class="category-icon" part="icon"></span>
<slot></slot>
</button>
`;
}
updated() {
this.setAttribute("aria-selected", this.selected);
this.setAttribute("tabindex", this.selected ? 0 : -1);
}
}
customElements.define("fxview-category-button", FxviewCategoryButton);

View file

@ -15,9 +15,6 @@ browser.jar:
content/browser/firefoxview/view-syncedtabs.css
content/browser/firefoxview/recentbrowsing.mjs
content/browser/firefoxview/firefoxview.css
content/browser/firefoxview/fxview-category-button.css
content/browser/firefoxview/fxview-category-navigation.css
content/browser/firefoxview/fxview-category-navigation.mjs
content/browser/firefoxview/fxview-empty-state.css
content/browser/firefoxview/fxview-empty-state.mjs
content/browser/firefoxview/helpers.mjs
@ -29,12 +26,12 @@ browser.jar:
content/browser/firefoxview/recentlyclosed.mjs
content/browser/firefoxview/viewpage.mjs
content/browser/firefoxview/history-empty.svg (content/history-empty.svg)
content/browser/firefoxview/category-history.svg (content/category-history.svg)
content/browser/firefoxview/category-opentabs.svg (content/category-opentabs.svg)
content/browser/firefoxview/category-recentbrowsing.svg (content/category-recentbrowsing.svg)
content/browser/firefoxview/category-recentlyclosed.svg (content/category-recentlyclosed.svg)
content/browser/firefoxview/category-syncedtabs.svg (content/category-syncedtabs.svg)
content/browser/firefoxview/recentlyclosed-empty.svg (content/recentlyclosed-empty.svg)
content/browser/firefoxview/synced-tabs-error.svg (content/synced-tabs-error.svg)
content/browser/callout-tab-pickup.svg (content/callout-tab-pickup.svg)
content/browser/callout-tab-pickup-dark.svg (content/callout-tab-pickup-dark.svg)
content/browser/firefoxview/view-history.svg (content/view-history.svg)
content/browser/firefoxview/view-opentabs.svg (content/view-opentabs.svg)
content/browser/firefoxview/view-recentbrowsing.svg (content/view-recentbrowsing.svg)
content/browser/firefoxview/view-recentlyclosed.svg (content/view-recentlyclosed.svg)
content/browser/firefoxview/view-syncedtabs.svg (content/view-syncedtabs.svg)

View file

@ -27,37 +27,37 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296
["browser_firefoxview.js"]
["browser_firefoxview_tab.js"]
["browser_notification_dot.js"]
skip-if = ["true"] # Bug 1851453
["browser_opentabs_changes.js"]
["browser_reload_firefoxview.js"]
["browser_tab_close_last_tab.js"]
["browser_tab_on_close_warning.js"]
["browser_firefoxview_paused.js"]
["browser_firefoxview_general_telemetry.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_firefoxview_navigation.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_firefoxview_paused.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_firefoxview_search_telemetry.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_firefoxview_tab.js"]
["browser_firefoxview_virtual_list.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_history_firefoxview.js"]
skip-if = ["true"] # Bug 1877594
["browser_opentabs_firefoxview.js"]
["browser_notification_dot.js"]
skip-if = ["true"] # Bug 1851453
["browser_opentabs_cards.js"]
fail-if = ["a11y_checks"] # Bugs 1858041, 1854625, and 1872174 clicked Show all link is not accessible because it is "hidden" when clicked
["browser_opentabs_changes.js"]
["browser_opentabs_firefoxview.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_opentabs_recency.js"]
skip-if = [
"os == 'win'",
@ -65,9 +65,19 @@ skip-if = [
] # macos times out, see bug 1857293, skipped for windows, see bug 1858460
["browser_opentabs_tab_indicators.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_recentlyclosed_firefoxview.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_reload_firefoxview.js"]
["browser_syncedtabs_errors_firefoxview.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_syncedtabs_firefoxview.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
["browser_tab_close_last_tab.js"]
["browser_tab_on_close_warning.js"]

View file

@ -6,10 +6,7 @@ add_task(async function about_firefoxview_smoke_test() {
const { document } = browser.contentWindow;
// sanity check the important regions exist on this page
ok(
document.querySelector("fxview-category-navigation"),
"fxview-category-navigation element exists"
);
ok(document.querySelector("moz-page-nav"), "moz-page-nav element exists");
ok(document.querySelector("named-deck"), "named-deck element exists");
});
});

View file

@ -39,7 +39,7 @@ add_task(async function firefox_view_entered_telemetry() {
enteredTelemetry[4] = { page: "recentlyclosed" };
enteredAndTabSelectedEvents = [tabSelectedTelemetry, enteredTelemetry];
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
await clearAllParentTelemetryEvents();
await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots");
is(
@ -107,9 +107,9 @@ add_task(async function test_change_page_telemetry() {
],
];
await clearAllParentTelemetryEvents();
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
await telemetryEvent(changePageEvent);
navigateToCategory(document, "recentbrowsing");
await navigateToViewAndWait(document, "recentbrowsing");
let openTabsComponent = document.querySelector(
"view-opentabs[slot=opentabs]"
@ -189,7 +189,7 @@ add_task(async function test_context_menu_new_window_telemetry() {
);
// Test history context menu options
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await TestUtils.waitForCondition(
@ -245,7 +245,7 @@ add_task(async function test_context_menu_private_window_telemetry() {
);
// Test history context menu options
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await TestUtils.waitForCondition(
@ -314,7 +314,7 @@ add_task(async function test_context_menu_delete_from_history_telemetry() {
);
// Test history context menu options
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await TestUtils.waitForCondition(

View file

@ -3,15 +3,15 @@
const URL_BASE = `${getFirefoxViewURL()}#`;
function assertCorrectPage(document, name, event) {
function assertCorrectPage(document, view, event) {
is(
document.location.hash,
`#${name}`,
`Navigation button for ${name} navigates to ${URL_BASE + name} on ${event}.`
`#${view}`,
`Navigation button for ${view} navigates to ${URL_BASE + view} on ${event}.`
);
is(
document.querySelector("named-deck").selectedViewName,
name,
view,
"The correct deck child is selected"
);
}
@ -22,21 +22,21 @@ add_task(async function test_side_component_navigation_by_click() {
const { document } = browser.contentWindow;
let win = browser.ownerGlobal;
const categoryButtons = document.querySelectorAll("fxview-category-button");
const pageNavButtons = document.querySelectorAll("moz-page-nav-button");
for (let element of categoryButtons) {
const name = element.name;
for (let element of pageNavButtons) {
const view = element.view;
let buttonClicked = BrowserTestUtils.waitForEvent(
element.buttonEl,
"click",
win
);
info(`Clicking navigation button for ${name}`);
info(`Clicking navigation button for ${view}`);
EventUtils.synthesizeMouseAtCenter(element.buttonEl, {}, content);
await buttonClicked;
assertCorrectPage(document, name, "click");
assertCorrectPage(document, view, "click");
}
});
});
@ -47,49 +47,49 @@ add_task(async function test_side_component_navigation_by_keyboard() {
const { document } = browser.contentWindow;
let win = browser.ownerGlobal;
const categoryButtons = document.querySelectorAll("fxview-category-button");
const firstButton = categoryButtons[0];
const pageNavButtons = document.querySelectorAll("moz-page-nav-button");
const firstButton = pageNavButtons[0].buttonEl;
firstButton.focus();
is(
document.activeElement,
document.activeElement.shadowRoot.activeElement,
firstButton,
"The first category button has focus"
"The first page nav button has focus"
);
for (let element of Array.from(categoryButtons).slice(1)) {
const name = element.name;
for (let element of Array.from(pageNavButtons).slice(1)) {
const view = element.view;
let buttonFocused = BrowserTestUtils.waitForEvent(element, "focus", win);
info(`Focus is on ${document.activeElement.name}`);
info(`Arrow down on navigation to ${name}`);
info(`Focus is on ${document.activeElement.view}`);
info(`Arrow down on navigation to ${view}`);
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
await buttonFocused;
assertCorrectPage(document, name, "key press");
assertCorrectPage(document, view, "key press");
}
});
});
add_task(async function test_direct_navigation_to_correct_category() {
add_task(async function test_direct_navigation_to_correct_view() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
const categoryButtons = document.querySelectorAll("fxview-category-button");
const pageNavButtons = document.querySelectorAll("moz-page-nav-button");
const namedDeck = document.querySelector("named-deck");
for (let element of categoryButtons) {
const name = element.name;
for (let element of pageNavButtons) {
const view = element.view;
info(`Navigating to ${URL_BASE + name}`);
document.location.assign(URL_BASE + name);
info(`Navigating to ${URL_BASE + view}`);
document.location.assign(URL_BASE + view);
await BrowserTestUtils.waitForCondition(() => {
return namedDeck.selectedViewName === name;
return namedDeck.selectedViewName === view;
}, "Wait for navigation to complete");
is(
namedDeck.selectedViewName,
name,
`The correct deck child for category ${name} is selected`
view,
`The correct deck child for view ${view} is selected`
);
}
});

View file

@ -322,7 +322,7 @@ add_task(async function test_opentabs() {
const document = browser.contentDocument;
const { openTabsView } = getTopLevelViewElements(document);
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
const { openTabsList } = await getElements(document);
ok(openTabsView, "Found the open tabs view");
@ -387,7 +387,7 @@ add_task(async function test_recentlyclosed() {
await withFirefoxView({}, async browser => {
const document = browser.contentDocument;
const { recentlyClosedView } = getTopLevelViewElements(document);
await navigateToCategoryAndWait(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
const { recentlyClosedList } = await getElements(document);
ok(recentlyClosedView, "Found the recently-closed view");

View file

@ -56,7 +56,7 @@ add_task(async function test_search_initiated_telemetry() {
EventUtils.sendString("example.com", content);
await telemetryEvent(searchEvent("recentbrowsing"));
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
await clearAllParentTelemetryEvents();
is(document.location.hash, "#opentabs", "Searching within open tabs.");
const openTabs = document.querySelector("named-deck > view-opentabs");
@ -65,7 +65,7 @@ add_task(async function test_search_initiated_telemetry() {
EventUtils.sendString("example.com", content);
await telemetryEvent(searchEvent("opentabs"));
await navigateToCategoryAndWait(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
await clearAllParentTelemetryEvents();
is(
document.location.hash,
@ -84,7 +84,7 @@ add_task(async function test_search_initiated_telemetry() {
EventUtils.sendString("example.com", content);
await telemetryEvent(searchEvent("recentlyclosed"));
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
await clearAllParentTelemetryEvents();
is(document.location.hash, "#syncedtabs", "Searching within synced tabs.");
const syncedTabs = document.querySelector("named-deck > view-syncedtabs");
@ -93,7 +93,7 @@ add_task(async function test_search_initiated_telemetry() {
EventUtils.sendString("example.com", content);
await telemetryEvent(searchEvent("syncedtabs"));
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
await clearAllParentTelemetryEvents();
is(document.location.hash, "#history", "Searching within history.");
const history = document.querySelector("named-deck > view-history");
@ -310,7 +310,7 @@ add_task(async function test_sort_history_search_telemetry() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
const historyComponent = document.querySelector("view-history");
const searchTextbox = await TestUtils.waitForCondition(
@ -426,7 +426,7 @@ add_task(async function test_cumulative_searches_recently_closed_telemetry() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
is(
document.location.hash,
"#recentlyclosed",
@ -471,7 +471,7 @@ add_task(async function test_cumulative_searches_open_tabs_telemetry() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
is(document.location.hash, "#opentabs", "Searching within open tabs.");
const openTabs = document.querySelector("named-deck > view-opentabs");
@ -517,7 +517,7 @@ add_task(async function test_cumulative_searches_history_telemetry() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
is(document.location.hash, "#history", "Searching within history.");
const history = document.querySelector("named-deck > view-history");
const searchTextbox = await TestUtils.waitForCondition(() => {
@ -579,7 +579,7 @@ add_task(async function test_cumulative_searches_syncedtabs_telemetry() {
const { document } = browser.contentWindow;
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
is(document.location.hash, "#syncedtabs", "Searching within synced tabs.");
let syncedTabs = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"

View file

@ -29,7 +29,7 @@ add_task(async function test_max_render_count_on_win_resize() {
"Firefox View is loaded to the Recent Browsing page."
);
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
let tabList = historyComponent.lists[0];

View file

@ -169,7 +169,7 @@ add_task(async function test_list_ordering() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
@ -262,7 +262,7 @@ add_task(async function test_empty_states() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
@ -350,7 +350,7 @@ add_task(async function test_observers_removed_when_view_is_hidden() {
);
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
const historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
let visitList = await TestUtils.waitForCondition(() =>
@ -399,7 +399,7 @@ add_task(async function test_show_all_history_telemetry() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
@ -424,7 +424,7 @@ add_task(async function test_show_all_history_telemetry() {
add_task(async function test_search_history() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
const historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
await historyComponentReady(historyComponent);
@ -504,7 +504,7 @@ add_task(async function test_persist_collapse_card_after_view_change() {
await addHistoryItems(today);
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "history");
const historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
await TestUtils.waitForCondition(
@ -529,8 +529,8 @@ add_task(async function test_persist_collapse_card_after_view_change() {
);
// Switch to a new view and then back to History
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToCategoryAndWait(document, "history");
await navigateToViewAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "history");
// Check that first history card is still collapsed after changing view
ok(

View file

@ -21,7 +21,7 @@ add_setup(function () {
async function navigateToOpenTabs(browser) {
const document = browser.contentDocument;
if (document.querySelector("named-deck").selectedViewName != "opentabs") {
await navigateToCategoryAndWait(browser.contentDocument, "opentabs");
await navigateToViewAndWait(browser.contentDocument, "opentabs");
}
}
@ -591,7 +591,7 @@ add_task(async function search_open_tabs_recent_browsing() {
});
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await navigateToCategoryAndWait(browser.contentDocument, "recentbrowsing");
await navigateToViewAndWait(browser.contentDocument, "recentbrowsing");
const recentBrowsing = browser.contentDocument.querySelector(
"view-recentbrowsing"
);

View file

@ -122,7 +122,7 @@ async function moreMenuSetup() {
await clickFirefoxViewButton(window);
const document = window.FirefoxViewHandler.tab.linkedBrowser.contentDocument;
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
setSortOption(openTabs, "tabStripOrder");

View file

@ -159,8 +159,9 @@ function getOpenTabsComponent(browser) {
async function checkTabList(browser, expected) {
const tabsView = getOpenTabsComponent(browser);
const openTabsCard = tabsView.shadowRoot.querySelector("view-opentabs-card");
await tabsView.getUpdateComplete();
await tabsView.updateComplete;
const tabList = openTabsCard.shadowRoot.querySelector("fxview-tab-list");
await tabList.getUpdateComplete();
Assert.ok(tabList, "Found the tab list element");
await TestUtils.waitForCondition(() => tabList.rowEls.length);
let actual = Array.from(tabList.rowEls).map(row => row.url);

View file

@ -22,7 +22,7 @@ add_task(async function test_notification_dot_indicator() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
let win = browser.ownerGlobal;
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
// load page that opens prompt when page is hidden
let openedTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
@ -48,7 +48,7 @@ add_task(async function test_notification_dot_indicator() {
await openTabs.updateComplete;
await TestUtils.waitForCondition(
() => openTabs.viewCards[0].tabList.rowEls[1].attention,
() => openTabs.viewCards[0].tabList.rowEls[0].attention,
"The opened tab doesn't have the attention property, so no notification dot is shown."
);
@ -79,7 +79,7 @@ add_task(async function test_container_indicator() {
URLs[0]
);
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
@ -118,7 +118,7 @@ add_task(async function test_container_indicator() {
add_task(async function test_sound_playing_muted_indicator() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "opentabs");
await navigateToViewAndWait(document, "opentabs");
// Load a page in a container tab
let soundTab = await BrowserTestUtils.openNewForegroundTab(
@ -146,7 +146,7 @@ add_task(async function test_sound_playing_muted_indicator() {
"The tab list hasn't rendered."
);
let soundPlayingTabElem = openTabs.viewCards[0].tabList.rowEls[1];
let soundPlayingTabElem = openTabs.viewCards[0].tabList.rowEls[0];
await TestUtils.waitForCondition(() => soundPlayingTabElem.soundPlaying);

View file

@ -196,7 +196,7 @@ add_task(async function test_initial_closed_tab() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
is(document.location.href, getFirefoxViewURL());
await navigateToCategoryAndWait(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
let { cleanup } = await prepareSingleClosedTab();
await switchToFxViewTab(window);
let [listItems] = await waitForRecentlyClosedTabsList(document);
@ -220,7 +220,7 @@ add_task(async function test_list_ordering() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await clearAllParentTelemetryEvents();
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
let [cardMainSlotNode, listItems] = await waitForRecentlyClosedTabsList(
document
);
@ -248,7 +248,7 @@ add_task(async function test_list_updates() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
let [listElem, listItems] = await waitForRecentlyClosedTabsList(document);
Assert.deepEqual(
@ -321,7 +321,7 @@ add_task(async function test_restore_tab() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
let [listElem, listItems] = await waitForRecentlyClosedTabsList(document);
Assert.deepEqual(
@ -365,7 +365,7 @@ add_task(async function test_dismiss_tab() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
let [listElem, listItems] = await waitForRecentlyClosedTabsList(document);
await clearAllParentTelemetryEvents();
@ -429,7 +429,7 @@ add_task(async function test_empty_states() {
const { document } = browser.contentWindow;
is(document.location.href, "about:firefoxview");
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
let recentlyClosedComponent = document.querySelector(
"view-recentlyclosed:not([slot=recentlyclosed])"
);
@ -479,7 +479,7 @@ add_task(async function test_observers_removed_when_view_is_hidden() {
await withFirefoxView({}, async function (browser) {
const { document } = browser.contentWindow;
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
const [listElem] = await waitForRecentlyClosedTabsList(document);
is(listElem.rowEls.length, 1);
@ -510,7 +510,7 @@ add_task(async function test_search() {
let { cleanup, expectedURLs } = await prepareClosedTabs();
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "recentlyclosed");
await navigateToViewAndWait(document, "recentlyclosed");
const [listElem] = await waitForRecentlyClosedTabsList(document);
const recentlyClosedComponent = document.querySelector(
"view-recentlyclosed:not([slot=recentlyclosed])"
@ -569,7 +569,7 @@ add_task(async function test_search_recent_browsing() {
const { document } = browser.contentWindow;
info("Input a search query.");
await navigateToCategoryAndWait(document, "recentbrowsing");
await navigateToViewAndWait(document, "recentbrowsing");
const recentBrowsing = document.querySelector("view-recentbrowsing");
EventUtils.synthesizeMouseAtCenter(
recentBrowsing.searchTextbox,

View file

@ -56,7 +56,7 @@ add_task(async function test_network_offline() {
sandbox.spy(TabsSetupFlowManager, "tryToClearError");
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
Services.obs.notifyObservers(
@ -112,7 +112,7 @@ add_task(async function test_sync_error() {
const sandbox = await setupWithDesktopDevices();
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
Services.obs.notifyObservers(null, "weave:service:sync:error");

View file

@ -29,7 +29,7 @@ add_task(async function test_unconfigured_initial_state() {
});
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -85,7 +85,7 @@ add_task(async function test_signed_in() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -148,7 +148,7 @@ add_task(async function test_no_synced_tabs() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -188,7 +188,7 @@ add_task(async function test_no_error_for_two_desktop() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -232,7 +232,7 @@ add_task(async function test_empty_state() {
await withFirefoxView({ openNewWindow: true }, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -277,7 +277,7 @@ add_task(async function test_tabs() {
await withFirefoxView({ openNewWindow: true }, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -365,7 +365,7 @@ add_task(async function test_empty_desktop_same_name() {
await withFirefoxView({ openNewWindow: true }, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -413,7 +413,7 @@ add_task(async function test_empty_desktop_same_name_three() {
await withFirefoxView({ openNewWindow: true }, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -459,7 +459,7 @@ add_task(async function search_synced_tabs() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "syncedtabs");
await navigateToViewAndWait(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
@ -666,7 +666,7 @@ add_task(async function search_synced_tabs_recent_browsing() {
});
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "recentbrowsing");
await navigateToViewAndWait(document, "recentbrowsing");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
const recentBrowsing = document.querySelector("view-recentbrowsing");

View file

@ -548,31 +548,19 @@ registerCleanupFunction(() => {
gSandbox?.restore();
});
function navigateToCategory(document, category) {
const navigation = document.querySelector("fxview-category-navigation");
let navButton = Array.from(navigation.categoryButtons).filter(
categoryButton => {
return categoryButton.name === category;
}
)[0];
navButton.buttonEl.click();
}
async function navigateToCategoryAndWait(document, category) {
info(`navigateToCategoryAndWait, for ${category}`);
const navigation = document.querySelector("fxview-category-navigation");
async function navigateToViewAndWait(document, view) {
info(`navigateToViewAndWait, for ${view}`);
const navigation = document.querySelector("moz-page-nav");
const win = document.ownerGlobal;
SimpleTest.promiseFocus(win);
let navButton = Array.from(navigation.categoryButtons).find(
categoryButton => {
return categoryButton.name === category;
}
);
let navButton = Array.from(navigation.pageNavButtons).find(pageNavButton => {
return pageNavButton.view === view;
});
const namedDeck = document.querySelector("named-deck");
await BrowserTestUtils.waitForCondition(
() => navButton.getBoundingClientRect().height,
`Waiting for ${category} button to be clickable`
`Waiting for ${view} button to be clickable`
);
EventUtils.synthesizeMouseAtCenter(navButton, {}, win);
@ -582,10 +570,10 @@ async function navigateToCategoryAndWait(document, category) {
child => child.slot == "selected"
);
return (
namedDeck.selectedViewName == category &&
namedDeck.selectedViewName == view &&
selectedView?.getBoundingClientRect().height
);
}, `Waiting for ${category} to be visible`);
}, `Waiting for ${view} to be visible`);
}
/**

View file

@ -2,6 +2,4 @@
["test_card_container.html"]
["test_fxview_category_navigation.html"]
["test_fxview_tab_list.html"]

View file

@ -1,322 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>FxviewCategoryNavigation Tests</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<script type="module" src="chrome://browser/content/firefoxview/fxview-category-navigation.mjs"></script>
</head>
<style>
body {
display: flex;
}
#navigation {
width: var(--in-content-sidebar-width);
}
fxview-category-button[name="category-one"]::part(icon) {
background-image: url("chrome://mozapps/skin/extensions/category-discover.svg");
}
fxview-category-button[name="category-two"]::part(icon) {
background-image: url("chrome://mozapps/skin/extensions/category-discover.svg");
}
fxview-category-button[name="category-three"]::part(icon) {
background-image: url("chrome://mozapps/skin/extensions/category-discover.svg");
}
fxview-category-button[name="category-four"]::part(icon) {
background-image: url("chrome://mozapps/skin/extensions/category-discover.svg");
}
fxview-category-button[name="category-five"]::part(icon) {
background-image: url("chrome://mozapps/skin/extensions/category-discover.svg");
}
</style>
<body>
<p id="display"></p>
<div id="content">
<div id="navigation">
<fxview-category-navigation>
<h2 slot="category-nav-header">Header</h2>
<fxview-category-button class="category" slot="category-button" name="category-one">
<span class="category-name">Category 1</span>
</fxview-category-button>
<fxview-category-button class="category" slot="category-button" name="category-two">
<span class="category-name">Category 2</span>
</fxview-category-button>
<fxview-category-button class="category" slot="category-button" name="category-three">
<span class="category-name">Category 3</span>
</fxview-category-button>
<fxview-category-button class="category" slot="category-button" name="category-four">
<span class="category-name">Category 4</span>
</fxview-category-button>
<fxview-category-button class="category" slot="category-button" name="category-five">
<span class="category-name">Category 5</span>
</fxview-category-button>
</fxview-category-navigation>
</div>
</div>
<pre id="test"></pre>
<script>
Services.scriptloader.loadSubScript(
"chrome://browser/content/utilityOverlay.js",
this
);
const { BrowserTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
const fxviewCategoryNav = document.querySelector("fxview-category-navigation");
function isActiveElement(expectedActiveEl) {
return expectedActiveEl.getRootNode().activeElement == expectedActiveEl;
}
/**
* Tests that the first category is selected by default
*/
add_task(async function test_first_item_selected_by_default() {
is(
fxviewCategoryNav.categoryButtons.length,
5,
"Five category buttons are in the navigation"
);
ok(
fxviewCategoryNav.categoryButtons[0].name === fxviewCategoryNav.currentCategory,
"The first category button is selected by default"
)
});
/**
* Tests that categories are selected when clicked
*/
add_task(async function test_select_category() {
let gBrowser = BrowserWindowTracker.getTopWindow().top.gBrowser;
let secondCategory = fxviewCategoryNav.categoryButtons[1];
let categoryChanged = BrowserTestUtils.waitForEvent(
gBrowser,
"change-category"
);
secondCategory.buttonEl.click();
await categoryChanged;
ok(
secondCategory.name === fxviewCategoryNav.currentCategory,
"The second category button is selected"
)
let thirdCategory = fxviewCategoryNav.categoryButtons[2];
categoryChanged = BrowserTestUtils.waitForEvent(
gBrowser,
"change-category"
);
thirdCategory.buttonEl.click();
await categoryChanged;
ok(
thirdCategory.name === fxviewCategoryNav.currentCategory,
"The third category button is selected"
)
let firstCategory = fxviewCategoryNav.categoryButtons[0];
categoryChanged = BrowserTestUtils.waitForEvent(
gBrowser,
"change-category"
);
firstCategory.buttonEl.click();
await categoryChanged;
ok(
firstCategory.name === fxviewCategoryNav.currentCategory,
"The first category button is selected"
)
});
/**
* Tests that categories are keyboard-navigable
*/
add_task(async function test_keyboard_navigation() {
const arrowDown = async () => {
info("Arrow down");
synthesizeKey("KEY_ArrowDown", {});
await fxviewCategoryNav.getUpdateComplete();
};
const arrowUp = async () => {
info("Arrow up");
synthesizeKey("KEY_ArrowUp", {});
await fxviewCategoryNav.getUpdateComplete();
};
const arrowLeft = async () => {
info("Arrow left");
synthesizeKey("KEY_ArrowLeft", {});
await fxviewCategoryNav.getUpdateComplete();
};
const arrowRight = async () => {
info("Arrow right");
synthesizeKey("KEY_ArrowRight", {});
await fxviewCategoryNav.getUpdateComplete();
};
// Setting this pref allows the test to run as expected with a keyboard on MacOS
await SpecialPowers.pushPrefEnv({
set: [["accessibility.tabfocus", 7]],
});
let firstCategory = fxviewCategoryNav.categoryButtons[0];
let secondCategory = fxviewCategoryNav.categoryButtons[1];
let thirdCategory = fxviewCategoryNav.categoryButtons[2];
let fourthCategory = fxviewCategoryNav.categoryButtons[3];
let fifthCategory = fxviewCategoryNav.categoryButtons[4];
is(
firstCategory.name,
fxviewCategoryNav.currentCategory,
"The first category button is selected"
)
firstCategory.focus();
await arrowDown();
ok(
isActiveElement(secondCategory),
"The second category button is the active element after first arrow down"
);
is(
secondCategory.name,
fxviewCategoryNav.currentCategory,
"The second category button is selected"
)
await arrowDown();
is(
thirdCategory.name,
fxviewCategoryNav.currentCategory,
"The third category button is selected"
)
await arrowDown();
is(
fourthCategory.name,
fxviewCategoryNav.currentCategory,
"The fourth category button is selected"
)
await arrowDown();
is(
fifthCategory.name,
fxviewCategoryNav.currentCategory,
"The fifth category button is selected"
)
await arrowDown();
is(
fifthCategory.name,
fxviewCategoryNav.currentCategory,
"The fifth category button is still selected"
)
await arrowUp();
is(
fourthCategory.name,
fxviewCategoryNav.currentCategory,
"The fourth category button is selected"
)
await arrowUp();
is(
thirdCategory.name,
fxviewCategoryNav.currentCategory,
"The third category button is selected"
)
await arrowUp();
is(
secondCategory.name,
fxviewCategoryNav.currentCategory,
"The second category button is selected"
)
await arrowUp();
is(
firstCategory.name,
fxviewCategoryNav.currentCategory,
"The first category button is selected"
)
await arrowUp();
is(
firstCategory.name,
fxviewCategoryNav.currentCategory,
"The first category button is still selected"
)
// Test navigation with arrow left/right keys
is(
firstCategory.name,
fxviewCategoryNav.currentCategory,
"The first category button is selected"
)
firstCategory.focus();
await arrowRight();
ok(
isActiveElement(secondCategory),
"The second category button is the active element after first arrow right"
);
is(
secondCategory.name,
fxviewCategoryNav.currentCategory,
"The second category button is selected"
)
await arrowRight();
is(
thirdCategory.name,
fxviewCategoryNav.currentCategory,
"The third category button is selected"
)
await arrowRight();
is(
fourthCategory.name,
fxviewCategoryNav.currentCategory,
"The fourth category button is selected"
)
await arrowRight();
is(
fifthCategory.name,
fxviewCategoryNav.currentCategory,
"The fifth category button is selected"
)
await arrowRight();
is(
fifthCategory.name,
fxviewCategoryNav.currentCategory,
"The fifth category button is still selected"
)
await arrowLeft();
is(
fourthCategory.name,
fxviewCategoryNav.currentCategory,
"The fourth category button is selected"
)
await arrowLeft();
is(
thirdCategory.name,
fxviewCategoryNav.currentCategory,
"The third category button is selected"
)
await arrowLeft();
is(
secondCategory.name,
fxviewCategoryNav.currentCategory,
"The second category button is selected"
)
await arrowLeft();
is(
firstCategory.name,
fxviewCategoryNav.currentCategory,
"The first category button is selected"
)
await arrowLeft();
is(
firstCategory.name,
fxviewCategoryNav.currentCategory,
"The first category button is still selected"
)
await SpecialPowers.popPrefEnv();
});
</script>
</body>
</html>

View file

@ -1,113 +0,0 @@
/* 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/. */
import { html } from "lit.all.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "browser/components/firefoxview/fxview-category-navigation.mjs";
export default {
title: "Domain-specific UI Widgets/Firefox View/Category Navigation",
component: "fxview-category-navigation",
parameters: {
status: "in-development",
actions: {
handles: ["change-category"],
},
fluent: `
fxview-category-button-one = Category 1
.title = Category 1
fxview-category-button-two = Category 2
.title = Category 2
fxview-category-button-three = Category 3
.title = Category 3
fxview-category-footer-button = Settings
.title = Settings
`,
},
};
const Template = () => html`
<style>
#page {
height: 100%;
display: grid;
grid-template-columns: var(--in-content-sidebar-width) 1fr;
}
fxview-category-navigation {
margin-inline-start: 10px;
}
fxview-category-button[name="category-one"]::part(icon) {
background-image: url("chrome://browser/skin/preferences/category-general.svg");
}
fxview-category-button[name="category-two"]::part(icon) {
background-image: url("chrome://browser/skin/preferences/category-general.svg");
}
fxview-category-button[name="category-three"]::part(icon) {
background-image: url("chrome://browser/skin/preferences/category-general.svg");
}
.footer-button {
display: flex;
gap: 12px;
font-weight: normal;
min-width: unset;
padding: 8px;
margin: 0;
}
.footer-button .cat-icon {
background-image: url("chrome://browser/skin/preferences/category-general.svg");
background-color: initial;
background-size: 20px;
background-repeat: no-repeat;
background-position: center;
height: 20px;
width: 20px;
display: inline-block;
-moz-context-properties: fill;
fill: currentColor;
}
@media (max-width: 52rem) {
#page {
grid-template-columns: 82px 1fr;
}
.cat-name {
display: none;
}
}
</style>
<div id="page">
<fxview-category-navigation>
<h2 slot="category-nav-header">Header</h2>
<fxview-category-button
slot="category-button"
name="category-one"
data-l10n-id="fxview-category-button-one"
>
</fxview-category-button>
<fxview-category-button
slot="category-button"
name="category-two"
data-l10n-id="fxview-category-button-two"
>
</fxview-category-button>
<fxview-category-button
slot="category-button"
name="category-three"
data-l10n-id="fxview-category-button-three"
>
</fxview-category-button>
<div slot="category-nav-footer" class="category-nav-footer">
<button class="footer-button ghost-button">
<span class="cat-icon"></span>
<span
class="cat-name"
data-l10n-id="fxview-category-footer-button"
></span>
</button>
</div>
</fxview-category-navigation>
</div>
`;
export const Default = Template.bind({});
Default.args = {};

View file

@ -12,6 +12,9 @@ menu-tools-firefox-view =
firefoxview-page-title = { -firefoxview-brand-name }
firefoxview-page-heading =
.heading = { -firefoxview-brand-name }
firefoxview-page-label =
.label = { -firefoxview-brand-name }

View file

@ -95,6 +95,9 @@ toolkit.jar:
content/global/elements/moz-button-group.mjs (widgets/moz-button-group/moz-button-group.mjs)
content/global/elements/moz-card.css (widgets/moz-card/moz-card.css)
content/global/elements/moz-card.mjs (widgets/moz-card/moz-card.mjs)
content/global/elements/moz-page-nav.css (widgets/moz-page-nav/moz-page-nav.css)
content/global/elements/moz-page-nav-button.css (widgets/moz-page-nav/moz-page-nav-button.css)
content/global/elements/moz-page-nav.mjs (widgets/moz-page-nav/moz-page-nav.mjs)
content/global/elements/moz-five-star.css (widgets/moz-five-star/moz-five-star.css)
content/global/elements/moz-five-star.mjs (widgets/moz-five-star/moz-five-star.mjs)
content/global/elements/moz-input-box.js (widgets/moz-input-box.js)

View file

@ -32,6 +32,8 @@ skip-if = ["os == 'mac'"]
["test_moz_message_bar.html"]
["test_moz_page_nav.html"]
["test_moz_support_link.html"]
["test_moz_toggle.html"]
@ -65,4 +67,5 @@ skip-if = [
"os == 'android'",
"os == 'linux' && debug", # Bug 1765783
]
["test_videocontrols_onclickplay.html"]

View file

@ -0,0 +1,306 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>MozPageNav Tests</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<script type="module" src="chrome://global/content/elements/moz-page-nav.mjs"></script>
</head>
<style>
body {
display: flex;
}
#navigation {
width: var(--page-nav-width);
}
</style>
<body>
<p id="display"></p>
<div id="content">
<div id="navigation">
<moz-page-nav heading="Heading">
<moz-page-nav-button view="view-one" iconSrc="chrome://mozapps/skin/extensions/category-discover.svg">
<span class="view-name">View 1</span>
</moz-page-nav-button>
<moz-page-nav-button view="view-two" iconSrc="chrome://mozapps/skin/extensions/category-discover.svg">
<span class="view-name">View 2</span>
</moz-page-nav-button>
<moz-page-nav-button view="view-three" iconSrc="chrome://mozapps/skin/extensions/category-discover.svg">
<span class="view-name">View 3</span>
</moz-page-nav-button>
<moz-page-nav-button view="view-four" iconSrc="chrome://mozapps/skin/extensions/category-discover.svg">
<span class="view-name">View 4</span>
</moz-page-nav-button>
<moz-page-nav-button view="view-five" iconSrc="chrome://mozapps/skin/extensions/category-discover.svg">
<span class="view-name">View 5</span>
</moz-page-nav-button>
</moz-page-nav>
</div>
</div>
<pre id="test"></pre>
<script>
Services.scriptloader.loadSubScript(
"chrome://browser/content/utilityOverlay.js",
this
);
const { BrowserTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
const mozPageNav = document.querySelector("moz-page-nav");
function isActiveElement(expectedActiveEl) {
return expectedActiveEl.getRootNode().activeElement == expectedActiveEl;
}
/**
* Tests that the first page nav button is selected by default
*/
add_task(async function test_first_item_selected_by_default() {
is(
mozPageNav.pageNavButtons.length,
5,
"Five page nav buttons are in the navigation"
);
ok(
mozPageNav.pageNavButtons[0].view === mozPageNav.currentView,
"The first page nav button is selected by default"
)
});
/**
* Tests that views are selected when clicked
*/
add_task(async function test_select_view() {
let gBrowser = BrowserWindowTracker.getTopWindow().top.gBrowser;
let secondViewButton = mozPageNav.pageNavButtons[1];
let viewChanged = BrowserTestUtils.waitForEvent(
gBrowser,
"change-view"
);
secondViewButton.buttonEl.click();
await viewChanged;
ok(
secondViewButton.view === mozPageNav.currentView,
"The second page nav button is selected"
)
let thirdPageNavButton = mozPageNav.pageNavButtons[2];
viewChanged = BrowserTestUtils.waitForEvent(
gBrowser,
"change-view"
);
thirdPageNavButton.buttonEl.click();
await viewChanged;
ok(
thirdPageNavButton.view === mozPageNav.currentView,
"The third page nav button is selected"
)
let firstPageNavButton = mozPageNav.pageNavButtons[0];
viewChanged = BrowserTestUtils.waitForEvent(
gBrowser,
"change-view"
);
firstPageNavButton.buttonEl.click();
await viewChanged;
ok(
firstPageNavButton.view === mozPageNav.currentView,
"The first page nav button is selected"
)
});
/**
* Tests that categories are keyboard-navigable
*/
add_task(async function test_keyboard_navigation() {
const arrowDown = async () => {
info("Arrow down");
synthesizeKey("KEY_ArrowDown", {});
await mozPageNav.updateComplete;
};
const arrowUp = async () => {
info("Arrow up");
synthesizeKey("KEY_ArrowUp", {});
await mozPageNav.updateComplete;
};
const arrowLeft = async () => {
info("Arrow left");
synthesizeKey("KEY_ArrowLeft", {});
await mozPageNav.updateComplete;
};
const arrowRight = async () => {
info("Arrow right");
synthesizeKey("KEY_ArrowRight", {});
await mozPageNav.updateComplete;
};
// Setting this pref allows the test to run as expected with a keyboard on MacOS
await SpecialPowers.pushPrefEnv({
set: [["accessibility.tabfocus", 7]],
});
let firstPageNavButton = mozPageNav.pageNavButtons[0];
let secondPageNavButton = mozPageNav.pageNavButtons[1];
let thirdPageNavButton = mozPageNav.pageNavButtons[2];
let fourthPageNavButton = mozPageNav.pageNavButtons[3];
let fifthPageNavButton = mozPageNav.pageNavButtons[4];
is(
firstPageNavButton.view,
mozPageNav.currentView,
"The first page nav button is selected"
)
firstPageNavButton.buttonEl.focus();
await arrowDown();
ok(
isActiveElement(secondPageNavButton),
"The second page nav button is the active element after first arrow down"
);
is(
secondPageNavButton.view,
mozPageNav.currentView,
"The second page nav button is selected"
)
await arrowDown();
is(
thirdPageNavButton.view,
mozPageNav.currentView,
"The third page nav button is selected"
)
await arrowDown();
is(
fourthPageNavButton.view,
mozPageNav.currentView,
"The fourth page nav button is selected"
)
await arrowDown();
is(
fifthPageNavButton.view,
mozPageNav.currentView,
"The fifth page nav button is selected"
)
await arrowDown();
is(
fifthPageNavButton.view,
mozPageNav.currentView,
"The fifth page nav button is still selected"
)
await arrowUp();
is(
fourthPageNavButton.view,
mozPageNav.currentView,
"The fourth page nav button is selected"
)
await arrowUp();
is(
thirdPageNavButton.view,
mozPageNav.currentView,
"The third page nav button is selected"
)
await arrowUp();
is(
secondPageNavButton.view,
mozPageNav.currentView,
"The second page nav button is selected"
)
await arrowUp();
is(
firstPageNavButton.view,
mozPageNav.currentView,
"The first page nav button is selected"
)
await arrowUp();
is(
firstPageNavButton.view,
mozPageNav.currentView,
"The first page nav button is still selected"
)
// Test navigation with arrow left/right keys
is(
firstPageNavButton.view,
mozPageNav.currentView,
"The first page nav button is selected"
)
firstPageNavButton.buttonEl.focus();
await arrowRight();
ok(
isActiveElement(secondPageNavButton),
"The second page nav button is the active element after first arrow right"
);
is(
secondPageNavButton.view,
mozPageNav.currentView,
"The second page nav button is selected"
)
await arrowRight();
is(
thirdPageNavButton.view,
mozPageNav.currentView,
"The third page nav button is selected"
)
await arrowRight();
is(
fourthPageNavButton.view,
mozPageNav.currentView,
"The fourth page nav button is selected"
)
await arrowRight();
is(
fifthPageNavButton.view,
mozPageNav.currentView,
"The fifth page nav button is selected"
)
await arrowRight();
is(
fifthPageNavButton.view,
mozPageNav.currentView,
"The fifth page nav button is still selected"
)
await arrowLeft();
is(
fourthPageNavButton.view,
mozPageNav.currentView,
"The fourth page nav button is selected"
)
await arrowLeft();
is(
thirdPageNavButton.view,
mozPageNav.currentView,
"The third page nav button is selected"
)
await arrowLeft();
is(
secondPageNavButton.view,
mozPageNav.currentView,
"The second page nav button is selected"
)
await arrowLeft();
is(
firstPageNavButton.view,
mozPageNav.currentView,
"The first page nav button is selected"
)
await arrowLeft();
is(
firstPageNavButton.view,
mozPageNav.currentView,
"The first page nav button is still selected"
)
await SpecialPowers.popPrefEnv();
});
</script>
</body>
</html>

View file

@ -3,12 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
:host {
border-radius: 4px;
border-radius: var(--border-radius-small);
&:focus-visible {
outline-offset: var(--page-nav-focus-outline-inset);
}
}
button {
background-color: initial;
border: 1px solid var(--in-content-primary-button-border-color);
background-color: var(--page-nav-button-background-color);
border: 1px solid var(--page-nav-border-color);
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
display: grid;
@ -18,11 +21,11 @@ button {
font-size: inherit;
width: 100%;
font-weight: normal;
border-radius: 4px;
color: inherit;
border-radius: var(--page-nav-button-border-radius);
color: var(--page-nav-button-text-color);
text-align: start;
transition: background-color 150ms;
padding: var(--fxviewcategorynav-button-padding);
padding: var(--page-nav-button-padding);
}
button:hover {
@ -38,8 +41,7 @@ button:hover {
button:hover,
button[selected]:hover {
background-color: var(--in-content-button-background-hover);
border-color: var(--in-content-button-border-color-hover);
background-color: var(--page-nav-button-background-color-hover);
}
button[selected]:hover {
@ -57,15 +59,15 @@ button:hover {
}
button[selected]:not(:hover) {
color: var(--in-content-accent-color);
background-color: color-mix(in srgb, var(--fxview-primary-action-background) 5%, transparent);
border-inline-start-color: var(--in-content-accent-color);
color: var(--color-accent-primary);
background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 5%, transparent);
border-inline-start-color: var(--color-accent-primary);
}
}
@media (prefers-color-scheme: dark) {
button[selected] {
background-color: color-mix(in srgb, var(--fxview-primary-action-background) 12%, transparent);
background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 12%, transparent);
}
}
@ -73,13 +75,10 @@ button:focus-visible,
button[selected]:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
border-radius: var(--border-radius-small);
}
.category-icon {
background-color: initial;
background-size: 20px;
background-repeat: no-repeat;
background-position: center;
.page-nav-icon {
height: 20px;
width: 20px;
-moz-context-properties: fill;
@ -90,7 +89,7 @@ button[selected]:focus-visible {
button {
transition: none;
border-color: ButtonText;
background-color: var(--in-content-button-background);
background-color: var(--button-background-color);
}
button:hover {
@ -105,8 +104,7 @@ button[selected]:focus-visible {
}
slot {
font-size: 1.13em;
line-height: 1.4;
font-size: var(--font-size-large);
margin: 0;
padding-inline-start: 0;
user-select: none;

View file

@ -0,0 +1,76 @@
/* 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/. */
:host {
--page-nav-button-border-radius: var(--button-border-radius);
--page-nav-button-text-color: var(--button-text-color);
--page-nav-button-background-color: transparent;
--page-nav-button-background-color-hover: var(--button-background-color-hover);
--page-nav-button-background-color-selected: var(--color-accent-primary);
--page-nav-button-padding: var(--space-small);
--page-nav-margin-top: 72px;
--page-nav-margin-bottom: 36px;
--page-nav-gap: 25px;
--page-nav-button-gap: var(--space-xsmall);
--page-nav-border-color: var(--border-color);
--page-nav-focus-outline-inset: var(--focus-outline-inset);
--page-nav-width: 240px;
margin-inline-start: 42px;
position: sticky;
top: 0;
height: 100vh;
width: var(--page-nav-width);
@media (prefers-reduced-motion) {
/* (See Bug 1610081) Setting border-inline-end to add clear differentiation between side navigation and main content area */
border-inline-end: 1px solid var(--page-nav-border-color);
}
@media (max-width: 52rem) {
grid-template-rows: 1fr auto;
--page-nav-width: 118px;
}
}
nav {
display: grid;
grid-template-rows: min-content 1fr auto;
gap: var(--page-nav-gap);
margin-block: var(--page-nav-margin-top) var(--page-nav-margin-bottom);
height: calc(100vh - var(--page-nav-margin-top) - var(--page-nav-margin-bottom));
@media (max-width: 52rem) {
grid-template-rows: 1fr auto;
}
}
.page-nav-header {
/* Align the header text/icon with the page nav button icons */
margin-inline-start: var(--page-nav-button-padding);
font-size: var(--font-size-xlarge);
font-weight: var(--font-weight-bold);
margin-block: 0;
@media (max-width: 52rem) {
display: none;
}
}
.primary-nav-group,
#secondary-nav-group {
display: grid;
grid-template-columns: 1fr;
grid-auto-rows: min-content;
gap: var(--page-nav-button-gap);
@media (max-width: 52rem) {
justify-content: center;
grid-template-columns: min-content;
}
}
@media (prefers-contrast) {
.primary-nav-group {
gap: var(--space-small);
}
}

View file

@ -0,0 +1,170 @@
/* 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/. */
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
/**
* A grouping of navigation buttons that is displayed at the page level,
* intended to change the selected view, provide a heading, and have links
* to external resources.
*
* @tagname moz-page-nav
* @property {string} currentView - The currently selected view.
* @property {string} heading - A heading to be displayed at the top of the navigation.
* @slot [default] - Used to append moz-page-nav-button elements to the navigation.
*/
export default class MozPageNav extends MozLitElement {
static properties = {
currentView: { type: String },
heading: { type: String },
};
static queries = {
headingEl: "#page-nav-header",
primaryNavGroupSlot: ".primary-nav-group slot",
secondaryNavGroupSlot: "#secondary-nav-group slot",
};
get pageNavButtons() {
return this.primaryNavGroupSlot
.assignedNodes()
.filter(
node => node?.localName === "moz-page-nav-button" && !node.hidden
);
}
onChangeView(e) {
this.currentView = e.target.view;
}
handleFocus(e) {
if (e.key == "ArrowDown" || e.key == "ArrowRight") {
e.preventDefault();
this.focusNextView();
} else if (e.key == "ArrowUp" || e.key == "ArrowLeft") {
e.preventDefault();
this.focusPreviousView();
}
}
focusPreviousView() {
let pageNavButtons = this.pageNavButtons;
let currentIndex = pageNavButtons.findIndex(b => b.selected);
let prev = pageNavButtons[currentIndex - 1];
if (prev) {
prev.activate();
prev.buttonEl.focus();
}
}
focusNextView() {
let pageNavButtons = this.pageNavButtons;
let currentIndex = pageNavButtons.findIndex(b => b.selected);
let next = pageNavButtons[currentIndex + 1];
if (next) {
next.activate();
next.buttonEl.focus();
}
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://global/content/elements/moz-page-nav.css"
/>
<nav>
<h1 class="page-nav-header" id="page-nav-header">${this.heading}</h1>
<div
class="primary-nav-group"
role="tablist"
aria-orientation="vertical"
aria-labelledby="page-nav-header"
>
<slot
@change-view=${this.onChangeView}
@keydown=${this.handleFocus}
></slot>
</div>
<div id="secondary-nav-group" role="group">
<slot name="secondary-nav" @keydown=${this.handleFocus}></slot>
</div>
</nav>
`;
}
updated() {
let isViewSelected = false;
let assignedPageNavButtons = this.pageNavButtons;
for (let button of assignedPageNavButtons) {
button.selected = button.view == this.currentView;
isViewSelected = isViewSelected || button.selected;
}
if (!isViewSelected && assignedPageNavButtons.length) {
// Current page nav has no matching view, reset to the first view.
assignedPageNavButtons[0].activate();
}
}
}
customElements.define("moz-page-nav", MozPageNav);
/**
* A navigation button intended to change the selected view within a page.
*
* @tagname moz-page-nav-button
* @property {string} iconSrc - The chrome:// url for the icon used for the button.
* @property {string} l10nId - The fluent ID for the button's text
* @property {boolean} selected - Whether or not the button is currently selected.
* @slot [default] - Used to append the l10n string to the button.
*/
export class MozPageNavButton extends MozLitElement {
static properties = {
iconSrc: { type: String },
l10nId: { type: String },
selected: { type: Boolean },
};
connectedCallback() {
super.connectedCallback();
this.setAttribute("role", "none");
}
static queries = {
buttonEl: "button",
};
get view() {
return this.getAttribute("view");
}
activate() {
this.dispatchEvent(
new CustomEvent("change-view", {
bubbles: true,
composed: true,
})
);
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://global/content/elements/moz-page-nav-button.css"
/>
<button
aria-selected=${this.selected}
tabindex=${this.selected ? 0 : -1}
role="tab"
?selected=${this.selected}
@click=${this.activate}
>
<img class="page-nav-icon" src=${this.iconSrc} />
<slot></slot>
</button>
`;
}
}
customElements.define("moz-page-nav-button", MozPageNavButton);

View file

@ -0,0 +1,77 @@
/* 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/. */
import { html } from "../vendor/lit.all.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "./moz-page-nav.mjs";
export default {
title: "UI Widgets/Page Nav",
component: "moz-page-nav",
parameters: {
status: "in-development",
actions: {
handles: ["change-view"],
},
fluent: `
moz-page-nav-button-one = View 1
.title = View 1
moz-page-nav-button-two = View 2
.title = View 2
moz-page-nav-button-three = View 3
.title = View 3
moz-page-link-one = Support Page
.title = Support Page
moz-page-nav-heading =
.heading = Heading
`,
},
};
const Template = () => html`
<style>
#page {
height: 100%;
display: flex;
@media (max-width: 52rem) {
grid-template-columns: 82px 1fr;
}
}
moz-page-nav {
margin-inline-start: 10px;
--page-nav-margin-top: 10px;
@media (max-width: 52rem) {
margin-inline-start: 0;
}
}
</style>
<div id="page">
<moz-page-nav data-l10n-id="moz-page-nav-heading" data-l10n-attrs="heading">
<moz-page-nav-button
view="view-one"
data-l10n-id="moz-page-nav-button-one"
iconSrc="chrome://browser/skin/preferences/category-general.svg"
>
</moz-page-nav-button>
<moz-page-nav-button
view="view-two"
data-l10n-id="moz-page-nav-button-two"
iconSrc="chrome://browser/skin/preferences/category-general.svg"
>
</moz-page-nav-button>
<moz-page-nav-button
view="view-three"
data-l10n-id="moz-page-nav-button-three"
iconSrc="chrome://browser/skin/preferences/category-general.svg"
>
</moz-page-nav-button>
</moz-page-nav>
<main></main>
</div>
`;
export const Default = Template.bind({});
Default.args = {};