atagen

malice

public
Star0
Watch0
77eddb6b710ffe2b884b8bdf7b0611041f7358e6
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/ContextMenu.svelteSvelte86 lines2.2 KB
1
<script lang="ts" context="module">
2
  export interface MenuItem {
3
    label: string;
4
    action: () => void;
5
    danger?: boolean;
6
    disabled?: boolean;
7
  }
8
</script>
9
10
<script lang="ts">
11
  import { onMount, tick } from 'svelte';
12
13
  export let items: MenuItem[];
14
  export let x: number;
15
  export let y: number;
16
  export let onClose: () => void;
17
18
  const MARGIN = 8;
19
20
  let menu: HTMLDivElement;
21
  let left = x;
22
  let top = y;
23
24
  onMount(async () => {
25
    await tick();
26
    const rect = menu.getBoundingClientRect();
27
    left = Math.max(MARGIN, Math.min(x, window.innerWidth - rect.width - MARGIN));
28
    top = Math.max(MARGIN, Math.min(y, window.innerHeight - rect.height - MARGIN));
29
    menu.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();
30
  });
31
32
  function pick(item: MenuItem): void {
33
    onClose();
34
    item.action();
35
  }
36
37
  function outside(ev: PointerEvent): void {
38
    if (!menu.contains(ev.target as Node)) onClose();
39
  }
40
41
  function key(ev: KeyboardEvent): void {
42
    if (ev.key === 'Escape') {
43
      ev.stopPropagation();
44
      onClose();
45
    }
46
  }
47
</script>
48
49
<svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} on:resize={onClose} on:blur={onClose} />
50
51
<div class="context-menu" role="menu" style={`left:${left}px;top:${top}px`} bind:this={menu}>
52
  {#each items as item}
53
    <button type="button" role="menuitem" class:danger={item.danger} disabled={item.disabled} on:click={() => pick(item)}>
54
      {item.label}
55
    </button>
56
  {/each}
57
</div>
58
59
<style>
60
  .context-menu {
61
    position: fixed;
62
    z-index: 90;
63
    min-width: 180px;
64
    display: flex;
65
    flex-direction: column;
66
    padding: 4px;
67
    background: var(--panel-2);
68
    border: 1px solid var(--border);
69
    border-radius: 8px;
70
    box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
71
  }
72
  button {
73
    background: transparent;
74
    border: none;
75
    border-radius: 6px;
76
    color: var(--text);
77
    font: inherit;
78
    font-size: 13.5px;
79
    text-align: left;
80
    padding: 7px 10px;
81
    cursor: pointer;
82
  }
83
  button:hover:not(:disabled), button:focus-visible { background: var(--hover); outline: none; }
84
  button:disabled { opacity: 0.5; cursor: default; }
85
  .danger { color: var(--danger); }
86
</style>