All files / virtual-keyboard/src/engine resolveLayout.ts

100% Statements 26/26
97.05% Branches 33/34
100% Functions 8/8
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                        230x         3541x 66x 22x               3541x   92x   3x   83x   3363x                                                       3541x   3541x 3541x 2966x 229x 2737x 206x   2531x 29x         3541x           3541x                                               139x     3290x                 97x              
import { isModifierAction } from "../constants";
import type {
  KeyboardLayout,
  KeyboardModifiers,
  KeyDefinition,
  ResolvedKey,
  ResolvedLayout,
  VirtualKeyEvent,
} from "../types";
 
/** Whether a string is a single Latin letter (a–z / A–Z). */
function isAlpha(value: string): boolean {
  return value.length === 1 && /[a-zA-Z]/.test(value);
}
 
/** Infer a key's semantic type when it is not declared explicitly. */
function inferType(key: KeyDefinition): NonNullable<KeyDefinition["type"]> {
  if (key.type) return key.type;
  if (key.action) return isModifierAction(key.action) ? "modifier" : "action";
  return "char";
}
 
/** Whether a resolved/definition key is a currently-active modifier. */
function isActiveModifier(
  key: KeyDefinition,
  modifiers: KeyboardModifiers
): boolean {
  switch (key.action) {
    case "shift":
      return modifiers.shift;
    case "capslock":
      return modifiers.capsLock;
    case "layer":
      return modifiers.layer;
    default:
      return false;
  }
}
 
/**
 * Resolve a single key against the current modifier state, computing the value
 * it will emit ({@link ResolvedKey.effectiveValue}) and the label to render
 * ({@link ResolvedKey.displayLabel}).
 *
 * Resolution precedence for character keys:
 * 1. **Symbol layer** — when the layer is active and the key declares a
 *    `layerKey`, that value is used.
 * 2. **Shift** — the explicit `shiftKey`, or the uppercased letter.
 * 3. **Caps Lock** — uppercases Latin letters only.
 * 4. Otherwise the base `key`.
 *
 * Action and modifier keys always emit their base `key` unchanged.
 *
 * @example
 * ```ts
 * resolveKey({ key: "a" }, { shift: true, capsLock: false, layer: false }).effectiveValue; // "A"
 * resolveKey({ key: "1", layerKey: "!" }, { shift: false, capsLock: false, layer: true }).effectiveValue; // "!"
 * ```
 */
export function resolveKey(
  key: KeyDefinition,
  modifiers: KeyboardModifiers
): ResolvedKey {
  const type = inferType(key);
 
  let effectiveValue = key.key;
  if (type === "char") {
    if (modifiers.layer && key.layerKey !== undefined) {
      effectiveValue = key.layerKey;
    } else if (modifiers.shift) {
      effectiveValue =
        key.shiftKey ?? (isAlpha(key.key) ? key.key.toUpperCase() : key.key);
    } else if (modifiers.capsLock && isAlpha(key.key)) {
      effectiveValue = key.key.toUpperCase();
    }
  }
 
  const displayLabel =
    key.label !== undefined
      ? key.label
      : type === "char"
        ? effectiveValue
        : key.key;
 
  return {
    ...key,
    type,
    effectiveValue,
    displayLabel,
    active: isActiveModifier(key, modifiers),
  };
}
 
/**
 * Resolve an entire {@link KeyboardLayout} against the current modifiers into a
 * {@link ResolvedLayout} ready to render. Pure — the same inputs always produce
 * the same output, which makes it cheap to memoize on `(layout, modifiers)`.
 *
 * @example
 * ```ts
 * const resolved = resolveLayout(qwertyLayout, { shift: false, capsLock: true, layer: false });
 * resolved.rows[0][0].effectiveValue; // "Q"
 * ```
 */
export function resolveLayout(
  layout: KeyboardLayout,
  modifiers: KeyboardModifiers
): ResolvedLayout {
  return {
    name: layout.name,
    direction: layout.direction ?? "ltr",
    rows: layout.rows.map((row) => row.map((key) => resolveKey(key, modifiers))),
  };
}
 
/** Build the SSR-safe synthetic event describing a resolved key press. */
export function buildKeyEvent(
  key: ResolvedKey,
  modifiers: KeyboardModifiers
): VirtualKeyEvent {
  return {
    key: key.effectiveValue,
    code: key.code,
    shiftKey: modifiers.shift,
    layer: modifiers.layer,
  };
}