Skip to content

Commit aeadd53

Browse files
Magneaclaude
andcommitted
feat(drawer): dismiss on Escape key
Drawer only closed via underlay click or navigation, both gated behind hasAutoClose. Register Escape unconditionally through a new reusable onEscapeKey action (GlobalEventBus-backed) so every drawer, including those with hasAutoClose disabled, closes on ESC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 680990b commit aeadd53

3 files changed

Lines changed: 71 additions & 0 deletions

File tree

projects/client/src/lib/components/drawer/Drawer.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import RenderFor from "$lib/guards/RenderFor.svelte";
66
import { useMedia, WellKnownMediaQuery } from "$lib/stores/css/useMedia";
77
import { appendClassList } from "$lib/utils/actions/appendClassList";
8+
import { onEscapeKey } from "$lib/utils/actions/onEscapeKey";
89
import { writable } from "$lib/utils/store/WritableSubject";
910
import { onMount, type Snippet } from "svelte";
1011
import { slide } from "svelte/transition";
@@ -99,6 +100,7 @@
99100
transition:slide={{ duration: 150, axis: slideAxis }}
100101
use:portal
101102
use:trap
103+
use:onEscapeKey={onClose}
102104
use:appendClassList={classList}
103105
onintrostart={() => isOpening.set(true)}
104106
onintroend={() => {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { onEscapeKey } from './onEscapeKey.ts';
3+
4+
function pressKey(key: string) {
5+
globalThis.window.dispatchEvent(new KeyboardEvent('keydown', { key }));
6+
}
7+
8+
describe('onEscapeKey', () => {
9+
const node = document.createElement('div');
10+
11+
it('should invoke the callback when Escape is pressed', () => {
12+
let callCount = 0;
13+
const action = onEscapeKey(node, () => {
14+
callCount += 1;
15+
});
16+
17+
pressKey('Escape');
18+
19+
expect(callCount).toBe(1);
20+
action.destroy();
21+
});
22+
23+
it('should NOT invoke the callback for other keys', () => {
24+
let callCount = 0;
25+
const action = onEscapeKey(node, () => {
26+
callCount += 1;
27+
});
28+
29+
pressKey('Enter');
30+
pressKey('a');
31+
32+
expect(callCount).toBe(0);
33+
action.destroy();
34+
});
35+
36+
it('should clean up the listener when destroyed', () => {
37+
let callCount = 0;
38+
const action = onEscapeKey(node, () => {
39+
callCount += 1;
40+
});
41+
42+
action.destroy();
43+
pressKey('Escape');
44+
45+
expect(callCount).toBe(0);
46+
});
47+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { GlobalEventBus } from '../events/GlobalEventBus.ts';
2+
3+
export function onEscapeKey(_node: HTMLElement, onEscape: () => void) {
4+
const handleKeyDown = (event: KeyboardEvent) => {
5+
if (event.key !== 'Escape') {
6+
return;
7+
}
8+
9+
onEscape();
10+
};
11+
12+
const unregister = GlobalEventBus.getInstance().register(
13+
'keydown',
14+
handleKeyDown,
15+
);
16+
17+
return {
18+
destroy() {
19+
unregister();
20+
},
21+
};
22+
}

0 commit comments

Comments
 (0)