immich/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte
Michel Heusschen 276101ee82
feat(web): improve shared link management on mobile (#11720)
* feat(web): improve shared link management on mobile

* fix format
2024-08-13 09:37:47 -05:00

157 lines
4 KiB
Svelte

<script lang="ts">
import CircleIconButton, {
type Color,
type Padding,
} from '$lib/components/elements/buttons/circle-icon-button.svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import {
getContextMenuPositionFromBoundingRect,
getContextMenuPositionFromEvent,
type Align,
} from '$lib/utils/context-menu';
import { generateId } from '$lib/utils/generate-id';
import { contextMenuNavigation } from '$lib/actions/context-menu-navigation';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
import { clickOutside } from '$lib/actions/click-outside';
import { shortcuts } from '$lib/actions/shortcut';
export let icon: string;
export let title: string;
/**
* The alignment of the context menu relative to the button.
*/
export let align: Align = 'top-left';
/**
* The direction in which the context menu should open.
*/
export let direction: 'left' | 'right' = 'right';
export let color: Color = 'transparent';
export let size: string | undefined = undefined;
export let padding: Padding | undefined = undefined;
/**
* Additional classes to apply to the button.
*/
export let buttonClass: string | undefined = undefined;
export let hideContent = false;
let isOpen = false;
let contextMenuPosition = { x: 0, y: 0 };
let menuContainer: HTMLUListElement;
let buttonContainer: HTMLDivElement;
const id = generateId();
const buttonId = `context-menu-button-${id}`;
const menuId = `context-menu-${id}`;
$: {
if (isOpen) {
$optionClickCallbackStore = handleOptionClick;
}
}
const openDropdown = (event: KeyboardEvent | MouseEvent) => {
contextMenuPosition = getContextMenuPositionFromEvent(event, align);
isOpen = true;
menuContainer?.focus();
};
const handleClick = (event: MouseEvent) => {
if (isOpen) {
closeDropdown();
return;
}
openDropdown(event);
};
const onEscape = (event: KeyboardEvent) => {
if (isOpen) {
// if the dropdown is open, stop the event from propagating
event.stopPropagation();
}
closeDropdown();
};
const onResize = () => {
if (!isOpen) {
return;
}
contextMenuPosition = getContextMenuPositionFromBoundingRect(buttonContainer.getBoundingClientRect(), align);
};
const closeDropdown = () => {
if (!isOpen) {
return;
}
focusButton();
isOpen = false;
$selectedIdStore = undefined;
};
const handleOptionClick = () => {
closeDropdown();
};
const focusButton = () => {
const button: HTMLButtonElement | null = buttonContainer.querySelector(`#${buttonId}`);
button?.focus();
};
</script>
<svelte:window on:resize={onResize} />
<div
use:contextMenuNavigation={{
closeDropdown,
container: menuContainer,
isOpen,
onEscape,
openDropdown,
selectedId: $selectedIdStore,
selectionChanged: (id) => ($selectedIdStore = id),
}}
use:clickOutside={{ onOutclick: closeDropdown }}
on:resize={onResize}
>
<div bind:this={buttonContainer}>
<CircleIconButton
{color}
{icon}
{padding}
{size}
{title}
aria-controls={menuId}
aria-expanded={isOpen}
aria-haspopup={true}
class={buttonClass}
id={buttonId}
on:click={handleClick}
/>
</div>
{#if isOpen || !hideContent}
<div
use:shortcuts={[
{
shortcut: { key: 'Tab' },
onShortcut: closeDropdown,
preventDefault: false,
},
{
shortcut: { key: 'Tab', shift: true },
onShortcut: closeDropdown,
preventDefault: false,
},
]}
>
<ContextMenu
{...contextMenuPosition}
{direction}
ariaActiveDescendant={$selectedIdStore}
ariaLabelledBy={buttonId}
bind:menuElement={menuContainer}
id={menuId}
isVisible={isOpen}
>
<slot />
</ContextMenu>
</div>
{/if}
</div>