События клавиатуры: key и code

ввод≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Клавиатура сообщает сразу две вещи: key — какой символ получился, code — какую физическую клавишу придавил палец. Из-за путаницы между ними горячие клавиши молча ломаются, стоит пользователю переключиться на русскую раскладку.

Разделы темы:·

Каждое нажатие даёт два события: keydown — клавиша пошла вниз, keyup — вернулась. Между ними браузер делает своё дело: вставляет символ в поле, прокручивает страницу пробелом, отправляет форму по Enter. Отсюда правило: отменять поведение по умолчанию можно только в keydown — в keyup отменять уже нечего, всё случилось.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Оба события всплывают, поэтому глобальные хоткеи вешают один раз на document, а не на каждое поле. В target лежит элемент, который был в фокусе; если фокуса нет ни на чём, там окажется body.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В обработчике keydown поле хранит ещё старый текст: символ попадёт в input.value только после того, как браузер доведёт событие до конца. Нужен новый текст — слушайте input.

key — это что получилось: буква с учётом регистра и раскладки либо имя служебной клавиши ("Enter", "Escape", "ArrowLeft", а у пробела — просто " "). code — что нажали: неизменная метка позиции клавиши на клавиатуре ("KeyS", "Digit1", "Space", "ShiftLeft"). Одна клавиша, две разные истории.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
Клавишаkeykey с Shiftkey на кириллицеcode
Буква SsSыKeyS
Цифра 11!1Digit1
Пробел" "" "" "Space
Стрелка влевоArrowLeftArrowLeftArrowLeftArrowLeft
Левый ShiftShiftShiftShiftShiftLeft

Правило простое: «что человек ввёл» — key; «на какую клавишу нажал» — code. Управление в игре на WASD только через code, иначе на кириллице герой встанет. А реакция на Escape — через key: это имя одинаково в любой раскладке.

code описывает позицию клавиши на условной US-раскладке железа. На французской AZERTY клавиша в позиции "KeyQ" подписана буквой A, так что «Ctrl+Z» по коду окажется совсем не там, где ждёт француз. Честный ответ — дать пользователю переназначить сочетания; в Chromium есть ещё navigator.keyboard.getLayoutMap(), которая возвращает реальные подписи клавиш.

Четыре булевых свойства события — ctrlKey, altKey, shiftKey, metaKey (Cmd на маке, Win на Windows) — говорят, что было зажато в момент нажатия. Сами модификаторы приходят и отдельными событиями: у Shift key равен "Shift", а code — "ShiftLeft" или "ShiftRight". Состояние залипающих клавиш спрашивают отдельно: e.getModifierState("CapsLock").

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Один и тот же жест «сохранить» — это Ctrl+S на Windows и Linux и Cmd+S на маке, поэтому в проверке почти всегда стоит e.ctrlKey || e.metaKey.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Если клавишу держать, операционная система включает автоповтор: keydown прилетает снова и снова, а keyup — ровно один раз, когда палец отпустил. У повторных событий свойство repeat равно true, у самого первого — false.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Для стрелок в списке повтор — благо: человек держит клавишу и едет вниз. Для действий вроде «отправить» или «открыть окно» — беда, поэтому такие обработчики начинают с if (e.repeat) return;.

Классическая ошибка — проверка e.key === "b" && e.ctrlKey. На русской раскладке та же клавиша даёт "и", и хоткей перестаёт работать ровно тогда, когда человек пишет по-русски, то есть в самый нужный момент.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Сочетание удобно собирать в строку-ключ и искать её в таблице: весь набор хоткеев виден целиком и легко расширяется. И не забывайте preventDefault() — у браузера свои планы на Ctrl+S, Ctrl+P и Ctrl+F.

Событие keypress объявлено устаревшим: оно не приходит на служебные клавиши и по-разному ведёт себя в браузерах. Свойства keyCode, charCode и which устарели следом: их значение зависит и от браузера, и от события — в keydown это код клавиши, в keypress код символа, а по стрелке keypress не приходит вовсе. Сегодня клавиши ловят через keydown и keyup, а вводимый текст — через beforeinput и input, которые честно работают и со вставкой мышью, и с экранной клавиатурой телефона.

  • keydown приходит до того, как браузер отработал нажатие, keyup — после; отменять поведение по умолчанию можно только в keydown.
  • key — символ или имя служебной клавиши, зависит от раскладки и Shift; code — метка физической клавиши, не зависит ни от того, ни от другого.
  • Модификаторы читаются флагами ctrlKey, altKey, shiftKey, metaKey; кроссплатформенная проверка — ctrlKey || metaKey.
  • Зажатая клавиша сыплет keydown с repeat: true, а keyup приходит один раз; одноразовые действия начинают с if (e.repeat) return.
  • Хоткеи с латинскими буквами вешают на code, реакцию на Enter и Escape — на key, а введённый текст ловят событием input.

1/3Пользователь переключился на русскую раскладку и нажал клавишу, подписанную латинской S. Что окажется в e.code?

2/3Стрелку вниз зажали и держат секунду. Что увидит обработчик?

3/3Какая проверка для «жирного шрифта» по Ctrl+B переживёт переключение раскладки?

Escape очищает, Enter отправляет

Повесьте на поле обработчик keydown: по Enter текущее значение попадает в массив sent, по Escape поле очищается. Остальные клавиши обработчик игнорирует.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Здесь работает именно key: имена "Enter" и "Escape" одинаковы в любой раскладке, а вот code у Enter на цифровом блоке был бы "NumpadEnter" — и клавиша перестала бы работать.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Enter приходит первым, поэтому в sent попадает ещё не очищенное значение, а после Escape поле остаётся пустым.

Хоткей Ctrl/Cmd+K без ложных срабатываний

Сделайте на document обработчик палитры команд: Ctrl+K или Cmd+K увеличивают opened.count. Автоповтор считать не нужно, одиночная K без модификатора — тоже, и русская раскладка не должна ломать сочетание.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Три отсечки подряд, каждая отвечает за свою беду: автоповтор, чужая клавиша, отсутствие модификатора.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка по code делает хоткей независимым от раскладки: key в примере равен "л", но клавиша всё та же.