# 键盘锁定与指针锁定

> 为沉浸式界面捕获指定物理按键与相对指针移动，并说明用户激活检查、解锁路径和功能不可用时的回退方案。

Keyboard Lock 让页面捕获操作系统允许的物理按键，Pointer Lock 则隐藏光标并向一个元素提供相对指针移动；两者面向游戏、远程访问应用等需要集中输入的界面。

MDN 把 `Keyboard.lock()` 和返回 Promise 的 `requestPointerLock()` 都标为有限可用。

Keyboard Lock 还仅限安全上下文，因此界面需要分别检测两项能力，并保留不锁定输入时仍可用的路径。

## 支持情况

MDN 把两项 API 都标为有限可用。Keyboard Lock 要求安全上下文和由 JavaScript 发起的全屏；Pointer Lock 的支持情况及返回 Promise 的请求形式因浏览器而异。

## 语法

```js
await navigator.keyboard.lock(["KeyW", "KeyA", "KeyS", "KeyD"]);
navigator.keyboard.unlock();

await element.requestPointerLock();
document.exitPointerLock();
```

不传列表调用 `keyboard.lock()`，会请求操作系统允许的全部有效按键代码。后一次调用会替换先前的按键集合。Keyboard Lock 规范在文档处于全屏状态时分派锁定的按键。Pointer Lock 会让 `clientX`、`clientY`、`screenX` 和 `screenY` 保持不变，而 `movementX` 与 `movementY` 继续报告相对移动。

## 参数

`keyboard.lock(keyCodes)` 接受可选的 `KeyboardEvent.code` 值序列。`element.requestPointerLock(options)` 接受可选的 `unadjustedMovement` 布尔值；该值为 `true` 时，Pointer Lock 规范请求不经过操作系统鼠标加速的移动数据。

## 异常

Keyboard Lock 的 `lock()` 算法要求代码运行在当前活动的顶层浏览上下文中，但不检查瞬时用户激活。Chrome 文档要求先进入由 JavaScript 发起的全屏。在活动顶层浏览上下文之外调用时，Promise 以 `InvalidStateError` 拒绝；代码无效时以 `InvalidAccessError` 拒绝；后一个锁定请求取代尚未完成的请求时，以 `AbortError` 拒绝。

Pointer Lock 要求瞬时激活，但文档此前成功锁定指针并用 `exitPointerLock()` 释放时例外。沙盒 iframe 需要 `allow-pointer-lock` token；按照 Pointer Lock 算法，请求平台不支持的未调整移动时会以 `NotSupportedError` 拒绝。

:::caution
Keyboard Lock 规范把 `Press ESC to exit fullscreen` 和 `Press and hold ESC to exit fullscreen` 列为用户代理可能显示的消息示例。规范说明锁定 Escape 后，用户代理可能需要调整界面，但未要求显示这两句原文（[Keyboard Lock](https://wicg.github.io/keyboard-lock/)，WICG）。
:::

## 示例

以下示例等 Pointer Lock 确认成功、Keyboard Lock Promise 兑现后才报告控制已锁定。任一请求失败时，屏幕控件仍可使用。

### 锁定控制，不支持时保留屏幕控件

第一次点击进入全屏，第二次点击请求 Pointer Lock。`pointerlockchange` 确认锁定后，页面才请求 Keyboard Lock。任一 API 缺失时，界面保留普通键盘和指针处理，并提示集中控制不可用。

```html
<section id="control-surface" tabindex="0" aria-label="交互控制区域">
  <p id="lock-status" role="status">可使用屏幕按钮，或点击此区域开始。</p>
  <output id="position">x: 0, y: 0</output>
  <div id="screen-controls" aria-label="屏幕控件">
    <button type="button" data-dx="0" data-dy="-1">上</button>
    <button type="button" data-dx="-1" data-dy="0">左</button>
    <button type="button" data-dx="1" data-dy="0">右</button>
    <button type="button" data-dx="0" data-dy="1">下</button>
  </div>
</section>

<script type="module">
const surface = document.querySelector("#control-surface");
const status = document.querySelector("#lock-status");
const position = document.querySelector("#position");
const controls = document.querySelector("#screen-controls");
let x = 0;
let y = 0;

function move(dx, dy) {
  x += dx;
  y += dy;
  position.value = `x: ${x}, y: ${y}`;
}

controls.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-dx]");
  if (!button) return;
  move(Number(button.dataset.dx), Number(button.dataset.dy));
});

surface.addEventListener("click", async (event) => {
  if (event.target.closest("#screen-controls")) return;
  const canLockKeyboard = "keyboard" in navigator &&
    "lock" in navigator.keyboard;
  const canLockPointer = "requestPointerLock" in surface;
  const canFullscreen = "requestFullscreen" in surface;

  if (!canLockKeyboard || !canLockPointer || !canFullscreen) {
    status.textContent = "请使用屏幕控件。";
    return;
  }

  if (document.fullscreenElement !== surface) {
    try {
      await surface.requestFullscreen();
      status.textContent = "请再次点击控制区域以锁定输入。";
    } catch (error) {
      status.textContent = `未进入全屏：${error.name}`;
    }
    return;
  }

  const request = surface.requestPointerLock();
  request?.catch((error) => {
    status.textContent = `Pointer Lock 失败：${error.name}`;
  });
});

document.addEventListener("pointerlockchange", async () => {
  if (document.pointerLockElement !== surface) {
    navigator.keyboard?.unlock?.();
    status.textContent = "控制已解锁。";
    return;
  }

  try {
    await navigator.keyboard.lock(["KeyW", "KeyA", "KeyS", "KeyD"]);
    status.textContent = "控制已锁定。按 Escape 退出。";
  } catch (error) {
    document.exitPointerLock();
    status.textContent = `Keyboard Lock 失败：${error.name}`;
  }
});

document.addEventListener("pointerlockerror", () => {
  status.textContent = "Pointer Lock 失败，请使用屏幕控件。";
});
document.addEventListener("mousemove", (event) => {
  if (document.pointerLockElement !== surface) return;
  move(event.movementX, event.movementY);
});
</script>
```

事件处理器等 Pointer Lock 确认成功后才报告结果。Keyboard Lock 失败时会释放 Pointer Lock，Pointer Lock 结束时会释放 Keyboard Lock；缺少支持或请求失败时，已经定义的屏幕控件仍可使用。相对指针移动与按钮操作会更新同一位置输出。

### 保留屏幕控件

示例从不隐藏 `#screen-controls`。按钮处理器和相对指针移动都调用 `move()`，因此锁定开始前或请求失败后仍可使用这些控件。

## 另请参阅

- [用户激活](/zh/reference/capabilities/user-activation/)，了解 Pointer Lock 请求使用的激活状态
- [Screen Capture API](/zh/reference/capabilities/screen-capture/)，用于在远程访问界面中共享显示内容
- [Keyboard Lock](https://wicg.github.io/keyboard-lock/)（wicg.github.io）
- [Pointer Lock 2.0](https://w3c.github.io/pointerlock/)（w3c.github.io）