|
4 | 4 | import { AlertTriangle, X, Trash2, Check } from 'lucide-svelte'; |
5 | 5 | import Button from '../components/Button.svelte'; |
6 | 6 | import { t } from '../stores/i18n.svelte.js'; |
| 7 | + import { getShortcut, matchesShortcut } from '../utils/keyboardShortcuts.js'; |
7 | 8 |
|
8 | 9 | const dispatch = createEventDispatcher(); // Keep for backward compatibility |
9 | 10 |
|
|
19 | 20 | oncancel = null |
20 | 21 | } = $props(); |
21 | 22 |
|
| 23 | + // Get shortcut configurations |
| 24 | + const submitShortcut = getShortcut('modal', 'submit'); |
| 25 | + const cancelShortcut = getShortcut('modal', 'cancel'); |
| 26 | +
|
| 27 | + // Focus management |
| 28 | + let backdropRef = $state(null); |
| 29 | + let previouslyFocusedElement = null; |
| 30 | +
|
| 31 | + // Store previously focused element when show becomes true |
| 32 | + $effect(() => { |
| 33 | + if (show && !previouslyFocusedElement) { |
| 34 | + previouslyFocusedElement = document.activeElement; |
| 35 | + } |
| 36 | + if (!show && previouslyFocusedElement) { |
| 37 | + // Restore focus when modal closes |
| 38 | + previouslyFocusedElement?.focus(); |
| 39 | + previouslyFocusedElement = null; |
| 40 | + } |
| 41 | + }); |
| 42 | +
|
| 43 | + // Focus backdrop after intro transition completes |
| 44 | + function handleIntroEnd() { |
| 45 | + backdropRef?.focus(); |
| 46 | + } |
| 47 | +
|
22 | 48 | // Use translations for defaults |
23 | 49 | const resolvedTitle = $derived(title ?? t('common.areYouSure')); |
24 | 50 | const resolvedMessage = $derived(message ?? t('common.confirmAction')); |
25 | 51 | const resolvedConfirmText = $derived(confirmText ?? t('common.confirm')); |
26 | 52 | const resolvedCancelText = $derived(cancelText ?? t('common.cancel')); |
27 | 53 |
|
28 | | - // Auto-close on escape key |
| 54 | + // Handle keyboard navigation using standard shortcuts |
29 | 55 | function handleKeydown(event) { |
30 | | - if (event.key === 'Escape' && show) { |
| 56 | + // Check for cancel shortcut (Escape) |
| 57 | + if (matchesShortcut(event, cancelShortcut)) { |
31 | 58 | cancel(); |
| 59 | + return; |
| 60 | + } |
| 61 | +
|
| 62 | + // Check for submit shortcut (Cmd/Ctrl+Enter) |
| 63 | + if (matchesShortcut(event, submitShortcut)) { |
| 64 | + event.preventDefault(); |
| 65 | + doConfirm(); |
| 66 | + return; |
| 67 | + } |
| 68 | +
|
| 69 | + // Enter without modifier confirms (unless on cancel button) |
| 70 | + if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey) { |
| 71 | + const activeElement = document.activeElement; |
| 72 | + const isOnCancelButton = activeElement?.textContent?.trim() === resolvedCancelText; |
| 73 | + if (!isOnCancelButton) { |
| 74 | + event.preventDefault(); |
| 75 | + doConfirm(); |
| 76 | + } |
32 | 77 | } |
33 | 78 | } |
34 | 79 |
|
35 | | - function confirm() { |
| 80 | + function doConfirm() { |
36 | 81 | dispatch('confirm'); // Keep for backward compatibility |
37 | 82 | onconfirm?.(); // New Svelte 5 style |
38 | 83 | show = false; |
|
80 | 125 | let styles = $derived(getVariantStyles(variant)); |
81 | 126 | </script> |
82 | 127 |
|
83 | | -<svelte:window onkeydown={handleKeydown} /> |
84 | | -
|
85 | 128 | {#if show} |
86 | 129 | <!-- Modal backdrop --> |
87 | 130 | <div |
| 131 | + bind:this={backdropRef} |
88 | 132 | transition:fade={{ duration: 150 }} |
89 | | - class="modal-backdrop fixed inset-0 z-50 flex items-center justify-center p-4" |
| 133 | + onintroend={handleIntroEnd} |
| 134 | + class="modal-backdrop fixed inset-0 z-50 flex items-center justify-center p-4 focus:outline-none" |
90 | 135 | onclick={handleBackdropClick} |
| 136 | + onkeydown={handleKeydown} |
91 | 137 | role="dialog" |
92 | 138 | aria-modal="true" |
93 | 139 | aria-labelledby="dialog-title" |
94 | 140 | aria-describedby="dialog-description" |
| 141 | + tabindex="-1" |
95 | 142 | > |
96 | 143 | <!-- Modal content --> |
97 | 144 | <div |
|
142 | 189 | variant="default" |
143 | 190 | onclick={cancel} |
144 | 191 | size="small" |
| 192 | + keyboardHint="Esc" |
145 | 193 | > |
146 | 194 | {resolvedCancelText} |
147 | 195 | </Button> |
148 | 196 |
|
149 | 197 | <Button |
150 | 198 | variant={styles.buttonVariant} |
151 | | - onclick={confirm} |
| 199 | + onclick={doConfirm} |
152 | 200 | size="small" |
| 201 | + keyboardHint="↵" |
153 | 202 | > |
154 | 203 | {resolvedConfirmText} |
155 | 204 | </Button> |
|
0 commit comments