immich/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte
2025-04-28 13:20:07 +02:00

112 lines
3.2 KiB
Svelte

<script lang="ts">
import { tick, type Snippet } from 'svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import { shortcuts } from '$lib/actions/shortcut';
import { generateId } from '$lib/utils/generate-id';
import { contextMenuNavigation } from '$lib/actions/context-menu-navigation';
import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
interface Props {
title: string;
direction?: 'left' | 'right';
x?: number;
y?: number;
isOpen?: boolean;
onClose: (() => unknown) | undefined;
children?: Snippet;
}
let { title, direction = 'right', x = 0, y = 0, isOpen = false, onClose, children }: Props = $props();
let uniqueKey = $state({});
let menuContainer: HTMLUListElement | undefined = $state();
let triggerElement: HTMLElement | undefined = $state(undefined);
const id = generateId();
const menuId = `context-menu-${id}`;
const reopenContextMenu = async (event: MouseEvent) => {
const contextMenuEvent = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
// eslint-disable-next-line unicorn/prefer-global-this
view: window,
clientX: event.x,
clientY: event.y,
});
const elements = document.elementsFromPoint(event.x, event.y);
if (menuContainer && elements.includes(menuContainer)) {
// User right-clicked on the context menu itself, we keep the context
// menu as is
return;
}
closeContextMenu();
await tick();
uniqueKey = {};
// Event will bubble through the DOM tree
const sectionIndex = elements.indexOf(event.target as Element);
elements.at(sectionIndex + 1)?.dispatchEvent(contextMenuEvent);
};
const closeContextMenu = () => {
triggerElement?.focus();
onClose?.();
};
$effect(() => {
if (isOpen && menuContainer) {
triggerElement = document.activeElement as HTMLElement;
menuContainer.focus();
contextMenuManager.optionClickCallback = closeContextMenu;
}
});
const oncontextmenu = async (event: MouseEvent) => {
event.preventDefault();
await reopenContextMenu(event);
};
</script>
{#key uniqueKey}
{#if isOpen}
<div
use:contextMenuNavigation={{
closeDropdown: closeContextMenu,
container: menuContainer,
isOpen,
selectedId: contextMenuManager.selectedId,
selectionChanged: (id) => (contextMenuManager.selectedId = id),
}}
use:shortcuts={[
{
shortcut: { key: 'Tab' },
onShortcut: closeContextMenu,
},
{
shortcut: { key: 'Tab', shift: true },
onShortcut: closeContextMenu,
},
]}
>
<section class="fixed left-0 top-0 z-10 flex h-dvh w-dvw" {oncontextmenu} role="presentation">
<ContextMenu
{direction}
{x}
{y}
ariaActiveDescendant={contextMenuManager.selectedId}
ariaLabel={title}
bind:menuElement={menuContainer}
id={menuId}
isVisible
onClose={closeContextMenu}
>
{@render children?.()}
</ContextMenu>
</section>
</div>
{/if}
{/key}