feat(ui)!: propagate action trigger events to action handlers (#818)

This commit is contained in:
Mees Frensel
2026-09-23 05:31:18 -04:00
committed by GitHub
parent 5b0a40c5d6
commit 2ed5c58981
11 changed files with 108 additions and 17 deletions
+1 -1
View File
@@ -208,7 +208,7 @@ export const components: ComponentItem[] = [
{ name: 'Tooltip', icon: mdiInformationSlabCircleOutline, activeIcon: mdiInformationSlabCircle },
];
const onAction: ActionItemHandler = (item) => {
const onAction: ActionItemHandler = ({ action: item }) => {
toastManager.show({
title: `Clicked ${item.title}`,
color: item.color ?? 'primary',
@@ -1,6 +1,43 @@
<script lang="ts">
import { ExampleActions } from '$lib/constants.js';
import { ActionButton, Heading } from '@immich/ui';
import {
ActionButton,
type ActionEvent,
type ActionItem,
CommandPaletteDefaultProvider,
Heading,
Kbd,
Text,
toastManager,
} from '@immich/ui';
import { mdiDeleteForeverOutline, mdiTrashCanOutline } from '@mdi/js';
const onAction = ({ action, event }: ActionEvent) => {
if (event.type === 'click') {
toastManager.show({
title: `Clicked ${action.title}…`,
description: '…using the mouse',
icon: action.icon,
});
} else {
const { key, ctrlKey: force } = event as KeyboardEvent;
toastManager.show({
title: `Used shortcut for ${action.title}…`,
description: `…using ${force ? 'Ctrl+' : ''}${key}`,
color: force ? 'danger' : undefined,
icon: force ? mdiDeleteForeverOutline : action.icon,
});
}
};
const DeleteWithShortcuts: ActionItem = {
title: 'Delete',
icon: mdiTrashCanOutline,
color: 'danger',
shortcuts: [{ key: 'Delete' }, { key: 'Delete', ctrl: true }],
onAction,
};
</script>
<Heading size="small" class="mb-2">Icon buttons</Heading>
@@ -20,3 +57,13 @@
<ActionButton type="button" action={ExampleActions.Delete} />
<ActionButton type="button" action={ExampleActions.Download} variant="filled" color="primary" />
</div>
<CommandPaletteDefaultProvider name="Example action" actions={[DeleteWithShortcuts]} />
<Heading size="small" class="mt-6 mb-2">Event info</Heading>
<div class="flex flex-col gap-2">
<Text>Use the mouse or keyboard shortcuts <Kbd>Del</Kbd> or <Kbd>Ctrl Del</Kbd></Text>
<div class="w-fit">
<ActionButton action={DeleteWithShortcuts} type="button" />
</div>
</div>
@@ -5,6 +5,8 @@
import { Markdown, Text } from '@immich/ui';
import BasicExample from './BasicExample.svelte';
import basicExample from './BasicExample.svelte?raw';
import EventExample from './EventExample.svelte';
import eventExample from './EventExample.svelte?raw';
</script>
<ComponentPage name="CommandPalette" description="A command menu that can be used to search, filter, and select items">
@@ -12,4 +14,5 @@
<Text>See <ComponentLink name="CommandPaletteButton" /> for a dedicated button to open the component</Text>
</Markdown.Alert>
<ComponentExamples examples={[{ title: 'Basic', code: basicExample, component: BasicExample }]} />
<ComponentExamples examples={[{ title: 'Event types', code: eventExample, component: EventExample }]} />
</ComponentPage>
@@ -0,0 +1,31 @@
<script lang="ts">
import {
Button,
CommandPaletteDefaultProvider,
commandPaletteManager,
Text,
toastManager,
type ActionEvent,
type ActionItem,
} from '@immich/ui';
import { mdiCallMerge } from '@mdi/js';
const onAction = ({ event }: ActionEvent) => {
toastManager.info(`Event type: ${event.type}`);
if (event instanceof KeyboardEvent) {
toastManager.info(`Key used: ${event.key}`);
}
};
const action: ActionItem = {
title: 'Trigger me',
description: 'Trigger me using your mouse or keyboard',
icon: mdiCallMerge,
onAction,
};
</script>
<CommandPaletteDefaultProvider actions={[action]} types={['custom']} />
<Text>Get the event that triggered the action</Text>
<Button onclick={() => commandPaletteManager.open('type:custom')}>Open</Button>
@@ -11,7 +11,7 @@
mdiTrashCanOutline,
} from '@mdi/js';
const onAction: ActionItemHandler = (item) => {
const onAction: ActionItemHandler = ({ action: item }) => {
toastManager.show({
title: `Clicked ${item.title}`,
color: item.color ?? 'primary',
@@ -19,7 +19,7 @@
const common = $derived({
variant: variantOverride ?? 'ghost',
color: colorOverride ?? action.color ?? 'secondary',
onclick: () => onAction(action),
onclick: (event: MouseEvent) => onAction({ action, event }),
});
</script>
@@ -11,7 +11,7 @@
type Props = {
item: ActionItem;
selected: boolean;
onSelect: () => void;
onSelect: (event: MouseEvent) => void;
};
const { item, selected, onSelect }: Props = $props();
@@ -37,7 +37,7 @@
<div bind:this={ref} class="p-1">
<Button
onclick={() => onSelect()}
onclick={(event: MouseEvent) => onSelect(event)}
fullWidth
variant={selected ? 'outline' : 'ghost'}
color="secondary"
@@ -152,7 +152,7 @@
<DropdownMenu.Item
textValue={item.title}
closeOnSelect
onSelect={() => item.onAction(item)}
onSelect={(event) => item.onAction({ action: item, event })}
class={itemStyles({ color: item.color, inset: true })}
>
{@render itemText(item)}
@@ -168,7 +168,7 @@
<DropdownMenu.Item
textValue={item.title}
closeOnSelect
onSelect={() => item.onAction(item)}
onSelect={(event) => item.onAction({ action: item, event })}
title={item.title}
class={itemStyles({ color: item.color })}
>
@@ -16,11 +16,11 @@
type CommandPaletteTranslations,
} from '$lib/services/command-palette-manager.svelte.js';
import { t } from '$lib/services/translation.svelte.js';
import type { ActionItem } from '$lib/types.js';
import type { ActionEvent, ActionItem } from '$lib/types.js';
import { mdiArrowDown, mdiArrowUp, mdiKeyboardEsc, mdiKeyboardReturn, mdiMagnify } from '@mdi/js';
type Props = {
onClose: (action?: ActionItem) => void;
onClose: (actionEvent?: ActionEvent) => void;
translations?: CommandPaletteTranslations;
initialQuery?: string;
};
@@ -54,7 +54,7 @@
}
case 'select': {
onClose(commandPaletteManager.selectedItem);
onClose({ action: commandPaletteManager.selectedItem, event });
break;
}
}
@@ -124,7 +124,7 @@
<CommandPaletteItem
{item}
selected={commandPaletteManager.isSelected(item)}
onSelect={() => onClose(item)}
onSelect={(event) => onClose({ action: item, event })}
/>
{/each}
</div>
@@ -2,7 +2,7 @@ import { matchesShortcut, shortcuts, shouldIgnoreEvent } from '$lib/actions/shor
import CommandPaletteModal from '$lib/internal/CommandPaletteModal.svelte';
import { modalManager } from '$lib/services/modal-manager.svelte.js';
import { isModalOpen } from '$lib/state/modal-state.svelte.js';
import type { ActionItem, MaybePromise, TranslationProps } from '$lib/types.js';
import type { ActionEvent, ActionItem, MaybePromise, TranslationProps } from '$lib/types.js';
import { isEnabled } from '$lib/utilities/common.js';
import { asArray, generateId } from '$lib/utilities/internal.js';
import Fuse, { type FuseResult, type FuseResultMatch, type IFuseOptions } from 'fuse.js';
@@ -220,13 +220,13 @@ class CommandPaletteManager {
event.preventDefault();
}
action?.onAction(action);
action?.onAction({ action, event });
return;
}
}
async #onClose(action?: ActionItem) {
await action?.onAction(action);
async #onClose(actionEvent?: ActionEvent) {
await actionEvent?.action.onAction(actionEvent);
this.#isOpen = false;
this.#results = [];
}
+11 -1
View File
@@ -370,7 +370,17 @@ export type DatePickerProps = {
export type IfLike = { $if?: () => boolean };
export type ActionItemHandler<T extends ActionItem = ActionItem> = (item: T) => unknown | Promise<unknown>;
/**
* The event will be a `KeyboardEvent | MouseEvent` for actions triggered via
* normal (icon) buttons and the command palette (including shortcuts), and
* a custom event type for actions triggered via bits-ui components like the
* context menu.
*/
export type ActionEvent<T extends ActionItem = ActionItem> = { action: T; event: Event };
export type ActionItemHandler<T extends ActionItem = ActionItem> = (
actionEvent: ActionEvent<T>,
) => unknown | Promise<unknown>;
export type LinkItem = {
title: string;