Global Context Menu
A context menu for a whole surface, with no trigger in the flyout at all. A contextmenu listener on the area assigns openAtPosition, and the menu opens at that fixed viewport coordinate. Scrolling is locked while the menu is open, because a fixed coordinate does not follow the page. Closing clears openAtPosition again: with no position and no trigger, the menu cannot open, so a right-click outside the area does nothing.
Code
The following example assumes that you have imported the components and set up the theme.
<div class="global-context-menu-example">16 collapsed lines
<div class="context-area" tabindex="-1"> <strong>Context menu area</strong> <span>Right-click or touch and hold anywhere in this rectangle.</span> </div>
<sv-menu class="global-context-menu" mode="context"> <sv-flyout aria-label="Page actions menu"> <button type="button" class="context-option" role="menuitem">Refresh</button> <button type="button" class="context-option" role="menuitem">Paste</button> <button type="button" class="context-option" role="menuitem">Select all</button> <div class="divider" role="separator"></div> <button type="button" class="context-option" role="menuitem">Open developer tools</button> </sv-flyout> </sv-menu>
<p class="context-result" aria-live="polite">Choose an action from the context menu.</p></div>
<script type="application/x-typescript" module>40 collapsed lines
const example = document.querySelector(".global-context-menu-example")!;
const contextArea = example.querySelector<HTMLElement>(".context-area")!; const menu = example.querySelector<Menu>(".global-context-menu")!; const flyout = menu.querySelector<Flyout>("sv-flyout")!; const result = example.querySelector<HTMLElement>(".context-result")!; const options = menu.querySelectorAll<HTMLButtonElement>(".context-option");
contextArea.addEventListener("contextmenu", event => { menu.openAtPosition = { left: event.clientX, top: event.clientY }; });
const lockScroll = () => { document.documentElement.style.setProperty("overflow", "hidden"); };
const unlockScroll = () => { document.documentElement.style.removeProperty("overflow"); menu.openAtPosition = undefined; };
flyout.addEventListener("openedchange", () => { if (flyout.opened) { lockScroll(); } });
flyout.addEventListener("transitionfinish", () => { if (!flyout.opened) { unlockScroll(); } }); window.addEventListener("pagehide", unlockScroll);
for (const option of options) { option.addEventListener("click", () => { result.textContent = `${option.textContent} selected.`; menu.close(); }); }</script>
<style>67 collapsed lines
.global-context-menu-example { display: grid; gap: 0.75em; max-inline-size: 40rem; margin: auto;
sv-flyout { --sv-flyout-offset: 0;
&::part(content) { min-block-size: 100px; } }
.context-area { display: grid; gap: 0.5em; place-content: center; min-block-size: 18rem; padding: 1.5em; color: var(--sv-gray-2); text-align: center; touch-action: manipulation; cursor: context-menu; user-select: none; outline: none; background: linear-gradient(var(--sv-gray-7), var(--sv-gray-7)) padding-box, repeating-linear-gradient(135deg, var(--sv-gray-5) 0 1px, transparent 1px 10px) border-box; border: var(--sv-bdr-sz-sm) solid transparent; border-radius: var(--sv-bdr-rad-lg); -webkit-touch-callout: none;
span { color: var(--sv-gray-3); } }
.context-option { padding: 0.4em 0.75em; font: inherit; color: var(--sv-gray-2); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--sv-bdr-rad-sm);
@media (hover: hover) and (pointer: fine) { &:hover { background: var(--sv-gray-5); } } }
.divider { block-size: 1px; background: var(--sv-gray-5); }
.context-result { min-block-size: 1.5em; margin: 0; color: var(--sv-gray-3); text-align: center; } }</style>