Keyboard Handling: Sequences and Combinations Keyboard Handling: Sequences and Combinationskeyboard open-source typescript

In this post, we'll explore using my keyboard handling library to recognize and respond to keystroke sequences and combinations.

Demo

Here's a simple demo that showcases how keystrokes can be detected and handled in real-time using the library. Try pressing any of the specified sequences or combinations, and see the visual feedback. You can extend this example by adding more sequences.

The TypeScript source code for the demo is available in this repository.

Code Explanation

Initial Setup

To begin, we use a mix of HTML and JSX elements to build the structure.

The #output div is an interactive area where users can click to start tracking their key inputs. As keys are pressed, results display here, connecting each recognized sequence with the corresponding text.

We include a TextArea within a Field component for user-defined key binding configurations. This TextArea comes pre-filled with JSON data specifying key sequences—like mapping "enter" to "hello" or the famous "KONAMI" code sequence.

document.body.append(
	<>
		<style>{`
			#bindings { font: 16px var(--cxl-font-monospace); }
			#output { display: flex; gap: 4px 8px; flex-wrap:wrap; }
		`}</style>
		<T font="h5"> Key Strokes </T>
		<div id="output">
			<T font="h6" center>
				Click here to start. Keep this window active and press any key.
				<br />
				If you press a key that matches one of the bindings below, the
				corresponding text value will appear in the output.
			</T>
		</div>
		<br />
		<br />
		<T font="h5"> Bindings </T>
		<Field outline>
			<TextArea
				id="bindings"
				rules="json"
				value='{
	"enter": "hello",
	"a b c": "ABC",
	"ctrl+alt+shift+enter": "MULTIKEY",
	"shift+a shift+d": "SHIFT KEY",
	"alt+a alt+b": "ALT Combination",
	"up up down down left right left right b a": "KONAMI"
	":": "COLON"
}'
			></TextArea>
		</Field>
	</>,
);

Handling Key Sequences

Our first challenge is translating key sequences into actions or text. We want users to see text outputs corresponding to key strokes or predefined key combinations.

For this we use a keymap object that maps key sequences to specific text values. The app listens for key sequences and displays the corresponding value if it matches the keymap.

Key Binding Inputs

The bindings are editable through a JSON input in a <TextArea>. This allows users to define their key sequences. We need error handling while parsing this JSON input since malformed inputs may break functionality.

The parse function parses JSON to initialize the keymap and handles any errors by logging them, ensuring the user experience isn't disrupted.

The keys in the keymap, which contain the keyboard shortcuts, are normalized to a standard format. This ensures that the library can recognize them properly.

function parse() {
	const val = bindings.value;
	try {
		if (val)
			keymap = Object.entries(JSON.parse(val)).reduce(
				(acc, [key, value]) => {
					acc[normalize(key)] = value as string;
					return acc;
				},
				{} as Record<string, string>,
			);
	} catch (e) {
		console.error(e);
	}

	keymap = keymap || {};
}

handleKeyboard

The handleKeyboard function accepts options such as the HTML element for capturing keyboard events, a callback for processing key events, a delay for resetting key sequences, an optional keyboard layout, and a boolean for event phase capture.

export interface KeyboardOptions {
	/**
	 * Specifies the HTML element on which keyboard events are captured.
	 * This element will receive focus and listen for key events.
	 */
	element: HTMLElement;

	/**
	 * A callback function that processes key events. It receives the current key being pressed
	 * and the sequence of keys accumulated so far. It returns `true` or `false` to indicate whether
	 * the sequence was consumed.
	 */
	onKey: (key: string, sequence: string[]) => boolean;

	/**
	 * The time in milliseconds to wait between key presses before resetting the sequence.
	 * Defaults to 250ms if not provided, ensuring thoughtful key sequences rather than accidental key presses.
	 */
	delay?: number;

	/**
	 * Optional parameter to specify a custom `KeyboardLayout`.
	 * Defaults to 'en-US' if not provided. This affects how key combinations and modifier keys are interpreted.
	 */
	layout?: KeyboardLayout;

	/**
	 * Boolean indicating whether to capture key events in the capture phase.
	 * This determines when the event listener reacts in the event propagation flow.
	 */
	capture?: boolean;
}

The library supports different keyboard layouts using the layout option. The KeyboardLayout interface describes how a layout should be structured internally.

export interface KeyboardLayout {
	/**
	 * A mapping of shifted keys to their non-shifted counterparts, used to accurately
	 * interpret which character a shifted key press represents.
	 */
	shiftMap: Record<string, string | undefined>;

	/**
	 * Describes a function that translates a `Key` object (representing a keyboard event) to a character string.
	 */
	translate(ev: Key): string;

	/**
	 * Indicates whether the 'meta' or 'ctrl' key is the primary modifier used for commands,
	 * differing based on the operating system (e.g., 'metaKey' for macOS).
	 */
	modKey?: 'metaKey' | 'ctrlKey';
}

Handling Key Events

The handleKeyboard function from our library translates key events into string keys. The handle callback checks if a key sequence matches any entry in our keymap, and if it does, it updates the display elements accordingly.

function handle(name: string, sequence: string[]) {
	if (!lastSequence) output.innerHTML = '';
	const found = keymap[name];
	if (sequence !== lastSequence) {
		kbd = (<Chip color="primary">{found || name}</Chip>) as Chip;
		lastSequence = sequence;
		output.insertBefore(kbd, output.children[0]);
	} else {
		kbd.textContent = found || sequence.join(' ');
	}

	return !!found;
}

handleKeyboard({ element: document.body, onKey: handle });

Key sequences often involve keys like Enter or Ctrl, which may trigger default browser behaviors. To prevent this and ensure sequences are captured correctly, we globally listen for key events on window. We use preventDefault() and stopPropagation() to manage these events.

window.addEventListener('keydown', ev => ev.preventDefault());
bindings.addEventListener('keydown', ev => ev.stopPropagation(), true);

To prevent lagging updates when users change the JSON input, we use debounceFunction. This delays parse execution after editing stops.

bindings.addEventListener('change', debounceFunction(parse, 500));
Back to Main Page