feedback7 R22-LATE: while a facing is chosen, Enter follows the focus — ✕ cancels, any other button does nothing, the wheel confirms; golden: unchanged
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).
This commit is contained in:
1 parent
2df1cf15bf
commit
a1bc37ed90
7 files changed
+256
-10
No files matched your search
@@ -11,7 +11,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
HOTKEY_ACTIONS, DEFAULT_HOTKEYS, isBindableCode, hotkeyLabel, sanitizeHotkeys, rebindHotkey, isDefaultHotkeys, hotkeyOf,
|
||||
actionForKey, captureHotkey, facingSwallows, shortcutFor, shortcutBlocked, sanitizeSettings, DEFAULT_SETTINGS,
|
||||
actionForKey, captureHotkey, facingSwallows, facingEnter, shortcutFor, shortcutBlocked, sanitizeSettings, DEFAULT_SETTINGS,
|
||||
} from '../../public/js/ui/gameLogic.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
@@ -201,6 +201,59 @@ describe('the one lookup (actionForKey) and shortcutFor', () => {
|
||||
assert.equal(facingSwallows({ key: ' ', code: 'Space' }, { ...DEFAULT_HOTKEYS, ready: 'KeyE' }), true, 'Space always: a focused button must not activate');
|
||||
assert.equal(facingSwallows({ key: 'z', code: 'KeyZ' }), false);
|
||||
});
|
||||
|
||||
// GitHub #394 / PR #395: Enter followed the previewed direction whatever had the focus — Tab to 准备 / 设置 / ✕ and
|
||||
// Enter deployed. A tiny element tree whose closest() understands the selector forms facingEnter uses.
|
||||
const el = (tag, attrs = {}, parent = null) => {
|
||||
const self = {
|
||||
tagName: tag.toUpperCase(), attrs, parent,
|
||||
matches(sel) {
|
||||
return sel.split(',').map((s) => s.trim()).some((s) => {
|
||||
const m = /^([a-z]*)((?:\.[\w-]+)*)((?:\[[\w-]+(?:="[^"]*")?\])*)$/.exec(s);
|
||||
if (!m) return false;
|
||||
if (m[1] && m[1].toUpperCase() !== self.tagName) return false;
|
||||
const classes = String(attrs.class || '').split(/\s+/);
|
||||
if (m[2] && !m[2].slice(1).split('.').every((c) => classes.includes(c))) return false;
|
||||
for (const [, k, v] of m[3].matchAll(/\[([\w-]+)(?:="([^"]*)")?\]/g)) {
|
||||
if (!(k in attrs) || (v !== undefined && String(attrs[k]) !== v)) return false;
|
||||
}
|
||||
return !!(m[1] || m[2] || m[3]);
|
||||
});
|
||||
},
|
||||
closest(sel) { for (let n = self; n; n = n.parent) if (n.matches(sel)) return n; return null; },
|
||||
};
|
||||
return self;
|
||||
};
|
||||
test('the facing wheel\'s Enter follows the focus: ✕ cancels, another control does nothing, elsewhere it commits', () => {
|
||||
const body = el('body');
|
||||
const wheel = el('div', { class: 'fwheel', role: 'dialog', tabindex: '-1' }, body);
|
||||
const cancel = el('button', { class: 'fwheel__cancel' }, el('div', { class: 'fwheel__dia' }, wheel));
|
||||
const cancelText = el('span', {}, cancel);
|
||||
const ready = el('button', { class: 'ready' }, el('header', {}, body));
|
||||
const gear = el('button', { class: 'gm__gear' }, body);
|
||||
const card = el('div', { role: 'button', tabindex: '0' }, body);
|
||||
const link = el('a', { href: '#' }, body);
|
||||
const field = el('input', { type: 'text' }, body);
|
||||
for (const chosen of [true, false]) {
|
||||
assert.equal(facingEnter({ key: 'Enter', target: cancel }, chosen), 'cancel', 'Enter on ✕ cancels (a direction previewed or not)');
|
||||
assert.equal(facingEnter({ key: 'Enter', target: cancelText }, chosen), 'cancel');
|
||||
for (const t of [ready, gear, card, link, field]) assert.equal(facingEnter({ key: 'Enter', target: t }, chosen), 'swallow', 'another control does nothing');
|
||||
}
|
||||
assert.equal(facingEnter({ key: 'Enter', target: wheel }, true), 'commit', 'the wheel has the focus when it opens: Enter confirms');
|
||||
assert.equal(facingEnter({ key: 'Enter', target: body }, true), 'commit');
|
||||
assert.equal(facingEnter({ key: 'Enter', target: null }, true), 'commit');
|
||||
assert.equal(facingEnter({ key: 'Enter', target: wheel }, false), 'swallow', 'nothing to confirm yet');
|
||||
assert.equal(facingEnter({ key: ' ', target: cancel }, true), null, 'not Enter: Space stays with facingSwallows');
|
||||
assert.equal(facingEnter({ key: 'ArrowUp', target: body }, true), null);
|
||||
assert.equal(facingEnter(null, true), null);
|
||||
});
|
||||
test('the wheel wires it: Enter through facingEnter, the wheel focusable and focused when it opens', () => {
|
||||
const src = read('public/js/ui/facingWheel.js');
|
||||
assert.match(src, /const enter = facingEnter\(e, !!L\.dir\);/);
|
||||
assert.doesNotMatch(src, /e\.key === 'Enter' && L\.dir/, 'no Enter that ignores the focus');
|
||||
assert.match(src, /<div ref=\$\{rootRef\} tabIndex="-1" class=\$\{cx\('fwheel'/);
|
||||
assert.match(src, /rootRef\.current\?\.focus\(\{ preventScroll: true \}\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('capturing a new key (the settings wait for a key)', () => {
|
||||
|
||||
Reference in new issue
Block a user