From a1bc37ed90fa6eb6d878851853041d4d9c1ada21 Mon Sep 17 00:00:00 2001 From: sganggs Date: Thu, 8 Oct 2026 21:20:27 +0800 Subject: [PATCH] =?UTF-8?q?feedback7=20R22-LATE:=20while=20a=20facing=20is?= =?UTF-8?q?=20chosen,=20Enter=20follows=20the=20focus=20=E2=80=94=20?= =?UTF-8?q?=E2=9C=95=20cancels,=20any=20other=20button=20does=20nothing,?= =?UTF-8?q?=20the=20wheel=20confirms;=20golden:=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub #394 / PR #395 by @Xinglan233. The wheel's capture listener committed the previewed direction on Enter whatever had the focus, so Tab to 准备 / 设置 / the exit button / ✕ and Enter deployed (the PR let only a focused ✕ keep its native Enter). Now gameLogic facingEnter decides: Enter on ✕ cancels; on any other control (button, link, form field, ARIA control) it is swallowed — the wheel already holds Space and the shortcuts; elsewhere it commits the previewed direction, and does nothing while none is previewed. The wheel (role="dialog") takes the focus when it opens (tabIndex -1, no focus ring over the screen), so a button focused before the drop cannot swallow the confirming Enter and the first Tab reaches ✕; nothing restores the old focus when it closes (a restored 准备 would take the next key). Docs: PLAYING.md's key paragraph. Tests: test/ui/feedback5-hotkeys.test.js (facingEnter and its wiring, 2 new); test/ui/facing.e2e.test.js (SP_E2E, the PR's drag / Tab flow, 4 cases: run here in headless Chrome, 4/4 pass, 4/4 fail on the parent — Enter on the focused exit button deployed). Golden unchanged (client only). --- docs/PLAYING.md | 2 +- public/css/screens/game-panels.css | 2 + public/js/ui/facingWheel.js | 23 +++- public/js/ui/gameLogic.js | 2 +- public/js/ui/gameLogic/shortcuts.js | 25 ++++- test/ui/facing.e2e.test.js | 157 ++++++++++++++++++++++++++++ test/ui/feedback5-hotkeys.test.js | 55 +++++++++- 7 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 test/ui/facing.e2e.test.js diff --git a/docs/PLAYING.md b/docs/PLAYING.md index c35229e5..4ed62f68 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -171,7 +171,7 @@ | `Space` | 准备就绪 / 取消准备;独立模拟的作战中暂停 / 继续 | | `Esc` | 取消 / 关闭:依次关闭表情面板、敌人预览、盟约浮层、详情卡、本局信息 / 敌方情报(固定,不能修改) | -刷新、冻结、升级、撤退、出售和准备只在休整期有效,撤退 / 出售要先点选一名干员。方向轮盘(方向键预览、`Enter` 确认、`Esc` 取消)、「玩法说明」和「干员调配」的翻页键是这些界面自己的按键,不能修改。 +刷新、冻结、升级、撤退、出售和准备只在休整期有效,撤退 / 出售要先点选一名干员。方向轮盘(方向键预览、`Enter` 确认、`Esc` 取消;按 Tab 先到轮盘上的「点击取消」,在它上面按 `Enter` 是取消,Tab 到其他按钮上按 `Enter` 没有反应)、「玩法说明」和「干员调配」的翻页键是这些界面自己的按键,不能修改。 **修改快捷键**:标题画面的「设置」按钮、大厅和房间顶栏的「设置」(玩法说明左边)或对局左下角的「设置」→「快捷键」。点一下要改的按键(只用键盘时按 Tab 移到它上面再按 Enter),出现「按下新按键…」后按新的键;按 `Esc` 取消,设置窗口不会关闭。可以用字母、数字、空格、标点符号键和 Delete / Insert / Home / End / PageUp / PageDown;`Esc`、`Tab`、`Enter`、方向键和 F1–F12 留给界面使用,也不能搭配 Ctrl / Alt / ⌘。新按键已被另一个快捷键使用时两者互换,列表下方会写明(例如把「撤退选中干员」改成 `X`,「出售选中干员」就换成原来的 `Q`)。「恢复默认」改回上表的按键。修改立即生效,商店栏、准备按钮和单位底部「撤退 / 出售」按钮上显示的按键也跟着变;设置和音量等一起保存在当前浏览器里(换浏览器或清除网站数据后回到默认)。按键按键帽上印的字符识别(AZERTY 等键盘按印着的字母),俄文等非拉丁布局按键位识别。手机等纯触屏设备要连接键盘才能用快捷键,设置里的快捷键列表默认折叠。 diff --git a/public/css/screens/game-panels.css b/public/css/screens/game-panels.css index 1cda7adb..94773851 100644 --- a/public/css/screens/game-panels.css +++ b/public/css/screens/game-panels.css @@ -515,6 +515,8 @@ button.bpop__harmony:hover { border-style: solid; color: #fff; } /* ---- direction wheel (ui/facingWheel.js, research 09 §1.2): white diamond, 4 chevrons, "✕ 点击取消", orange range ---- */ .fwheel { position: fixed; inset: 0; z-index: 6; touch-action: none; user-select: none; -webkit-user-select: none; cursor: crosshair; } +/* the wheel takes the keyboard focus when it opens (Enter confirms there, GitHub #394): no ring around the whole screen */ +.fwheel:focus-visible { outline: none; } .fwheel__stripes { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; } .fwheel__dia { position: absolute; transform: translate(-50%, -50%); pointer-events: none; animation: fwheel-in 160ms var(--ease-out); } @keyframes fwheel-in { from { opacity: 0; transform: translate(-50%, -50%) scale(.82); } } diff --git a/public/js/ui/facingWheel.js b/public/js/ui/facingWheel.js index a4987ccf..c688a1d8 100644 --- a/public/js/ui/facingWheel.js +++ b/public/js/ui/facingWheel.js @@ -13,7 +13,8 @@ // LEFT, facing.js boardDir): the chevrons follow the finger, the range / model / intent use the board direction; // * release outside the centre commits (g.move {uid, to, dir} / g.art {…, dir}); release inside the centre, a tap // on ✕, a press outside the diamond or Esc cancels (the piece returns to where it came from). Arrow keys preview a -// direction and Enter commits it (keyboard alternative). Mouse, pen and touch all use pointer events. +// direction and Enter commits it (keyboard alternative) — Enter on a focused ✕ cancels, on any other focused +// button does nothing while the wheel is open (GitHub #394). Mouse, pen and touch all use pointer events. // // View bridge: the field view (render/app.js or the DOM fallback) may implement `tileScreen(row, col)`, // `holdPiece(uid, tile|null)` and `setPieceDir(uid, dir)` (render/app.js does, DESIGN §9); a view without them falls @@ -25,7 +26,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from '../../vendor/hooks import { html } from './components.js'; import { LocalSprite } from './gameComponents.js'; import { DIRS, DIR_LABEL, DEAD_ZONE_TILES, dirFromDelta, dirFromKey, rangeTiles, normDir, boardDir, viewMirrored } from './facing.js'; -import { facingSwallows } from './gameLogic.js'; +import { facingSwallows, facingEnter } from './gameLogic.js'; import { settingsStore } from './settings.js'; import { t } from '../../../shared/i18n.js'; @@ -236,24 +237,36 @@ export function FacingWheel({ view, row, col, grid, name = '', onPreview, onComm }; const onPointerCancel = () => { setDrag(null); setDir(null); }; - // keyboard: arrows preview, Enter commits, Esc cancels (capture: the game's own shortcuts must not see them) + // keyboard: arrows preview, Enter commits, Esc cancels (capture: the game's own shortcuts must not see them). Enter + // follows the focus (gameLogic facingEnter, GitHub #394): on ✕ it cancels, on any other button (Tab reaches the HUD + // behind the wheel) it does nothing, elsewhere it commits the previewed direction. The wheel takes the focus when it + // opens, so a button focused before the drop (a click leaves it focused) cannot swallow the confirming Enter; Tab then + // reaches ✕ first. Nothing gives the focus back when it closes (a restored 准备 would take the next Enter / Space). + const rootRef = useRef(null); useEffect(() => { const onKey = (e) => { const L = live.current; if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); L.onCancel(); return; } const k = dirFromKey(e.key); if (k) { e.preventDefault(); e.stopImmediatePropagation(); setDir(k); return; } - if (e.key === 'Enter' && L.dir) { e.preventDefault(); e.stopImmediatePropagation(); L.onCommit(L.bdir); return; } + const enter = facingEnter(e, !!L.dir); + if (enter) { + e.preventDefault(); e.stopImmediatePropagation(); + if (enter === 'cancel') L.onCancel(); + else if (enter === 'commit') L.onCommit(L.bdir); + return; + } // no ready / shop while choosing: Space and every key of the player's map (设置 → 快捷键) if (facingSwallows(e, settingsStore.get().keys)) { e.preventDefault(); e.stopImmediatePropagation(); } }; window.addEventListener('keydown', onKey, true); + try { rootRef.current?.focus({ preventScroll: true }); } catch { /* no focus: Enter still follows whatever has it */ } return () => window.removeEventListener('keydown', onKey, true); }, []); const outside = !!drag && !!dir; const box = half * 2; - return html`