forked from mirrors/gecko-dev
Bug 1845333 - Add submenu items to open tabs more menu r=mstriemer,fxview-reviewers,fluent-reviewers,desktop-theme-reviewers,reusable-components-reviewers,sfoster,flod,tgiles
* Modify panel-list and panel-item to support submenu list items * Add submenu items for Move Tabs and Send Tabs to Devices to open tabs * Add test coverage for submenu items in open tabs Differential Revision: https://phabricator.services.mozilla.com/D186471
This commit is contained in:
parent
603f4bb7b1
commit
24889f6a8c
13 changed files with 676 additions and 77 deletions
|
|
@ -163,6 +163,10 @@ panel-item::part(button):hover:active {
|
||||||
background-color: var(--fxview-element-background-active);
|
background-color: var(--fxview-element-background-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
panel-list {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
fxview-category-navigation {
|
fxview-category-navigation {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,19 @@ const lazy = {};
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
|
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
|
||||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||||
|
UIState: "resource://services-sync/UIState.sys.mjs",
|
||||||
|
TabsSetupFlowManager:
|
||||||
|
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
|
||||||
|
return ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/FxAccounts.sys.mjs"
|
||||||
|
).getFxAccountsSingleton();
|
||||||
|
});
|
||||||
|
|
||||||
|
const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of open tabs grouped by window.
|
* A collection of open tabs grouped by window.
|
||||||
*
|
*
|
||||||
|
|
@ -37,6 +48,8 @@ class OpenTabsInView extends ViewPage {
|
||||||
this.isPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(
|
this.isPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(
|
||||||
this.currentWindow
|
this.currentWindow
|
||||||
);
|
);
|
||||||
|
this.boundObserve = (...args) => this.observe(...args);
|
||||||
|
this.devices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -69,10 +82,48 @@ class OpenTabsInView extends ViewPage {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this._updateOpenTabsList();
|
this._updateOpenTabsList();
|
||||||
|
this.addObserversIfNeeded();
|
||||||
|
|
||||||
|
if (this.currentWindow.gSync) {
|
||||||
|
this.devices = this.currentWindow.gSync.getSendTabTargets();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
lazy.EveryWindow.unregisterCallback(this.everyWindowCallbackId);
|
lazy.EveryWindow.unregisterCallback(this.everyWindowCallbackId);
|
||||||
|
this.removeObserversIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
addObserversIfNeeded() {
|
||||||
|
if (!this.observerAdded) {
|
||||||
|
Services.obs.addObserver(this.boundObserve, lazy.UIState.ON_UPDATE);
|
||||||
|
Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
|
||||||
|
this.observerAdded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeObserversIfNeeded() {
|
||||||
|
if (this.observerAdded) {
|
||||||
|
Services.obs.removeObserver(this.boundObserve, lazy.UIState.ON_UPDATE);
|
||||||
|
Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
|
||||||
|
this.observerAdded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async observe(subject, topic, data) {
|
||||||
|
switch (topic) {
|
||||||
|
case lazy.UIState.ON_UPDATE:
|
||||||
|
if (!this.devices.length && lazy.TabsSetupFlowManager.fxaSignedIn) {
|
||||||
|
this.devices = this.currentWindow.gSync.getSendTabTargets();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TOPIC_DEVICELIST_UPDATED:
|
||||||
|
const deviceListUpdated =
|
||||||
|
await lazy.fxAccounts.device.refreshDeviceList();
|
||||||
|
if (deviceListUpdated) {
|
||||||
|
this.devices = this.currentWindow.gSync.getSendTabTargets();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -93,6 +144,7 @@ class OpenTabsInView extends ViewPage {
|
||||||
otherWindows.push([index++, tabs, win]);
|
otherWindows.push([index++, tabs, win]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cardClasses = classMap({
|
const cardClasses = classMap({
|
||||||
"height-limited": this.windows.size > 3,
|
"height-limited": this.windows.size > 3,
|
||||||
"width-limited": this.windows.size > 1,
|
"width-limited": this.windows.size > 1,
|
||||||
|
|
@ -130,6 +182,7 @@ class OpenTabsInView extends ViewPage {
|
||||||
data-l10n-args="${JSON.stringify({
|
data-l10n-args="${JSON.stringify({
|
||||||
winID: currentWindowIndex,
|
winID: currentWindowIndex,
|
||||||
})}"
|
})}"
|
||||||
|
.devices=${this.devices}
|
||||||
></view-opentabs-card>
|
></view-opentabs-card>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
|
@ -142,6 +195,7 @@ class OpenTabsInView extends ViewPage {
|
||||||
data-inner-id="${win.windowGlobalChild.innerWindowId}"
|
data-inner-id="${win.windowGlobalChild.innerWindowId}"
|
||||||
data-l10n-id="firefoxview-opentabs-window-header"
|
data-l10n-id="firefoxview-opentabs-window-header"
|
||||||
data-l10n-args="${JSON.stringify({ winID })}"
|
data-l10n-args="${JSON.stringify({ winID })}"
|
||||||
|
.devices=${this.devices}
|
||||||
></view-opentabs-card>
|
></view-opentabs-card>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
|
@ -150,8 +204,8 @@ class OpenTabsInView extends ViewPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a template for the 'Recent browsing' page, which shows a single list of
|
* Render a template for the 'Recent browsing' page, which shows a shorter list of
|
||||||
* recently accessed tabs, rather than a list of tabs per window.
|
* open tabs in the current window.
|
||||||
*
|
*
|
||||||
* @returns {TemplateResult}
|
* @returns {TemplateResult}
|
||||||
* The recent browsing template.
|
* The recent browsing template.
|
||||||
|
|
@ -163,6 +217,7 @@ class OpenTabsInView extends ViewPage {
|
||||||
return html`<view-opentabs-card
|
return html`<view-opentabs-card
|
||||||
.tabs=${tabs}
|
.tabs=${tabs}
|
||||||
.recentBrowsing=${true}
|
.recentBrowsing=${true}
|
||||||
|
.devices=${this.devices}
|
||||||
></view-opentabs-card>`;
|
></view-opentabs-card>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,6 +300,8 @@ class OpenTabsInViewCard extends ViewPage {
|
||||||
tabs: { type: Array },
|
tabs: { type: Array },
|
||||||
title: { type: String },
|
title: { type: String },
|
||||||
recentBrowsing: { type: Boolean },
|
recentBrowsing: { type: Boolean },
|
||||||
|
devices: { type: Array },
|
||||||
|
triggerNode: { type: Object },
|
||||||
};
|
};
|
||||||
static MAX_TABS_FOR_COMPACT_HEIGHT = 7;
|
static MAX_TABS_FOR_COMPACT_HEIGHT = 7;
|
||||||
|
|
||||||
|
|
@ -254,6 +311,7 @@ class OpenTabsInViewCard extends ViewPage {
|
||||||
this.tabs = [];
|
this.tabs = [];
|
||||||
this.title = "";
|
this.title = "";
|
||||||
this.recentBrowsing = false;
|
this.recentBrowsing = false;
|
||||||
|
this.devices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static queries = {
|
static queries = {
|
||||||
|
|
@ -264,11 +322,81 @@ class OpenTabsInViewCard extends ViewPage {
|
||||||
|
|
||||||
closeTab(e) {
|
closeTab(e) {
|
||||||
const tab = this.triggerNode.tabElement;
|
const tab = this.triggerNode.tabElement;
|
||||||
const browserWindow = tab.ownerGlobal;
|
tab?.ownerGlobal.gBrowser.removeTab(tab);
|
||||||
browserWindow.gBrowser.removeTab(tab, { animate: true });
|
|
||||||
this.recordContextMenuTelemetry("close-tab", e);
|
this.recordContextMenuTelemetry("close-tab", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveTabsToStart() {
|
||||||
|
const tab = this.triggerNode.tabElement;
|
||||||
|
tab?.ownerGlobal.gBrowser.moveTabsToStart(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTabsToEnd() {
|
||||||
|
const tab = this.triggerNode.tabElement;
|
||||||
|
tab?.ownerGlobal.gBrowser.moveTabsToEnd(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTabsToWindow() {
|
||||||
|
const tab = this.triggerNode.tabElement;
|
||||||
|
tab?.ownerGlobal.gBrowser.replaceTabsWithWindow(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveMenuTemplate() {
|
||||||
|
const tab = this.triggerNode?.tabElement;
|
||||||
|
const browserWindow = tab?.ownerGlobal;
|
||||||
|
const position = tab?._tPos;
|
||||||
|
const tabs = browserWindow?.gBrowser.tabs || [];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<panel-list slot="submenu" id="move-tab-menu">
|
||||||
|
${position > 0
|
||||||
|
? html`<panel-item
|
||||||
|
@click=${this.moveTabsToStart}
|
||||||
|
data-l10n-id="fxviewtabrow-move-tab-start"
|
||||||
|
data-l10n-attrs="accesskey"
|
||||||
|
></panel-item>`
|
||||||
|
: null}
|
||||||
|
${position < tabs.length - 1
|
||||||
|
? html`<panel-item
|
||||||
|
@click=${this.moveTabsToEnd}
|
||||||
|
data-l10n-id="fxviewtabrow-move-tab-end"
|
||||||
|
data-l10n-attrs="accesskey"
|
||||||
|
></panel-item>`
|
||||||
|
: null}
|
||||||
|
<panel-item
|
||||||
|
@click=${this.moveTabsToWindow}
|
||||||
|
data-l10n-id="fxviewtabrow-move-tab-window"
|
||||||
|
data-l10n-attrs="accesskey"
|
||||||
|
></panel-item>
|
||||||
|
</panel-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTabToDevice(e) {
|
||||||
|
let deviceId = e.target.getAttribute("device-id");
|
||||||
|
let device = this.devices.find(dev => dev.id == deviceId);
|
||||||
|
|
||||||
|
if (device && this.triggerNode) {
|
||||||
|
await this.getWindow().gSync.sendTabToDevice(
|
||||||
|
this.triggerNode.url,
|
||||||
|
[device],
|
||||||
|
this.triggerNode.title
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTabTemplate() {
|
||||||
|
return html` <panel-list slot="submenu" id="send-tab-menu">
|
||||||
|
${this.devices.map(device => {
|
||||||
|
return html`
|
||||||
|
<panel-item @click=${this.sendTabToDevice} device-id=${device.id}
|
||||||
|
>${device.name}</panel-item
|
||||||
|
>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</panel-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
panelListTemplate() {
|
panelListTemplate() {
|
||||||
return html`
|
return html`
|
||||||
<panel-list slot="menu" data-tab-type="opentabs">
|
<panel-list slot="menu" data-tab-type="opentabs">
|
||||||
|
|
@ -277,11 +405,27 @@ class OpenTabsInViewCard extends ViewPage {
|
||||||
data-l10n-attrs="accesskey"
|
data-l10n-attrs="accesskey"
|
||||||
@click=${this.closeTab}
|
@click=${this.closeTab}
|
||||||
></panel-item>
|
></panel-item>
|
||||||
|
<panel-item
|
||||||
|
data-l10n-id="fxviewtabrow-move-tab"
|
||||||
|
data-l10n-attrs="accesskey"
|
||||||
|
submenu="move-tab-menu"
|
||||||
|
>
|
||||||
|
${this.moveMenuTemplate()}
|
||||||
|
</panel-item>
|
||||||
|
<hr />
|
||||||
<panel-item
|
<panel-item
|
||||||
data-l10n-id="fxviewtabrow-copy-link"
|
data-l10n-id="fxviewtabrow-copy-link"
|
||||||
data-l10n-attrs="accesskey"
|
data-l10n-attrs="accesskey"
|
||||||
@click=${this.copyLink}
|
@click=${this.copyLink}
|
||||||
></panel-item>
|
></panel-item>
|
||||||
|
${this.devices.length >= 1
|
||||||
|
? html`<panel-item
|
||||||
|
data-l10n-id="fxviewtabrow-send-tab"
|
||||||
|
data-l10n-attrs="accesskey"
|
||||||
|
submenu="send-tab-menu"
|
||||||
|
>${this.sendTabTemplate()}</panel-item
|
||||||
|
>`
|
||||||
|
: null}
|
||||||
</panel-list>
|
</panel-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ async function setupWithDesktopDevices(state = UIState.STATUS_SIGNED_IN) {
|
||||||
return sandbox;
|
return sandbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promiseSyncReady() {
|
||||||
|
let service = Cc["@mozilla.org/weave/service;1"].getService(
|
||||||
|
Ci.nsISupports
|
||||||
|
).wrappedJSObject;
|
||||||
|
return service.whenLoaded();
|
||||||
|
}
|
||||||
async function tearDown(sandbox) {
|
async function tearDown(sandbox) {
|
||||||
sandbox?.restore();
|
sandbox?.restore();
|
||||||
Services.prefs.clearUserPref("services.sync.lastTabFetch");
|
Services.prefs.clearUserPref("services.sync.lastTabFetch");
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ support-files = ../head.js
|
||||||
[browser_firefoxview_next.js]
|
[browser_firefoxview_next.js]
|
||||||
[browser_firefoxview_next_general_telemetry.js]
|
[browser_firefoxview_next_general_telemetry.js]
|
||||||
[browser_history_firefoxview_next.js]
|
[browser_history_firefoxview_next.js]
|
||||||
|
[browser_opentabs_firefoxview_next.js]
|
||||||
[browser_recentlyclosed_firefoxview_next.js]
|
[browser_recentlyclosed_firefoxview_next.js]
|
||||||
[browser_syncedtabs_errors_firefoxview_next.js]
|
[browser_syncedtabs_errors_firefoxview_next.js]
|
||||||
[browser_syncedtabs_firefoxview_next.js]
|
[browser_syncedtabs_firefoxview_next.js]
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,11 @@ add_task(async function test_context_menu_telemetry() {
|
||||||
await EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content);
|
await EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content);
|
||||||
await BrowserTestUtils.waitForEvent(panelList, "shown");
|
await BrowserTestUtils.waitForEvent(panelList, "shown");
|
||||||
await clearAllParentTelemetryEvents();
|
await clearAllParentTelemetryEvents();
|
||||||
let copyLinkOption = panelList.children[1];
|
let copyLinkOption = panelList.querySelector(
|
||||||
|
"panel-item[data-l10n-id=fxviewtabrow-copy-link]"
|
||||||
|
);
|
||||||
|
ok(copyLinkOption, "Copy link panel item exists");
|
||||||
|
|
||||||
let contextMenuEvent = [
|
let contextMenuEvent = [
|
||||||
[
|
[
|
||||||
"firefoxview_next",
|
"firefoxview_next",
|
||||||
|
|
|
||||||
|
|
@ -27,26 +27,6 @@ oneMonthAgo.setMonth(
|
||||||
oneMonthAgo.getMonth() === 0 ? 11 : oneMonthAgo.getMonth() - 1
|
oneMonthAgo.getMonth() === 0 ? 11 : oneMonthAgo.getMonth() - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
function isElInViewport(element) {
|
|
||||||
const boundingRect = element.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
boundingRect.top >= 0 &&
|
|
||||||
boundingRect.left >= 0 &&
|
|
||||||
boundingRect.bottom <=
|
|
||||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
||||||
boundingRect.right <=
|
|
||||||
(window.innerWidth || document.documentElement.clientWidth)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openFirefoxView(win) {
|
|
||||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
|
||||||
"#firefox-view-button",
|
|
||||||
{ type: "mousedown" },
|
|
||||||
win.browsingContext
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addHistoryItems(dateAdded) {
|
async function addHistoryItems(dateAdded) {
|
||||||
await PlacesUtils.history.insert({
|
await PlacesUtils.history.insert({
|
||||||
url: URLs[0],
|
url: URLs[0],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/* import-globals-from ../head.js */
|
||||||
|
|
||||||
|
const TEST_URL1 = "about:robot";
|
||||||
|
const TEST_URL2 = "https://example.org/";
|
||||||
|
const TEST_URL3 = "about:mozilla";
|
||||||
|
|
||||||
|
const fxaDevicesWithCommands = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "My desktop device",
|
||||||
|
availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "test" },
|
||||||
|
lastAccessTime: Date.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "My mobile device",
|
||||||
|
availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "boo" },
|
||||||
|
lastAccessTime: Date.now() + 60000, // add 30min
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCards(openTabs) {
|
||||||
|
return openTabs.shadowRoot.querySelectorAll("view-opentabs-card");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowsForCard(card) {
|
||||||
|
return card.tabList.rowEls;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moreMenuSetup(document) {
|
||||||
|
await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL2);
|
||||||
|
await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL3);
|
||||||
|
|
||||||
|
// once we've opened a few tabs, navigate to the open tabs section in firefox view
|
||||||
|
await clickFirefoxViewButton(window);
|
||||||
|
navigateToCategory(document, "opentabs");
|
||||||
|
|
||||||
|
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
|
||||||
|
|
||||||
|
let cards;
|
||||||
|
await TestUtils.waitForCondition(() => {
|
||||||
|
cards = getCards(openTabs);
|
||||||
|
return cards.length == 1;
|
||||||
|
});
|
||||||
|
is(cards.length, 1, "There is one open window.");
|
||||||
|
|
||||||
|
let rows = getRowsForCard(cards[0]);
|
||||||
|
is(rows.length, 3, "There are three tabs in the open tabs list.");
|
||||||
|
|
||||||
|
let firstTab = rows[0];
|
||||||
|
|
||||||
|
firstTab.scrollIntoView();
|
||||||
|
is(
|
||||||
|
isElInViewport(firstTab),
|
||||||
|
true,
|
||||||
|
"first tab list item is visible in viewport"
|
||||||
|
);
|
||||||
|
|
||||||
|
return [cards, rows];
|
||||||
|
}
|
||||||
|
add_task(async function test_more_menus() {
|
||||||
|
await withFirefoxView({}, async browser => {
|
||||||
|
const { document } = browser.contentWindow;
|
||||||
|
let win = browser.ownerGlobal;
|
||||||
|
|
||||||
|
gBrowser.selectedTab = gBrowser.visibleTabs[0];
|
||||||
|
ok(
|
||||||
|
gBrowser.selectedTab.linkedBrowser.currentURI.spec == "about:blank",
|
||||||
|
"Selected tab is about:blank"
|
||||||
|
);
|
||||||
|
|
||||||
|
win.gURLBar.focus();
|
||||||
|
win.gURLBar.value = TEST_URL1;
|
||||||
|
EventUtils.synthesizeKey("KEY_Enter", {}, win);
|
||||||
|
|
||||||
|
let [cards, rows] = await moreMenuSetup(document);
|
||||||
|
|
||||||
|
let firstTab = rows[0];
|
||||||
|
let panelList = cards[0].shadowRoot.querySelector(
|
||||||
|
"panel-list:not([submenu])"
|
||||||
|
);
|
||||||
|
|
||||||
|
// click on the first list items button element (more menu)
|
||||||
|
// and wait for the panel list to be shown
|
||||||
|
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
|
||||||
|
firstTab.buttonEl.click();
|
||||||
|
await shown;
|
||||||
|
|
||||||
|
// Close Tab menu item
|
||||||
|
info("Panel list shown. Clicking on panel-item");
|
||||||
|
let panelItem = cards[0].shadowRoot.querySelector(
|
||||||
|
"panel-item[data-l10n-id=fxviewtabrow-close-tab]"
|
||||||
|
);
|
||||||
|
ok(panelItem, "Close Tab panel item exists");
|
||||||
|
|
||||||
|
// close a tab via the menu
|
||||||
|
panelItem.click();
|
||||||
|
|
||||||
|
let visibleTabs = gBrowser.visibleTabs;
|
||||||
|
is(visibleTabs.length, 2, "Expected to now have 2 open tabs");
|
||||||
|
await cards[0].getUpdateComplete();
|
||||||
|
|
||||||
|
// Move Tab submenu item
|
||||||
|
firstTab = rows[0];
|
||||||
|
is(firstTab.url, TEST_URL2, `First tab list item is ${TEST_URL2}`);
|
||||||
|
|
||||||
|
is(
|
||||||
|
visibleTabs[0].linkedBrowser.currentURI.spec,
|
||||||
|
TEST_URL2,
|
||||||
|
`First tab in tab strip is ${TEST_URL2}`
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
visibleTabs[visibleTabs.length - 1].linkedBrowser.currentURI.spec,
|
||||||
|
TEST_URL3,
|
||||||
|
`Last tab in tab strip is ${TEST_URL3}`
|
||||||
|
);
|
||||||
|
|
||||||
|
let moveTabsPanelItem = cards[0].shadowRoot.querySelector(
|
||||||
|
"panel-item[data-l10n-id=fxviewtabrow-move-tab]"
|
||||||
|
);
|
||||||
|
|
||||||
|
let moveTabsSubmenuList = moveTabsPanelItem.shadowRoot.querySelector(
|
||||||
|
"panel-list[id=move-tab-menu]"
|
||||||
|
);
|
||||||
|
ok(moveTabsSubmenuList, "Move tabs submenu panel list exists");
|
||||||
|
|
||||||
|
// click on the first list items button element (more menu)
|
||||||
|
// and wait for the panel list to be shown again
|
||||||
|
shown = BrowserTestUtils.waitForEvent(panelList, "shown");
|
||||||
|
firstTab.buttonEl.click();
|
||||||
|
await shown;
|
||||||
|
|
||||||
|
// navigate down to the "Move tabs" submenu option, and
|
||||||
|
// open it with the right arrow key
|
||||||
|
EventUtils.synthesizeKey("KEY_ArrowDown", {});
|
||||||
|
shown = BrowserTestUtils.waitForEvent(moveTabsSubmenuList, "shown");
|
||||||
|
EventUtils.synthesizeKey("KEY_ArrowRight", {});
|
||||||
|
await shown;
|
||||||
|
|
||||||
|
// click on the first option, which should be "Move to the end" since
|
||||||
|
// this is the first tab
|
||||||
|
EventUtils.synthesizeKey("KEY_Enter", {});
|
||||||
|
|
||||||
|
visibleTabs = gBrowser.visibleTabs;
|
||||||
|
is(
|
||||||
|
visibleTabs[0].linkedBrowser.currentURI.spec,
|
||||||
|
TEST_URL3,
|
||||||
|
`First tab in tab strip is now ${TEST_URL3}`
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
visibleTabs[visibleTabs.length - 1].linkedBrowser.currentURI.spec,
|
||||||
|
TEST_URL2,
|
||||||
|
`Last tab in tab strip is now ${TEST_URL2}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// this entire "move tabs" submenu test can be reordered above
|
||||||
|
// closing a tab since it very clearly reveals the issues
|
||||||
|
// outlined in bug 1852622 when there are 3 or more tabs open
|
||||||
|
// and one is moved via the more menus.
|
||||||
|
await BrowserTestUtils.waitForMutationCondition(
|
||||||
|
cards[0].shadowRoot,
|
||||||
|
{ characterData: true, childList: true, subtree: true },
|
||||||
|
() => {
|
||||||
|
rows = getRowsForCard(cards[0]);
|
||||||
|
firstTab = rows[0];
|
||||||
|
return firstTab.url == TEST_URL3;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy Link menu item (copyLink function that's called is a member of Viewpage.mjs)
|
||||||
|
shown = BrowserTestUtils.waitForEvent(panelList, "shown");
|
||||||
|
firstTab.buttonEl.click();
|
||||||
|
await shown;
|
||||||
|
|
||||||
|
panelItem = cards[0].shadowRoot.querySelector(
|
||||||
|
"panel-item[data-l10n-id=fxviewtabrow-copy-link]"
|
||||||
|
);
|
||||||
|
ok(panelItem, "Copy link panel item exists");
|
||||||
|
panelItem.click();
|
||||||
|
|
||||||
|
let copiedText = SpecialPowers.getClipboardData(
|
||||||
|
"text/plain",
|
||||||
|
Ci.nsIClipboard.kGlobalClipboard
|
||||||
|
);
|
||||||
|
is(copiedText, TEST_URL3, "The correct url has been copied and pasted");
|
||||||
|
|
||||||
|
while (gBrowser.tabs.length > 1) {
|
||||||
|
BrowserTestUtils.removeTab(gBrowser.tabs[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_send_device_submenu() {
|
||||||
|
const sandbox = setupMocks({
|
||||||
|
state: UIState.STATUS_SIGNED_IN,
|
||||||
|
fxaDevices: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "This Device",
|
||||||
|
isCurrentDevice: true,
|
||||||
|
type: "desktop",
|
||||||
|
tabs: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
sandbox
|
||||||
|
.stub(gSync, "getSendTabTargets")
|
||||||
|
.callsFake(() => fxaDevicesWithCommands);
|
||||||
|
|
||||||
|
await withFirefoxView({}, async browser => {
|
||||||
|
const { document } = browser.contentWindow;
|
||||||
|
|
||||||
|
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
|
||||||
|
let [cards, rows] = await moreMenuSetup(document);
|
||||||
|
|
||||||
|
let firstTab = rows[0];
|
||||||
|
let panelList = cards[0].shadowRoot.querySelector(
|
||||||
|
"panel-list:not([submenu])"
|
||||||
|
);
|
||||||
|
|
||||||
|
await cards[0].getUpdateComplete();
|
||||||
|
|
||||||
|
// click on the first list items button element (more menu)
|
||||||
|
// and wait for the panel list to be shown
|
||||||
|
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
|
||||||
|
firstTab.buttonEl.click();
|
||||||
|
await shown;
|
||||||
|
|
||||||
|
let sendTabPanelItem = cards[0].shadowRoot.querySelector(
|
||||||
|
"panel-item[data-l10n-id=fxviewtabrow-send-tab]"
|
||||||
|
);
|
||||||
|
|
||||||
|
ok(sendTabPanelItem, "Send tabs to device submenu panel item exists");
|
||||||
|
|
||||||
|
let sendTabSubmenuList = sendTabPanelItem.shadowRoot.querySelector(
|
||||||
|
"panel-list[id=send-tab-menu]"
|
||||||
|
);
|
||||||
|
ok(sendTabSubmenuList, "Send tabs to device submenu panel list exists");
|
||||||
|
|
||||||
|
// navigate down to the "Send tabs" submenu option, and
|
||||||
|
// open it with the right arrow key
|
||||||
|
EventUtils.synthesizeKey("KEY_ArrowDown", {});
|
||||||
|
EventUtils.synthesizeKey("KEY_ArrowDown", {});
|
||||||
|
EventUtils.synthesizeKey("KEY_ArrowDown", {});
|
||||||
|
|
||||||
|
shown = BrowserTestUtils.waitForEvent(sendTabSubmenuList, "shown");
|
||||||
|
EventUtils.synthesizeKey("KEY_ArrowRight", {});
|
||||||
|
await shown;
|
||||||
|
|
||||||
|
let expectation = sandbox
|
||||||
|
.mock(gSync)
|
||||||
|
.expects("sendTabToDevice")
|
||||||
|
.once()
|
||||||
|
.withExactArgs(
|
||||||
|
TEST_URL2,
|
||||||
|
[fxaDevicesWithCommands[0]],
|
||||||
|
"mochitest index /"
|
||||||
|
)
|
||||||
|
.returns(true);
|
||||||
|
|
||||||
|
// click on the first device and verify it was "sent"
|
||||||
|
EventUtils.synthesizeKey("KEY_Enter", {});
|
||||||
|
expectation.verify();
|
||||||
|
|
||||||
|
sandbox.restore();
|
||||||
|
TabsSetupFlowManager.resetInternalState();
|
||||||
|
while (gBrowser.tabs.length > 1) {
|
||||||
|
BrowserTestUtils.removeTab(gBrowser.tabs[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
/* import-globals-from ../head.js */
|
/* import-globals-from ../head.js */
|
||||||
|
|
||||||
|
requestLongerTimeout(2);
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(globalThis, {
|
ChromeUtils.defineESModuleGetters(globalThis, {
|
||||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||||
});
|
});
|
||||||
|
|
@ -17,26 +19,6 @@ const DISMISS_CLOSED_TAB_EVENT = [
|
||||||
];
|
];
|
||||||
const initialTab = gBrowser.selectedTab;
|
const initialTab = gBrowser.selectedTab;
|
||||||
|
|
||||||
function isElInViewport(element) {
|
|
||||||
const boundingRect = element.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
boundingRect.top >= 0 &&
|
|
||||||
boundingRect.left >= 0 &&
|
|
||||||
boundingRect.bottom <=
|
|
||||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
||||||
boundingRect.right <=
|
|
||||||
(window.innerWidth || document.documentElement.clientWidth)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openFirefoxView(win) {
|
|
||||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
|
||||||
"#firefox-view-button",
|
|
||||||
{ type: "mousedown" },
|
|
||||||
win.browsingContext
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForRecentlyClosedTabsList(doc) {
|
async function waitForRecentlyClosedTabsList(doc) {
|
||||||
let recentlyClosedComponent = doc.querySelector(
|
let recentlyClosedComponent = doc.querySelector(
|
||||||
"view-recentlyclosed:not([slot=recentlyclosed])"
|
"view-recentlyclosed:not([slot=recentlyclosed])"
|
||||||
|
|
@ -313,7 +295,7 @@ add_task(async function test_list_updates() {
|
||||||
);
|
);
|
||||||
SessionStore.undoCloseById(closedTabItem.closedId);
|
SessionStore.undoCloseById(closedTabItem.closedId);
|
||||||
await promiseClosedObjectsChanged;
|
await promiseClosedObjectsChanged;
|
||||||
await openFirefoxView(window);
|
await clickFirefoxViewButton(window);
|
||||||
|
|
||||||
// we expect the last item to be removed
|
// we expect the last item to be removed
|
||||||
expectedURLs.pop();
|
expectedURLs.pop();
|
||||||
|
|
@ -339,7 +321,7 @@ add_task(async function test_list_updates() {
|
||||||
);
|
);
|
||||||
SessionStore.forgetClosedWindowById(closedTabItem.sourceClosedId);
|
SessionStore.forgetClosedWindowById(closedTabItem.sourceClosedId);
|
||||||
await promiseClosedObjectsChanged;
|
await promiseClosedObjectsChanged;
|
||||||
await openFirefoxView(window);
|
await clickFirefoxViewButton(window);
|
||||||
|
|
||||||
listItems = listElem.rowEls;
|
listItems = listElem.rowEls;
|
||||||
expectedURLs.shift(); // we expect to have removed the firsts URL from the list
|
expectedURLs.shift(); // we expect to have removed the firsts URL from the list
|
||||||
|
|
@ -378,7 +360,7 @@ add_task(async function test_restore_tab() {
|
||||||
await clearAllParentTelemetryEvents();
|
await clearAllParentTelemetryEvents();
|
||||||
await restore_tab(closeTabItem, browser, closeTabItem.url);
|
await restore_tab(closeTabItem, browser, closeTabItem.url);
|
||||||
await recentlyClosedTelemetry();
|
await recentlyClosedTelemetry();
|
||||||
await openFirefoxView(window);
|
await clickFirefoxViewButton(window);
|
||||||
|
|
||||||
listItems = listElem.rowEls;
|
listItems = listElem.rowEls;
|
||||||
is(listItems.length, 3, "Three tabs are shown in the list.");
|
is(listItems.length, 3, "Three tabs are shown in the list.");
|
||||||
|
|
@ -387,7 +369,7 @@ add_task(async function test_restore_tab() {
|
||||||
await clearAllParentTelemetryEvents();
|
await clearAllParentTelemetryEvents();
|
||||||
await restore_tab(closeTabItem, browser, closeTabItem.url);
|
await restore_tab(closeTabItem, browser, closeTabItem.url);
|
||||||
await recentlyClosedTelemetry();
|
await recentlyClosedTelemetry();
|
||||||
await openFirefoxView(window);
|
await clickFirefoxViewButton(window);
|
||||||
|
|
||||||
listItems = listElem.rowEls;
|
listItems = listElem.rowEls;
|
||||||
is(listItems.length, 2, "Two tabs are shown in the list.");
|
is(listItems.length, 2, "Two tabs are shown in the list.");
|
||||||
|
|
|
||||||
|
|
@ -605,3 +605,25 @@ function navigateToCategory(document, category) {
|
||||||
async function switchToFxViewTab(win = window) {
|
async function switchToFxViewTab(win = window) {
|
||||||
return BrowserTestUtils.switchTab(win.gBrowser, win.FirefoxViewHandler.tab);
|
return BrowserTestUtils.switchTab(win.gBrowser, win.FirefoxViewHandler.tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isElInViewport(element) {
|
||||||
|
const boundingRect = element.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
boundingRect.top >= 0 &&
|
||||||
|
boundingRect.left >= 0 &&
|
||||||
|
boundingRect.bottom <=
|
||||||
|
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
boundingRect.right <=
|
||||||
|
(window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO once we port over old tests, helpers and cleanup old firefox view
|
||||||
|
// we should decide whether to keep this or openFirefoxViewTab.
|
||||||
|
async function clickFirefoxViewButton(win) {
|
||||||
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
|
"#firefox-view-button",
|
||||||
|
{ type: "mousedown" },
|
||||||
|
win.browsingContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,16 @@ fxviewtabrow-copy-link = Copy Link
|
||||||
.accesskey = L
|
.accesskey = L
|
||||||
fxviewtabrow-close-tab = Close Tab
|
fxviewtabrow-close-tab = Close Tab
|
||||||
.accesskey = C
|
.accesskey = C
|
||||||
|
fxviewtabrow-move-tab = Move Tab
|
||||||
|
.accesskey = v
|
||||||
|
fxviewtabrow-move-tab-start = Move to Start
|
||||||
|
.accesskey = S
|
||||||
|
fxviewtabrow-move-tab-end = Move to End
|
||||||
|
.accesskey = E
|
||||||
|
fxviewtabrow-move-tab-window = Move to New Window
|
||||||
|
.accesskey = W
|
||||||
|
fxviewtabrow-send-tab = Send Tab to Device
|
||||||
|
.accesskey = n
|
||||||
|
|
||||||
# Variables:
|
# Variables:
|
||||||
# $tabTitle (string) - Title of the tab to which the context menu is associated
|
# $tabTitle (string) - Title of the tab to which the context menu is associated
|
||||||
|
|
|
||||||
|
|
@ -70,3 +70,27 @@ button:focus-visible {
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submenu-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-icon {
|
||||||
|
display: inline-block;
|
||||||
|
background-image: url("chrome://global/skin/icons/arrow-right.svg");
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
fill: currentColor;
|
||||||
|
width: var(--size-item-small);
|
||||||
|
height: var(--size-item-small);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
&:dir(rtl) {
|
||||||
|
background-image: url("chrome://global/skin/icons/arrow-left.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-label {
|
||||||
|
flex: 90% 1 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,13 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:not([slot=submenu])) {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:host([stay-open]) {
|
:host([stay-open]) {
|
||||||
position: initial;
|
position: initial;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,14 @@
|
||||||
this.toggleAttribute("open", val);
|
this.toggleAttribute("open", val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get stayOpen() {
|
||||||
|
return this.hasAttribute("stay-open");
|
||||||
|
}
|
||||||
|
|
||||||
|
set stayOpen(val) {
|
||||||
|
this.toggleAttribute("stay-open", val);
|
||||||
|
}
|
||||||
|
|
||||||
getTargetForEvent(event) {
|
getTargetForEvent(event) {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -77,13 +85,17 @@
|
||||||
return event._savedComposedTarget || event.target;
|
return event._savedComposedTarget || event.target;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(triggeringEvent) {
|
show(triggeringEvent, target) {
|
||||||
this.triggeringEvent = triggeringEvent;
|
this.triggeringEvent = triggeringEvent;
|
||||||
this.lastAnchorNode = this.getTargetForEvent(this.triggeringEvent);
|
this.lastAnchorNode =
|
||||||
|
target || this.getTargetForEvent(this.triggeringEvent);
|
||||||
|
|
||||||
this.wasOpenedByKeyboard =
|
this.wasOpenedByKeyboard =
|
||||||
triggeringEvent &&
|
triggeringEvent &&
|
||||||
(triggeringEvent.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD ||
|
(triggeringEvent.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD ||
|
||||||
triggeringEvent.inputSource == MouseEvent.MOZ_SOURCE_UNKNOWN);
|
triggeringEvent.inputSource == MouseEvent.MOZ_SOURCE_UNKNOWN ||
|
||||||
|
triggeringEvent.code == "ArrowRight" ||
|
||||||
|
triggeringEvent.code == "ArrowLeft");
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
|
||||||
if (this.parentIsXULPanel()) {
|
if (this.parentIsXULPanel()) {
|
||||||
|
|
@ -112,7 +124,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(triggeringEvent, { force = false } = {}) {
|
hide(triggeringEvent, { force = false } = {}, eventTarget) {
|
||||||
// It's possible this is being used in an unprivileged context, in which
|
// It's possible this is being used in an unprivileged context, in which
|
||||||
// case it won't have access to Services / Services will be undeclared.
|
// case it won't have access to Services / Services will be undeclared.
|
||||||
const autohideDisabled = this.hasServices()
|
const autohideDisabled = this.hasServices()
|
||||||
|
|
@ -136,18 +148,18 @@
|
||||||
panel.hidePopup();
|
panel.hidePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = this.getTargetForEvent(openingEvent);
|
let target = eventTarget || this.getTargetForEvent(openingEvent);
|
||||||
// Refocus the button that opened the menu if we have one.
|
// Refocus the button that opened the menu if we have one.
|
||||||
if (target && this.wasOpenedByKeyboard) {
|
if (target && this.wasOpenedByKeyboard) {
|
||||||
target.focus();
|
target.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(triggeringEvent) {
|
toggle(triggeringEvent, target = null) {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
this.hide(triggeringEvent, { force: true });
|
this.hide(triggeringEvent, { force: true }, target);
|
||||||
} else {
|
} else {
|
||||||
this.show(triggeringEvent);
|
this.show(triggeringEvent, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,12 +288,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
addHideListeners() {
|
addHideListeners() {
|
||||||
if (this.hasAttribute("stay-open")) {
|
if (this.hasAttribute("stay-open") && !this.lastAnchorNode.hasSubmenu) {
|
||||||
// This is intended for inspection in Storybook.
|
// This is intended for inspection in Storybook.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Hide when a panel-item is clicked in the list.
|
// Hide when a panel-item is clicked in the list.
|
||||||
this.addEventListener("click", this);
|
this.addEventListener("click", this);
|
||||||
|
// Allows submenus to stopPropagation when focus is already in the menu
|
||||||
|
this.addEventListener("keydown", this);
|
||||||
|
// We need Escape/Tab/ArrowDown to work when opened with the mouse.
|
||||||
document.addEventListener("keydown", this);
|
document.addEventListener("keydown", this);
|
||||||
// Hide when a click is initiated outside the panel.
|
// Hide when a click is initiated outside the panel.
|
||||||
document.addEventListener("mousedown", this);
|
document.addEventListener("mousedown", this);
|
||||||
|
|
@ -300,6 +315,7 @@
|
||||||
|
|
||||||
removeHideListeners() {
|
removeHideListeners() {
|
||||||
this.removeEventListener("click", this);
|
this.removeEventListener("click", this);
|
||||||
|
this.removeEventListener("keydown", this);
|
||||||
document.removeEventListener("keydown", this);
|
document.removeEventListener("keydown", this);
|
||||||
document.removeEventListener("mousedown", this);
|
document.removeEventListener("mousedown", this);
|
||||||
document.removeEventListener("focusin", this);
|
document.removeEventListener("focusin", this);
|
||||||
|
|
@ -357,19 +373,15 @@
|
||||||
// Don't scroll the page or let the regular tab order take effect.
|
// Don't scroll the page or let the regular tab order take effect.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Prevents the host panel list from responding to these events while
|
||||||
|
// the submenu is active.
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
// Keep moving to the next/previous element sibling until we find a
|
// Keep moving to the next/previous element sibling until we find a
|
||||||
// panel-item that isn't hidden.
|
// panel-item that isn't hidden.
|
||||||
let moveForward =
|
let moveForward =
|
||||||
e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey);
|
e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey);
|
||||||
|
|
||||||
// If the menu is opened with the mouse, the active element might be
|
|
||||||
// somewhere else in the document. In that case we should ignore it
|
|
||||||
// to avoid walking unrelated DOM nodes.
|
|
||||||
this.focusWalker.currentNode = this.contains(
|
|
||||||
this.getRootNode().activeElement
|
|
||||||
)
|
|
||||||
? this.getRootNode().activeElement
|
|
||||||
: this;
|
|
||||||
let nextItem = moveForward
|
let nextItem = moveForward
|
||||||
? this.focusWalker.nextNode()
|
? this.focusWalker.nextNode()
|
||||||
: this.focusWalker.previousNode();
|
: this.focusWalker.previousNode();
|
||||||
|
|
@ -458,18 +470,70 @@
|
||||||
}
|
}
|
||||||
return this._focusWalker;
|
return this._focusWalker;
|
||||||
}
|
}
|
||||||
|
async setSubmenuAlign() {
|
||||||
|
const hostElement =
|
||||||
|
this.lastAnchorNode.parentElement || this.getRootNode().host;
|
||||||
|
// The showing attribute allows layout of the panel while remaining hidden
|
||||||
|
// from the user until alignment is set.
|
||||||
|
this.setAttribute("showing", "true");
|
||||||
|
|
||||||
|
// Wait for a layout flush, then find the bounds.
|
||||||
|
let { anchorWidth, anchorTop, panelTop } = await new Promise(resolve => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// It's possible this is being used in a context where windowUtils is
|
||||||
|
// not available. In that case, fallback to using the element.
|
||||||
|
let getBounds = el =>
|
||||||
|
window.windowUtils
|
||||||
|
? window.windowUtils.getBoundsWithoutFlushing(el)
|
||||||
|
: el.getBoundingClientRect();
|
||||||
|
let anchorBounds = getBounds(this.lastAnchorNode);
|
||||||
|
let panelBounds = getBounds(hostElement);
|
||||||
|
resolve({
|
||||||
|
anchorWidth: anchorBounds.width,
|
||||||
|
anchorTop: anchorBounds.top,
|
||||||
|
panelTop: panelBounds.top,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let align = hostElement.getAttribute("align");
|
||||||
|
|
||||||
|
if (align == "right") {
|
||||||
|
this.style.right = `${anchorWidth}px`;
|
||||||
|
this.style.left = "";
|
||||||
|
} else {
|
||||||
|
this.style.left = `${anchorWidth}px`;
|
||||||
|
this.style.right = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let topOffset =
|
||||||
|
anchorTop -
|
||||||
|
panelTop -
|
||||||
|
(parseFloat(window.getComputedStyle(this)?.paddingTop) || 0);
|
||||||
|
this.style.top = `${topOffset}px`;
|
||||||
|
|
||||||
|
this.removeAttribute("showing");
|
||||||
|
}
|
||||||
|
|
||||||
async onShow() {
|
async onShow() {
|
||||||
this.sendEvent("showing");
|
this.sendEvent("showing");
|
||||||
this.addHideListeners();
|
this.addHideListeners();
|
||||||
|
|
||||||
|
if (this.lastAnchorNode?.hasSubmenu) {
|
||||||
|
await this.setSubmenuAlign();
|
||||||
|
} else {
|
||||||
await this.setAlign();
|
await this.setAlign();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always reset this regardless of how the panel list is opened
|
||||||
|
// so the first child will be focusable.
|
||||||
|
this.focusWalker.currentNode = this;
|
||||||
|
|
||||||
// Wait until the next paint for the alignment to be set and panel to be
|
// Wait until the next paint for the alignment to be set and panel to be
|
||||||
// visible.
|
// visible.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (this.wasOpenedByKeyboard) {
|
if (this.wasOpenedByKeyboard) {
|
||||||
// Focus the first focusable panel-item if opened by keyboard.
|
// Focus the first focusable panel-item if opened by keyboard.
|
||||||
this.focusWalker.currentNode = this;
|
|
||||||
this.focusWalker.nextNode();
|
this.focusWalker.nextNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,11 +578,11 @@
|
||||||
this.button = document.createElement("button");
|
this.button = document.createElement("button");
|
||||||
this.button.setAttribute("role", "menuitem");
|
this.button.setAttribute("role", "menuitem");
|
||||||
this.button.setAttribute("part", "button");
|
this.button.setAttribute("part", "button");
|
||||||
|
|
||||||
// Use a XUL label element if possible to show the accesskey.
|
// Use a XUL label element if possible to show the accesskey.
|
||||||
this.label = document.createXULElement
|
this.label = document.createXULElement
|
||||||
? document.createXULElement("label")
|
? document.createXULElement("label")
|
||||||
: document.createElement("span");
|
: document.createElement("span");
|
||||||
|
|
||||||
this.button.appendChild(this.label);
|
this.button.appendChild(this.label);
|
||||||
|
|
||||||
let supportLinkSlot = document.createElement("slot");
|
let supportLinkSlot = document.createElement("slot");
|
||||||
|
|
@ -527,6 +591,24 @@
|
||||||
this.#defaultSlot = document.createElement("slot");
|
this.#defaultSlot = document.createElement("slot");
|
||||||
this.#defaultSlot.style.display = "none";
|
this.#defaultSlot.style.display = "none";
|
||||||
|
|
||||||
|
if (this.hasSubmenu) {
|
||||||
|
this.icon = document.createElement("div");
|
||||||
|
this.icon.setAttribute("class", "submenu-icon");
|
||||||
|
this.label.setAttribute("class", "submenu-label");
|
||||||
|
|
||||||
|
this.button.setAttribute("class", "submenu-container");
|
||||||
|
this.button.appendChild(this.icon);
|
||||||
|
|
||||||
|
this.submenuSlot = document.createElement("slot");
|
||||||
|
this.submenuSlot.name = "submenu";
|
||||||
|
|
||||||
|
this.shadowRoot.append(
|
||||||
|
style,
|
||||||
|
this.button,
|
||||||
|
this.#defaultSlot,
|
||||||
|
this.submenuSlot
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this.shadowRoot.append(
|
this.shadowRoot.append(
|
||||||
style,
|
style,
|
||||||
this.button,
|
this.button,
|
||||||
|
|
@ -534,8 +616,14 @@
|
||||||
this.#defaultSlot
|
this.#defaultSlot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
if (!this._l10nRootConnected && document.l10n) {
|
||||||
|
document.l10n.connectRoot(this.shadowRoot);
|
||||||
|
this._l10nRootConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.#initialized) {
|
if (!this.#initialized) {
|
||||||
this.#initialized = true;
|
this.#initialized = true;
|
||||||
// When click listeners are added to the panel-item it creates a node in
|
// When click listeners are added to the panel-item it creates a node in
|
||||||
|
|
@ -553,22 +641,48 @@
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.hasSubmenu) {
|
||||||
|
this.setSubmenuContents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.panel = this.closest("panel-list");
|
this.panel =
|
||||||
|
this.getRootNode()?.host?.closest("panel-list") ||
|
||||||
|
this.closest("panel-list");
|
||||||
|
|
||||||
if (this.panel) {
|
if (this.panel) {
|
||||||
this.panel.addEventListener("hidden", this);
|
this.panel.addEventListener("hidden", this);
|
||||||
this.panel.addEventListener("shown", this);
|
this.panel.addEventListener("shown", this);
|
||||||
}
|
}
|
||||||
|
if (this.hasSubmenu) {
|
||||||
|
this.addEventListener("mouseenter", this);
|
||||||
|
this.addEventListener("mouseleave", this);
|
||||||
|
this.addEventListener("keydown", this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
if (this._l10nRootConnected) {
|
||||||
|
document.l10n.disconnectRoot(this.shadowRoot);
|
||||||
|
this._l10nRootConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.panel) {
|
if (this.panel) {
|
||||||
this.panel.removeEventListener("hidden", this);
|
this.panel.removeEventListener("hidden", this);
|
||||||
this.panel.removeEventListener("shown", this);
|
this.panel.removeEventListener("shown", this);
|
||||||
this.panel = null;
|
this.panel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hasSubmenu) {
|
||||||
|
this.removeEventListener("mouseenter", this);
|
||||||
|
this.removeEventListener("mouseleave", this);
|
||||||
|
this.removeEventListener("keydown", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasSubmenu() {
|
||||||
|
return this.hasAttribute("submenu");
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name, oldVal, newVal) {
|
attributeChangedCallback(name, oldVal, newVal) {
|
||||||
|
|
@ -606,6 +720,11 @@
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSubmenuContents() {
|
||||||
|
this.submenuPanel = this.submenuSlot.assignedNodes()[0];
|
||||||
|
this.shadowRoot.append(this.submenuPanel);
|
||||||
|
}
|
||||||
|
|
||||||
get disabled() {
|
get disabled() {
|
||||||
return this.button.hasAttribute("disabled");
|
return this.button.hasAttribute("disabled");
|
||||||
}
|
}
|
||||||
|
|
@ -626,6 +745,17 @@
|
||||||
this.button.focus();
|
this.button.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setArrowKeyRTL() {
|
||||||
|
let arrowOpenKey = "ArrowRight";
|
||||||
|
let arrowCloseKey = "ArrowLeft";
|
||||||
|
|
||||||
|
if (this.submenuPanel.isDocumentRTL()) {
|
||||||
|
arrowOpenKey = "ArrowLeft";
|
||||||
|
arrowCloseKey = "ArrowRight";
|
||||||
|
}
|
||||||
|
return [arrowOpenKey, arrowCloseKey];
|
||||||
|
}
|
||||||
|
|
||||||
handleEvent(e) {
|
handleEvent(e) {
|
||||||
// Bug 1588156 - Accesskey is not ignored for hidden non-input elements.
|
// Bug 1588156 - Accesskey is not ignored for hidden non-input elements.
|
||||||
// Since the accesskey won't be ignored, we need to remove it ourselves
|
// Since the accesskey won't be ignored, we need to remove it ourselves
|
||||||
|
|
@ -644,6 +774,21 @@
|
||||||
this.accessKey = "";
|
this.accessKey = "";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "mouseenter":
|
||||||
|
case "mouseleave":
|
||||||
|
this.submenuPanel.toggle(e);
|
||||||
|
break;
|
||||||
|
case "keydown":
|
||||||
|
let [arrowOpenKey, arrowCloseKey] = this.setArrowKeyRTL();
|
||||||
|
if (e.key === arrowOpenKey) {
|
||||||
|
this.submenuPanel.show(e, e.target);
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
if (e.key === arrowCloseKey) {
|
||||||
|
this.submenuPanel.hide(e, { force: true }, e.target);
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue