Skip to content

Commit e368200

Browse files
authored
feat: Context-Menu (#156)
* feat: Context-Menu * fix: name of component * fix: as per suggestion * fix: action menu position * fix: class
1 parent 29af7fe commit e368200

File tree

4 files changed

+109
-2
lines changed

4 files changed

+109
-2
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ComponentProps } from 'svelte';
2+
import { ActionMenu } from '..';
3+
4+
export default {
5+
title: 'UI/ActionMenu',
6+
component: ActionMenu,
7+
tags: ['autodocs'],
8+
render: (args: { Component: ActionMenu; props: ComponentProps<typeof ActionMenu> }) => ({
9+
Component: ActionMenu,
10+
props: args
11+
})
12+
};
13+
14+
export const Primary = {
15+
args: {
16+
options: [
17+
{ name: 'Report', handler: () => alert('report') },
18+
{ name: 'Clear chat', handler: () => alert('clear') }
19+
]
20+
}
21+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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}

platforms/metagram/src/lib/fragments/Drawer/Drawer.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
cssClass: '',
4949
initialBreak: 'middle',
5050
events: {
51-
onBackdropTap: () => dismiss()
52-
}
51+
onBackdropTap: () => dismiss()
52+
}
5353
});
5454
if (isPaneOpen) {
5555
drawer.present({ animate: true });

platforms/metagram/src/lib/fragments/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export { default as MessageInput } from './MessageInput/MessageInput.svelte';
55
export { default as InputFile } from './InputFile/InputFile.svelte';
66
export { default as Drawer } from './Drawer/Drawer.svelte';
77
export { default as Message } from './Message/Message.svelte';
8+
export { default as ActionMenu } from './ActionMenu/ActionMenu.svelte';
89
export { default as Modal } from './Modal/Modal.svelte';

0 commit comments

Comments
 (0)