зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
ec323f2b5e
Коммит
ec86095ac6
|
@ -163,6 +163,10 @@ panel-item::part(button):hover:active {
|
|||
background-color: var(--fxview-element-background-active);
|
||||
}
|
||||
|
||||
panel-list {
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
fxview-category-navigation {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
|
@ -15,8 +15,19 @@ const lazy = {};
|
|||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
EveryWindow: "resource:///modules/EveryWindow.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.
|
||||
*
|
||||
|
@ -37,6 +48,8 @@ class OpenTabsInView extends ViewPage {
|
|||
this.isPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(
|
||||
this.currentWindow
|
||||
);
|
||||
this.boundObserve = (...args) => this.observe(...args);
|
||||
this.devices = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
@ -69,10 +82,46 @@ class OpenTabsInView extends ViewPage {
|
|||
}
|
||||
);
|
||||
this._updateOpenTabsList();
|
||||
this.addObserversIfNeeded();
|
||||
|
||||
this.devices = this.currentWindow.gSync.getSendTabTargets();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
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() {
|
||||
|
@ -93,6 +142,7 @@ class OpenTabsInView extends ViewPage {
|
|||
otherWindows.push([index++, tabs, win]);
|
||||
}
|
||||
});
|
||||
|
||||
const cardClasses = classMap({
|
||||
"height-limited": this.windows.size > 3,
|
||||
"width-limited": this.windows.size > 1,
|
||||
|
@ -130,6 +180,7 @@ class OpenTabsInView extends ViewPage {
|
|||
data-l10n-args="${JSON.stringify({
|
||||
winID: currentWindowIndex,
|
||||
})}"
|
||||
.devices=${this.devices}
|
||||
></view-opentabs-card>
|
||||
`
|
||||
)}
|
||||
|
@ -142,6 +193,7 @@ class OpenTabsInView extends ViewPage {
|
|||
data-inner-id="${win.windowGlobalChild.innerWindowId}"
|
||||
data-l10n-id="firefoxview-opentabs-window-header"
|
||||
data-l10n-args="${JSON.stringify({ winID })}"
|
||||
.devices=${this.devices}
|
||||
></view-opentabs-card>
|
||||
`
|
||||
)}
|
||||
|
@ -150,8 +202,8 @@ class OpenTabsInView extends ViewPage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Render a template for the 'Recent browsing' page, which shows a single list of
|
||||
* recently accessed tabs, rather than a list of tabs per window.
|
||||
* Render a template for the 'Recent browsing' page, which shows a shorter list of
|
||||
* open tabs in the current window.
|
||||
*
|
||||
* @returns {TemplateResult}
|
||||
* The recent browsing template.
|
||||
|
@ -163,6 +215,7 @@ class OpenTabsInView extends ViewPage {
|
|||
return html`<view-opentabs-card
|
||||
.tabs=${tabs}
|
||||
.recentBrowsing=${true}
|
||||
.devices=${this.devices}
|
||||
></view-opentabs-card>`;
|
||||
}
|
||||
|
||||
|
@ -245,6 +298,8 @@ class OpenTabsInViewCard extends ViewPage {
|
|||
tabs: { type: Array },
|
||||
title: { type: String },
|
||||
recentBrowsing: { type: Boolean },
|
||||
devices: { type: Array },
|
||||
triggerNode: { type: Object },
|
||||
};
|
||||
static MAX_TABS_FOR_COMPACT_HEIGHT = 7;
|
||||
|
||||
|
@ -254,6 +309,7 @@ class OpenTabsInViewCard extends ViewPage {
|
|||
this.tabs = [];
|
||||
this.title = "";
|
||||
this.recentBrowsing = false;
|
||||
this.devices = [];
|
||||
}
|
||||
|
||||
static queries = {
|
||||
|
@ -264,11 +320,81 @@ class OpenTabsInViewCard extends ViewPage {
|
|||
|
||||
closeTab(e) {
|
||||
const tab = this.triggerNode.tabElement;
|
||||
const browserWindow = tab.ownerGlobal;
|
||||
browserWindow.gBrowser.removeTab(tab, { animate: true });
|
||||
tab?.ownerGlobal.gBrowser.removeTab(tab);
|
||||
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() {
|
||||
return html`
|
||||
<panel-list slot="menu" data-tab-type="opentabs">
|
||||
|
@ -277,11 +403,27 @@ class OpenTabsInViewCard extends ViewPage {
|
|||
data-l10n-attrs="accesskey"
|
||||
@click=${this.closeTab}
|
||||
></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
|
||||
data-l10n-id="fxviewtabrow-copy-link"
|
||||
data-l10n-attrs="accesskey"
|
||||
@click=${this.copyLink}
|
||||
></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>
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,12 @@ async function setupWithDesktopDevices(state = UIState.STATUS_SIGNED_IN) {
|
|||
return sandbox;
|
||||
}
|
||||
|
||||
function promiseSyncReady() {
|
||||
let service = Cc["@mozilla.org/weave/service;1"].getService(
|
||||
Ci.nsISupports
|
||||
).wrappedJSObject;
|
||||
return service.whenLoaded();
|
||||
}
|
||||
async function tearDown(sandbox) {
|
||||
sandbox?.restore();
|
||||
Services.prefs.clearUserPref("services.sync.lastTabFetch");
|
||||
|
|
|
@ -4,6 +4,7 @@ support-files = ../head.js
|
|||
[browser_firefoxview_next.js]
|
||||
[browser_firefoxview_next_general_telemetry.js]
|
||||
[browser_history_firefoxview_next.js]
|
||||
[browser_opentabs_firefoxview_next.js]
|
||||
[browser_recentlyclosed_firefoxview_next.js]
|
||||
[browser_syncedtabs_errors_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 BrowserTestUtils.waitForEvent(panelList, "shown");
|
||||
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 = [
|
||||
[
|
||||
"firefoxview_next",
|
||||
|
|
|
@ -27,26 +27,6 @@ oneMonthAgo.setMonth(
|
|||
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) {
|
||||
await PlacesUtils.history.insert({
|
||||
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]);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -17,26 +17,6 @@ const DISMISS_CLOSED_TAB_EVENT = [
|
|||
];
|
||||
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) {
|
||||
let recentlyClosedComponent = doc.querySelector(
|
||||
"view-recentlyclosed:not([slot=recentlyclosed])"
|
||||
|
@ -313,7 +293,7 @@ add_task(async function test_list_updates() {
|
|||
);
|
||||
SessionStore.undoCloseById(closedTabItem.closedId);
|
||||
await promiseClosedObjectsChanged;
|
||||
await openFirefoxView(window);
|
||||
await clickFirefoxViewButton(window);
|
||||
|
||||
// we expect the last item to be removed
|
||||
expectedURLs.pop();
|
||||
|
@ -339,7 +319,7 @@ add_task(async function test_list_updates() {
|
|||
);
|
||||
SessionStore.forgetClosedWindowById(closedTabItem.sourceClosedId);
|
||||
await promiseClosedObjectsChanged;
|
||||
await openFirefoxView(window);
|
||||
await clickFirefoxViewButton(window);
|
||||
|
||||
listItems = listElem.rowEls;
|
||||
expectedURLs.shift(); // we expect to have removed the firsts URL from the list
|
||||
|
@ -378,7 +358,7 @@ add_task(async function test_restore_tab() {
|
|||
await clearAllParentTelemetryEvents();
|
||||
await restore_tab(closeTabItem, browser, closeTabItem.url);
|
||||
await recentlyClosedTelemetry();
|
||||
await openFirefoxView(window);
|
||||
await clickFirefoxViewButton(window);
|
||||
|
||||
listItems = listElem.rowEls;
|
||||
is(listItems.length, 3, "Three tabs are shown in the list.");
|
||||
|
@ -387,7 +367,7 @@ add_task(async function test_restore_tab() {
|
|||
await clearAllParentTelemetryEvents();
|
||||
await restore_tab(closeTabItem, browser, closeTabItem.url);
|
||||
await recentlyClosedTelemetry();
|
||||
await openFirefoxView(window);
|
||||
await clickFirefoxViewButton(window);
|
||||
|
||||
listItems = listElem.rowEls;
|
||||
is(listItems.length, 2, "Two tabs are shown in the list.");
|
||||
|
|
|
@ -605,3 +605,25 @@ function navigateToCategory(document, category) {
|
|||
async function switchToFxViewTab(win = window) {
|
||||
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
|
||||
fxviewtabrow-close-tab = Close Tab
|
||||
.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:
|
||||
# $tabTitle (string) - Title of the tab to which the context menu is associated
|
||||
|
|
|
@ -70,3 +70,27 @@ button:focus-visible {
|
|||
button:disabled {
|
||||
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;
|
||||
cursor: default;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host(:not([slot=submenu])) {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:host([stay-open]) {
|
||||
position: initial;
|
||||
display: inline-block;
|
||||
|
|
|
@ -63,6 +63,14 @@
|
|||
this.toggleAttribute("open", val);
|
||||
}
|
||||
|
||||
get stayOpen() {
|
||||
return this.hasAttribute("stay-open");
|
||||
}
|
||||
|
||||
set stayOpen(val) {
|
||||
this.toggleAttribute("stay-open", val);
|
||||
}
|
||||
|
||||
getTargetForEvent(event) {
|
||||
if (!event) {
|
||||
return null;
|
||||
|
@ -77,13 +85,17 @@
|
|||
return event._savedComposedTarget || event.target;
|
||||
}
|
||||
|
||||
show(triggeringEvent) {
|
||||
show(triggeringEvent, target) {
|
||||
this.triggeringEvent = triggeringEvent;
|
||||
this.lastAnchorNode = this.getTargetForEvent(this.triggeringEvent);
|
||||
this.lastAnchorNode =
|
||||
target || this.getTargetForEvent(this.triggeringEvent);
|
||||
|
||||
this.wasOpenedByKeyboard =
|
||||
triggeringEvent &&
|
||||
(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;
|
||||
|
||||
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
|
||||
// case it won't have access to Services / Services will be undeclared.
|
||||
const autohideDisabled = this.hasServices()
|
||||
|
@ -136,18 +148,18 @@
|
|||
panel.hidePopup();
|
||||
}
|
||||
|
||||
let target = this.getTargetForEvent(openingEvent);
|
||||
let target = eventTarget || this.getTargetForEvent(openingEvent);
|
||||
// Refocus the button that opened the menu if we have one.
|
||||
if (target && this.wasOpenedByKeyboard) {
|
||||
target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
toggle(triggeringEvent) {
|
||||
toggle(triggeringEvent, target = null) {
|
||||
if (this.open) {
|
||||
this.hide(triggeringEvent, { force: true });
|
||||
this.hide(triggeringEvent, { force: true }, target);
|
||||
} else {
|
||||
this.show(triggeringEvent);
|
||||
this.show(triggeringEvent, target);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,12 +288,15 @@
|
|||
}
|
||||
|
||||
addHideListeners() {
|
||||
if (this.hasAttribute("stay-open")) {
|
||||
if (this.hasAttribute("stay-open") && !this.lastAnchorNode.hasSubmenu) {
|
||||
// This is intended for inspection in Storybook.
|
||||
return;
|
||||
}
|
||||
// Hide when a panel-item is clicked in the list.
|
||||
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);
|
||||
// Hide when a click is initiated outside the panel.
|
||||
document.addEventListener("mousedown", this);
|
||||
|
@ -300,6 +315,7 @@
|
|||
|
||||
removeHideListeners() {
|
||||
this.removeEventListener("click", this);
|
||||
this.removeEventListener("keydown", this);
|
||||
document.removeEventListener("keydown", this);
|
||||
document.removeEventListener("mousedown", this);
|
||||
document.removeEventListener("focusin", this);
|
||||
|
@ -357,19 +373,15 @@
|
|||
// Don't scroll the page or let the regular tab order take effect.
|
||||
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
|
||||
// panel-item that isn't hidden.
|
||||
let moveForward =
|
||||
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
|
||||
? this.focusWalker.nextNode()
|
||||
: this.focusWalker.previousNode();
|
||||
|
@ -458,18 +470,70 @@
|
|||
}
|
||||
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() {
|
||||
this.sendEvent("showing");
|
||||
this.addHideListeners();
|
||||
await this.setAlign();
|
||||
|
||||
if (this.lastAnchorNode?.hasSubmenu) {
|
||||
await this.setSubmenuAlign();
|
||||
} else {
|
||||
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
|
||||
// visible.
|
||||
requestAnimationFrame(() => {
|
||||
if (this.wasOpenedByKeyboard) {
|
||||
// Focus the first focusable panel-item if opened by keyboard.
|
||||
this.focusWalker.currentNode = this;
|
||||
this.focusWalker.nextNode();
|
||||
}
|
||||
|
||||
|
@ -514,11 +578,11 @@
|
|||
this.button = document.createElement("button");
|
||||
this.button.setAttribute("role", "menuitem");
|
||||
this.button.setAttribute("part", "button");
|
||||
|
||||
// Use a XUL label element if possible to show the accesskey.
|
||||
this.label = document.createXULElement
|
||||
? document.createXULElement("label")
|
||||
: document.createElement("span");
|
||||
|
||||
this.button.appendChild(this.label);
|
||||
|
||||
let supportLinkSlot = document.createElement("slot");
|
||||
|
@ -527,15 +591,39 @@
|
|||
this.#defaultSlot = document.createElement("slot");
|
||||
this.#defaultSlot.style.display = "none";
|
||||
|
||||
this.shadowRoot.append(
|
||||
style,
|
||||
this.button,
|
||||
supportLinkSlot,
|
||||
this.#defaultSlot
|
||||
);
|
||||
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(
|
||||
style,
|
||||
this.button,
|
||||
supportLinkSlot,
|
||||
this.#defaultSlot
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this._l10nRootConnected && document.l10n) {
|
||||
document.l10n.connectRoot(this.shadowRoot);
|
||||
this._l10nRootConnected = true;
|
||||
}
|
||||
|
||||
if (!this.#initialized) {
|
||||
this.#initialized = true;
|
||||
// When click listeners are added to the panel-item it creates a node in
|
||||
|
@ -553,22 +641,48 @@
|
|||
childList: 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) {
|
||||
this.panel.addEventListener("hidden", this);
|
||||
this.panel.addEventListener("shown", this);
|
||||
}
|
||||
if (this.hasSubmenu) {
|
||||
this.addEventListener("mouseenter", this);
|
||||
this.addEventListener("mouseleave", this);
|
||||
this.addEventListener("keydown", this);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this._l10nRootConnected) {
|
||||
document.l10n.disconnectRoot(this.shadowRoot);
|
||||
this._l10nRootConnected = false;
|
||||
}
|
||||
|
||||
if (this.panel) {
|
||||
this.panel.removeEventListener("hidden", this);
|
||||
this.panel.removeEventListener("shown", this);
|
||||
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) {
|
||||
|
@ -606,6 +720,11 @@
|
|||
.join("");
|
||||
}
|
||||
|
||||
setSubmenuContents() {
|
||||
this.submenuPanel = this.submenuSlot.assignedNodes()[0];
|
||||
this.shadowRoot.append(this.submenuPanel);
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return this.button.hasAttribute("disabled");
|
||||
}
|
||||
|
@ -626,6 +745,17 @@
|
|||
this.button.focus();
|
||||
}
|
||||
|
||||
setArrowKeyRTL() {
|
||||
let arrowOpenKey = "ArrowRight";
|
||||
let arrowCloseKey = "ArrowLeft";
|
||||
|
||||
if (this.submenuPanel.isDocumentRTL()) {
|
||||
arrowOpenKey = "ArrowLeft";
|
||||
arrowCloseKey = "ArrowRight";
|
||||
}
|
||||
return [arrowOpenKey, arrowCloseKey];
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
// Bug 1588156 - Accesskey is not ignored for hidden non-input elements.
|
||||
// Since the accesskey won't be ignored, we need to remove it ourselves
|
||||
|
@ -644,6 +774,21 @@
|
|||
this.accessKey = "";
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче