|
| 1 | +<script lang="ts"> |
| 2 | + import { clickOutside, cn } from '$lib/utils'; |
| 3 | + import { MoreVerticalIcon } from '@hugeicons/core-free-icons'; |
| 4 | + import { HugeiconsIcon } from '@hugeicons/svelte'; |
| 5 | + import { tick } from 'svelte'; |
| 6 | + import type { HTMLAttributes } from 'svelte/elements'; |
| 7 | +
|
| 8 | + interface IContextMenuProps extends HTMLAttributes<HTMLElement> { |
| 9 | + options: Array<{ name: string; handler: () => void }>; |
| 10 | + } |
| 11 | +
|
| 12 | + let { options = [], ...restProps }: IContextMenuProps = $props(); |
| 13 | + let showActionMenu = $state(false); |
| 14 | + let menuEl: HTMLUListElement | null = $state(null); |
| 15 | + let buttonEl: HTMLElement | null = null; |
| 16 | +
|
| 17 | + function openMenu() { |
| 18 | + showActionMenu = true; |
| 19 | +
|
| 20 | + tick().then(() => { |
| 21 | + if (menuEl && buttonEl) { |
| 22 | + const { innerWidth, innerHeight } = window; |
| 23 | + const menuRect = menuEl.getBoundingClientRect(); |
| 24 | + const buttonRect = buttonEl.getBoundingClientRect(); |
| 25 | +
|
| 26 | + // Position vertically aligned to button top (viewport) |
| 27 | + let top = buttonRect.top; |
| 28 | + let left = buttonRect.right; |
| 29 | +
|
| 30 | + // If it overflows right, position to the left of the button |
| 31 | + if (innerWidth - buttonRect.right < menuRect.width) { |
| 32 | + left = buttonRect.left - menuRect.width; |
| 33 | + } |
| 34 | +
|
| 35 | + // If it overflows bottom, adjust upward |
| 36 | + if (innerHeight - buttonRect.top < menuRect.height) { |
| 37 | + top = innerHeight - menuRect.height - 10; |
| 38 | + } |
| 39 | +
|
| 40 | + menuEl.style.left = `${left}px`; |
| 41 | + menuEl.style.top = `${top}px`; |
| 42 | + } |
| 43 | + }); |
| 44 | + } |
| 45 | +
|
| 46 | + function closeMenu() { |
| 47 | + showActionMenu = false; |
| 48 | + } |
| 49 | +
|
| 50 | + const cBase = 'fixed z-50 w-[max-content] py-2 px-5 rounded-2xl bg-white shadow-lg'; |
| 51 | +</script> |
| 52 | + |
| 53 | +<div class="relative inline-block"> |
| 54 | + <button |
| 55 | + bind:this={buttonEl} |
| 56 | + onclick={(e) => { |
| 57 | + e.preventDefault(), openMenu(); |
| 58 | + }} |
| 59 | + > |
| 60 | + <HugeiconsIcon icon={MoreVerticalIcon} size={24} color="black" /> |
| 61 | + </button> |
| 62 | +</div> |
| 63 | + |
| 64 | +{#if showActionMenu} |
| 65 | + <ul |
| 66 | + {...restProps} |
| 67 | + use:clickOutside={() => closeMenu()} |
| 68 | + bind:this={menuEl} |
| 69 | + class={cn([cBase, restProps.class].join(' '))} |
| 70 | + > |
| 71 | + {#each options as option} |
| 72 | + <!-- svelte-ignore a11y_click_events_have_key_events --> |
| 73 | + <!-- svelte-ignore a11y_no_noninteractive_element_interactions --> |
| 74 | + <li |
| 75 | + class="cursor-pointer py-3" |
| 76 | + onclick={() => { |
| 77 | + option.handler(); |
| 78 | + closeMenu(); |
| 79 | + }} |
| 80 | + > |
| 81 | + <p class="text-black-800">{option.name}</p> |
| 82 | + </li> |
| 83 | + {/each} |
| 84 | + </ul> |
| 85 | +{/if} |
0 commit comments