Skip to content

Commit 0a80766

Browse files
committed
✨ focus: navigate from keyboard events
1 parent 9c0b102 commit 0a80766

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { KeyDown, KeyEvent, KeyRepeat, KeyUp } from '@bomb.sh/tty';
2+
import type { Node } from '@bomb.sh/tree';
3+
import { describe, expect, test } from 'vitest';
4+
import { DispatchApi, useDispatch } from './dispatch.ts';
5+
import { getFocus, setFocusable, useFocus } from './focus.ts';
6+
import { useFocusNavigation } from './focus-navigation.ts';
7+
import { createHost, type Host } from './host.ts';
8+
import { KeyboardApi, useKeyboard } from './keyboard.ts';
9+
10+
const { dispatch } = DispatchApi.methods;
11+
12+
describe('focus navigation', () => {
13+
test.each([
14+
{ event: keydown('Tab'), expected: 'second' },
15+
{ event: keyrepeat('Tab'), expected: 'second' },
16+
{ event: keydown('Backtab'), expected: 'third' },
17+
{ event: keyrepeat('Backtab'), expected: 'third' },
18+
])('$event.type $event.code moves focus and consumes the event', ({ event, expected }) => {
19+
const { host, first, second, third } = setup();
20+
const delegated: RecordedKeyEvent[] = [];
21+
recordKeyboard(first, delegated);
22+
23+
dispatch(host.root, event);
24+
25+
expect(getFocus(host.root)).toBe(expected === 'second' ? second : third);
26+
expect(delegated).toEqual([]);
27+
});
28+
29+
test.each([keydown('a'), keyrepeat('a'), keyup('Tab'), keyup('Backtab')])(
30+
'delegates $type $code without moving focus',
31+
(event) => {
32+
const { host, first } = setup();
33+
const delegated: RecordedKeyEvent[] = [];
34+
recordKeyboard(first, delegated);
35+
36+
dispatch(host.root, event);
37+
38+
expect(getFocus(host.root)).toBe(first);
39+
expect(delegated).toEqual([{ node: first, event }]);
40+
},
41+
);
42+
});
43+
44+
interface Setup {
45+
host: Host;
46+
first: Node;
47+
second: Node;
48+
third: Node;
49+
}
50+
51+
interface RecordedKeyEvent {
52+
node: Node;
53+
event: KeyEvent;
54+
}
55+
56+
function setup(): Setup {
57+
const host = createHost();
58+
useDispatch(host);
59+
useFocus(host);
60+
useKeyboard(host);
61+
useFocusNavigation(host);
62+
63+
return {
64+
host,
65+
first: createFocusable(host),
66+
second: createFocusable(host),
67+
third: createFocusable(host),
68+
};
69+
}
70+
71+
function createFocusable(host: Host): Node {
72+
const element = host.createElement('box');
73+
host.insertBefore(host.element, element);
74+
setFocusable(element.node!);
75+
return element.node!;
76+
}
77+
78+
function keydown(code: KeyDown['code']): KeyDown {
79+
return { type: 'keydown', key: code, code };
80+
}
81+
82+
function keyrepeat(code: KeyRepeat['code']): KeyRepeat {
83+
return { type: 'keyrepeat', key: code, code };
84+
}
85+
86+
function keyup(code: KeyUp['code']): KeyUp {
87+
return { type: 'keyup', key: code, code };
88+
}
89+
90+
function recordKeyboard(node: Node, events: RecordedKeyEvent[]): void {
91+
KeyboardApi.around(node, {
92+
keydown([target, event]) {
93+
events.push({ node: target, event });
94+
},
95+
keyup([target, event]) {
96+
events.push({ node: target, event });
97+
},
98+
keyrepeat([target, event]) {
99+
events.push({ node: target, event });
100+
},
101+
});
102+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { advanceFocus, retreatFocus } from './focus.ts';
2+
import type { Host } from './host.ts';
3+
import { KeyboardApi } from './keyboard.ts';
4+
5+
export function useFocusNavigation(host: Host): void {
6+
KeyboardApi.around(host.root, {
7+
keydown([node, event], next) {
8+
if (event.code === 'Tab') {
9+
advanceFocus(node);
10+
} else if (event.code === 'Backtab') {
11+
retreatFocus(node);
12+
} else {
13+
next(node, event);
14+
}
15+
},
16+
keyrepeat([node, event], next) {
17+
if (event.code === 'Tab') {
18+
advanceFocus(node);
19+
} else if (event.code === 'Backtab') {
20+
retreatFocus(node);
21+
} else {
22+
next(node, event);
23+
}
24+
},
25+
});
26+
}

0 commit comments

Comments
 (0)