Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!doctype html>
<!-- Any copyright is dedicated to the Public Domain.
<html>
<head>
<meta charset="utf-8" />
<title>Test Panel List Popover</title>
<link
rel="stylesheet"
/>
</head>
<body>
<p id="display"></p>
<div id="content">
<div id="outside-scroller" style="overflow: auto; height: 100px">
<div style="height: 300px">Tall content to enable scrolling</div>
</div>
<button id="anchor-button">Open</button>
<panel-list id="panel-list">
<panel-item>one</panel-item>
<panel-item submenu="sub-panel-list" id="submenu-host">
two
<panel-list slot="submenu" id="sub-panel-list">
<panel-item>sub one</panel-item>
<panel-item>sub two</panel-item>
</panel-list>
</panel-item>
<panel-item>three</panel-item>
</panel-list>
</div>
<script class="testbody" type="application/javascript">
const { BrowserTestUtils } = ChromeUtils.importESModule(
);
const { TestUtils } = ChromeUtils.importESModule(
);
let anchorButton, panelList, outsideScroller;
add_setup(async function setup() {
await customElements.whenDefined("panel-list");
await customElements.whenDefined("panel-item");
panelList = document.getElementById("panel-list");
anchorButton = document.getElementById("anchor-button");
outsideScroller = document.getElementById("outside-scroller");
anchorButton.addEventListener("click", e => panelList.toggle(e));
});
function waitForSubmenuShown(subPanelList) {
return new Promise(resolve => {
subPanelList.addEventListener("shown", resolve, { once: true });
});
}
async function openParentPanel() {
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
anchorButton.click();
await shown;
}
async function closePanels() {
let subPanelList = document.getElementById("submenu-host").submenuPanel;
if (subPanelList?.open) {
let hidden = BrowserTestUtils.waitForEvent(subPanelList, "hidden");
subPanelList.hide(undefined, { force: true });
await hidden;
}
if (panelList.open) {
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide(undefined, { force: true });
await hidden;
}
}
// Install a new submenu on the host, replacing any existing submenu.
// A reused submenu would carry a cached width that hides the measurement bug.
function installSubmenu(host) {
if (host.submenuPanel) {
host.submenuPanel.remove();
}
let submenu = document.createElement("panel-list");
submenu.setAttribute("slot", "submenu");
let one = document.createElement("panel-item");
one.textContent = "sub one";
let two = document.createElement("panel-item");
two.textContent = "sub two";
submenu.append(one, two);
host.append(submenu);
host.setSubmenuContents();
return host.submenuPanel;
}
add_task(async function testPopoverAttributeSetOnConnect() {
is(
panelList.getAttribute("popover"),
"auto",
"Panel list should have popover='auto' attribute set"
);
});
add_task(async function testSupportsPopover() {
ok(
panelList.supportsPopover(),
"Panel list should support popover (not in XUL panel, not a submenu)"
);
});
add_task(async function testSubmenuDoesNotSupportPopover() {
let subPanelList = document.getElementById("submenu-host").submenuPanel;
ok(subPanelList, "Submenu panel list should be wired up from markup");
ok(
!subPanelList.supportsPopover(),
"Submenu panel list should not support popover"
);
ok(
!subPanelList.hasAttribute("popover"),
"Submenu panel list should not have popover attribute"
);
});
add_task(async function testShowPopoverCalledOnShow() {
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
ok(
panelList.matches(":popover-open"),
"Panel should match :popover-open after show()"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide();
await hidden;
});
add_task(async function testHidePopoverCalledOnHide() {
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
ok(
panelList.matches(":popover-open"),
"Panel should be open before hiding"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide();
await hidden;
ok(
!panelList.matches(":popover-open"),
"Panel should no longer match :popover-open after hide()"
);
});
add_task(async function testNoRecursionOnShow() {
let shownCount = 0;
let shownListener = () => {
shownCount++;
};
panelList.addEventListener("shown", shownListener);
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
is(
shownCount,
1,
"shown event should fire exactly once (no recursion)"
);
panelList.removeEventListener("shown", shownListener);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide();
await hidden;
});
add_task(async function testPopoverPanelHidesOnOutsideScroll() {
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
ok(
panelList.matches(":popover-open"),
"Panel should be open before scroll"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
outsideScroller.scrollTop = 50;
await hidden;
ok(
!panelList.matches(":popover-open"),
"Popover panel should be hidden after scroll outside the panel"
);
outsideScroller.scrollTop = 0;
});
add_task(async function testPopoverPanelStaysOpenOnInsideScroll() {
// has-submenu forces overflow: visible which prevents scrolling.
// Remove it temporarily so we can assert scrolling in the panel list
// doesn't close it.
let hadSubmenu = panelList.hasAttribute("has-submenu");
panelList.removeAttribute("has-submenu");
let extraItems = [];
for (let i = 0; i < 30; i++) {
let item = document.createElement("panel-item");
item.textContent = `extra ${i}`;
panelList.appendChild(item);
extraItems.push(item);
}
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
ok(
panelList.matches(":popover-open"),
"Panel should be open before inside scroll"
);
panelList.scrollTop = 50;
await TestUtils.waitForTick();
ok(
panelList.matches(":popover-open"),
"Popover panel should remain open after scroll inside the panel"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide();
await hidden;
for (let item of extraItems) {
item.remove();
}
if (hadSubmenu) {
panelList.setAttribute("has-submenu", "");
}
});
add_task(async function testPopoverPanelClosesOnResize() {
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
ok(
panelList.matches(":popover-open"),
"Panel should be open before resize"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
window.dispatchEvent(new Event("resize"));
await hidden;
ok(
!panelList.matches(":popover-open"),
"Popover panel should close after resize event"
);
});
add_task(async function testToggleViaAnchorButtonClick() {
ok(
!panelList.matches(":popover-open"),
"Panel should be closed initially"
);
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
synthesizeMouseAtCenter(anchorButton, {});
await shown;
ok(
panelList.matches(":popover-open"),
"Panel should open after clicking anchor button"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
synthesizeMouseAtCenter(anchorButton, {});
await hidden;
ok(
!panelList.matches(":popover-open"),
"Panel should close after clicking anchor button again"
);
});
add_task(async function testOutsideClickLightDismiss() {
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
ok(panelList.matches(":popover-open"), "Panel should be open");
ok(panelList.open, "Panel open attribute should be true");
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
// Click outside the panel to trigger light dismiss
synthesizeMouseAtCenter(document.body, {});
await hidden;
ok(
!panelList.matches(":popover-open"),
"Panel should be closed after outside click"
);
ok(
!panelList.open,
"Panel open attribute should be false after light dismiss"
);
});
add_task(async function testSecondPanelAutoClosesFirst() {
let secondAnchor = document.createElement("button");
secondAnchor.id = "second-anchor";
secondAnchor.textContent = "Second";
document.getElementById("content").appendChild(secondAnchor);
let secondPanelList = document.createElement("panel-list");
let secondItem = document.createElement("panel-item");
secondItem.textContent = "second one";
secondPanelList.appendChild(secondItem);
document.getElementById("content").appendChild(secondPanelList);
secondAnchor.addEventListener("click", e => secondPanelList.toggle(e));
let firstShown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await firstShown;
ok(panelList.matches(":popover-open"), "First panel should be open");
let firstHidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
let secondShown = BrowserTestUtils.waitForEvent(
secondPanelList,
"shown"
);
synthesizeMouseAtCenter(secondAnchor, {});
await secondShown;
ok(
secondPanelList.matches(":popover-open"),
"Second panel should be open"
);
// The browser's auto mutual exclusion closes the first panel; wait for that
await firstHidden;
ok(
!panelList.matches(":popover-open"),
"First panel should be auto-closed when second panel opens"
);
ok(!panelList.open, "First panel open attribute should be false");
let secondHidden = BrowserTestUtils.waitForEvent(
secondPanelList,
"hidden"
);
secondPanelList.hide();
await secondHidden;
secondAnchor.remove();
secondPanelList.remove();
});
add_task(async function testDisableAutohideFallsBackToManual() {
await SpecialPowers.pushPrefEnv({
set: [["ui.popup.disable_autohide", true]],
});
let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await shown;
is(
panelList.getAttribute("popover"),
"manual",
"Panel should use popover='manual' when disable_autohide is true"
);
ok(panelList.matches(":popover-open"), "Panel should be open");
// Outside click should NOT close a manual popover
synthesizeMouseAtCenter(document.body, {});
await TestUtils.waitForTick();
ok(
panelList.matches(":popover-open"),
"Panel should remain open after outside click with disable_autohide"
);
let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide(undefined, { force: true });
await hidden;
await SpecialPowers.popPrefEnv();
// After clearing the pref, the next show() call should use "auto" again.
let reshown = BrowserTestUtils.waitForEvent(panelList, "shown");
panelList.show();
await reshown;
is(
panelList.getAttribute("popover"),
"auto",
"Panel should use popover='auto' again after pref is cleared"
);
let rehidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelList.hide(undefined, { force: true });
await rehidden;
});
// Bug 2050478: a submenu that would overflow the right
// edge of the viewport must open to the left instead.
add_task(async function testSubmenuOpensOnCorrectSideViaKeyboard() {
let submenuHost = document.getElementById("submenu-host");
let subPanelList = installSubmenu(submenuHost);
try {
// Submenu width decides the open side only when the parent panel is
// align="left", so anchor at the horizontal center to keep it there.
let cw = document.scrollingElement.clientWidth;
anchorButton.style.position = "fixed";
anchorButton.style.top = "32px";
anchorButton.style.left = `${Math.round(cw * 0.5)}px`;
anchorButton.style.right = "unset";
anchorButton.style.bottom = "unset";
await openParentPanel();
// Size the submenu from the real layout so it overflows if opened right but
// fits if opened left. A fixed width could exceed the harness viewport and
// fit nowhere. Setting it before show() does not lay it out, so without the
// fix it still measures 0.
let hostLeft = submenuHost.getBoundingClientRect().left;
subPanelList.style.width = `${Math.round(hostLeft - 20)}px`;
let keyEvent = new KeyboardEvent("keydown", { key: "ArrowRight" });
let submenuShown = waitForSubmenuShown(subPanelList);
subPanelList.show(keyEvent, submenuHost);
await submenuShown;
let submenuBounds = subPanelList.getBoundingClientRect();
let clientWidth = document.scrollingElement.clientWidth;
ok(
submenuBounds.width > 0,
"Submenu should have a non-zero width after opening"
);
ok(
Math.round(submenuBounds.right) <= clientWidth,
`Submenu right edge (${Math.round(submenuBounds.right)}) should ` +
`not exceed viewport width (${clientWidth})`
);
ok(
Math.round(submenuBounds.left) >= 0,
`Submenu left edge (${Math.round(submenuBounds.left)}) should ` +
`not be negative`
);
} finally {
await closePanels();
anchorButton.style.cssText = "";
}
});
// Bug 2050478 in RTL. setSubmenuAlign is not RTL-aware,
// so this can only check the submenu opens on-screen, not reproduce the
// LTR overflow.
add_task(async function testSubmenuOpensOnCorrectSideViaKeyboardRTL() {
await SpecialPowers.pushPrefEnv({
set: [["intl.l10n.pseudo", "bidi"]],
});
ok(panelList.isDocumentRTL(), "Document should be RTL for this test");
let submenuHost = document.getElementById("submenu-host");
let subPanelList = installSubmenu(submenuHost);
try {
anchorButton.style.position = "fixed";
anchorButton.style.top = "32px";
anchorButton.style.left = "32px";
anchorButton.style.right = "unset";
anchorButton.style.bottom = "unset";
await openParentPanel();
let keyEvent = new KeyboardEvent("keydown", { key: "ArrowLeft" });
let submenuShown = waitForSubmenuShown(subPanelList);
subPanelList.show(keyEvent, submenuHost);
await submenuShown;
let submenuBounds = subPanelList.getBoundingClientRect();
let clientWidth = document.scrollingElement.clientWidth;
ok(
submenuBounds.width > 0,
"Submenu should have a non-zero width after opening in RTL"
);
ok(
Math.round(submenuBounds.right) <= clientWidth,
`Submenu right edge (${Math.round(submenuBounds.right)}) should ` +
`not exceed viewport width (${clientWidth}) in RTL`
);
ok(
Math.round(submenuBounds.left) >= 0,
`Submenu left edge (${Math.round(submenuBounds.left)}) should ` +
`not be negative in RTL`
);
} finally {
await closePanels();
anchorButton.style.cssText = "";
// pushPrefEnv only restores at file end, so pop the bidi pref now to keep
// it out of later tasks.
await SpecialPowers.popPrefEnv();
}
});
// Bug 2050478: opening the submenu by keyboard must not
// scroll the page sideways and trip the parent's scroll-hide listener.
add_task(async function testSubmenuKeyboardOpenDoesNotScroll() {
let submenuHost = document.getElementById("submenu-host");
let subPanelList = installSubmenu(submenuHost);
// Give the page room to scroll horizontally, or an arrow key has nothing
// to scroll and the test passes trivially.
let wideSpacer = document.createElement("div");
wideSpacer.style.width = "5000px";
wideSpacer.style.height = "1px";
document.getElementById("content").appendChild(wideSpacer);
document.scrollingElement.scrollLeft = 0;
try {
// Open by click, not keyboard. A keyboard open focuses the first item on a
// later rAF, so focus leaves submenuHost before the ArrowRight below.
await openParentPanel();
submenuHost.focus();
let scrollBefore = document.scrollingElement.scrollLeft;
// Race "shown" against a deadline so the task can't hang if a scroll
// dismisses the submenu before "shown" fires.
let submenuShown = waitForSubmenuShown(subPanelList);
synthesizeKey("KEY_ArrowRight", {});
await Promise.race([
submenuShown,
new Promise(resolve => setTimeout(resolve, 2000)),
]);
is(
document.scrollingElement.scrollLeft,
scrollBefore,
"Page should not have scrolled horizontally"
);
ok(panelList.open, "Parent panel should stay open");
ok(subPanelList.open, "Submenu should be open after ArrowRight");
} finally {
await closePanels();
wideSpacer.remove();
document.scrollingElement.scrollLeft = 0;
anchorButton.style.cssText = "";
}
});
</script>
</body>
</html>