118 lines
3.0 KiB
TypeScript
118 lines
3.0 KiB
TypeScript
|
|
import { test } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { onTabPress, preventScrollWithArrowKey, onEnterOrSpace, stopPropagation } from './keyboard';
|
||
|
|
|
||
|
|
type Evt = {
|
||
|
|
key: string;
|
||
|
|
which: number;
|
||
|
|
altKey: boolean;
|
||
|
|
ctrlKey: boolean;
|
||
|
|
metaKey: boolean;
|
||
|
|
shiftKey: boolean;
|
||
|
|
prevented: boolean;
|
||
|
|
preventDefault(): void;
|
||
|
|
};
|
||
|
|
const evt = (key: string, which: number): Evt => ({
|
||
|
|
key,
|
||
|
|
which,
|
||
|
|
altKey: false,
|
||
|
|
ctrlKey: false,
|
||
|
|
metaKey: false,
|
||
|
|
shiftKey: false,
|
||
|
|
prevented: false,
|
||
|
|
preventDefault() {
|
||
|
|
this.prevented = true;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
test('onTabPress fires only on Tab', () => {
|
||
|
|
let called = 0;
|
||
|
|
const tab = evt('Tab', 9);
|
||
|
|
onTabPress(tab, () => {
|
||
|
|
called += 1;
|
||
|
|
});
|
||
|
|
assert.equal(called, 1);
|
||
|
|
assert.equal(tab.prevented, true);
|
||
|
|
|
||
|
|
const a = evt('a', 65);
|
||
|
|
onTabPress(a, () => {
|
||
|
|
called += 1;
|
||
|
|
});
|
||
|
|
assert.equal(called, 1); // unchanged
|
||
|
|
assert.equal(a.prevented, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('preventScrollWithArrowKey prevents default only on arrows', () => {
|
||
|
|
const up = evt('ArrowUp', 38);
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
preventScrollWithArrowKey(up as any);
|
||
|
|
assert.equal(up.prevented, true);
|
||
|
|
|
||
|
|
const a = evt('a', 65);
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
preventScrollWithArrowKey(a as any);
|
||
|
|
assert.equal(a.prevented, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('onEnterOrSpace fires on Enter or Space, not others', () => {
|
||
|
|
let count = 0;
|
||
|
|
const handler = onEnterOrSpace(() => {
|
||
|
|
count += 1;
|
||
|
|
});
|
||
|
|
|
||
|
|
const enter = evt('Enter', 13);
|
||
|
|
handler(enter);
|
||
|
|
assert.equal(count, 1);
|
||
|
|
assert.equal(enter.prevented, true);
|
||
|
|
|
||
|
|
const space = evt(' ', 32);
|
||
|
|
handler(space);
|
||
|
|
assert.equal(count, 2);
|
||
|
|
assert.equal(space.prevented, true);
|
||
|
|
|
||
|
|
const other = evt('a', 65);
|
||
|
|
handler(other);
|
||
|
|
assert.equal(count, 2); // unchanged
|
||
|
|
assert.equal(other.prevented, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('stopPropagation: stops unless an editable element is focused', () => {
|
||
|
|
const makeKeyEvt = () => {
|
||
|
|
let stopped = false;
|
||
|
|
return {
|
||
|
|
ev: {
|
||
|
|
stopPropagation() {
|
||
|
|
stopped = true;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wasStopped: () => stopped,
|
||
|
|
};
|
||
|
|
};
|
||
|
|
const withActive = (activeElement: unknown) => {
|
||
|
|
(globalThis as { document?: unknown }).document = { activeElement };
|
||
|
|
};
|
||
|
|
|
||
|
|
// nothing focused → stops, returns true
|
||
|
|
withActive(null);
|
||
|
|
let k = makeKeyEvt();
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
assert.equal(stopPropagation(k.ev as any), true);
|
||
|
|
assert.equal(k.wasStopped(), true);
|
||
|
|
|
||
|
|
// input focused → does not stop, returns false
|
||
|
|
withActive({ nodeName: 'INPUT', getAttribute: () => null });
|
||
|
|
k = makeKeyEvt();
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
assert.equal(stopPropagation(k.ev as any), false);
|
||
|
|
assert.equal(k.wasStopped(), false);
|
||
|
|
|
||
|
|
// contenteditable focused → does not stop
|
||
|
|
withActive({
|
||
|
|
nodeName: 'DIV',
|
||
|
|
getAttribute: (a: string) => (a === 'contenteditable' ? 'true' : null),
|
||
|
|
});
|
||
|
|
k = makeKeyEvt();
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
assert.equal(stopPropagation(k.ev as any), false);
|
||
|
|
});
|