diff --git a/public/js/screens/diy.js b/public/js/screens/diy.js index 97542f94..489cf3f4 100644 --- a/public/js/screens/diy.js +++ b/public/js/screens/diy.js @@ -9,7 +9,7 @@ // the model is ui/diyModel.js. Styles: css/screens/loadout.css (diy-*). An owned pick's card carries its operator's 潜能 / // 练度 selects (0.2.2, screens/cultivation.js — the same per-operator settings as 干员调配, `ops`); a prototype has neither. -import { useEffect, useState } from '../../vendor/hooks.module.js'; +import { useLayoutEffect, useState } from '../../vendor/hooks.module.js'; import { html, Icon, Button, TierChip } from '../ui/components.js'; import { Img, RichText, BondGlyph } from '../ui/gameComponents.js'; import { chessAvatarUrl, chessPortraitUrl, profIconUrl, skillRecordIconUrl, moduleTypeIconUrl } from '../ui/assetUrls.js'; @@ -146,8 +146,10 @@ export function DiyPicker(props) { const [query, setQuery] = useState(''); const [draft, setDraft] = useState(cur ? { ...cur } : null); // Esc cancels only the picker, like its 取消 (GitHub #284, idea from PR #286); the 干员调配 overlay's own Esc skips - // while a picker is open, and a dialog over the picker (导入) still takes Esc first - useEffect(() => { + // while a picker is open, and a dialog over the picker (导入) still takes Esc first. A layout effect: the listener is + // attached in the same commit as the picker (a plain effect waits for the next frame, and an Esc pressed in between + // was swallowed — the overlay skipped it, the picker did not hear it yet; the 0.2.2 full browser pass) + useLayoutEffect(() => { const onKey = (e) => { if (e.key !== 'Escape' || e.ctrlKey || e.metaKey || e.altKey || document.querySelector('.modal')) return; e.preventDefault(); diff --git a/test/ui/diy.e2e.test.js b/test/ui/diy.e2e.test.js index 00127367..da026989 100644 --- a/test/ui/diy.e2e.test.js +++ b/test/ui/diy.e2e.test.js @@ -134,6 +134,19 @@ describe('自选编队 picker: Esc closes only the picker (real server, no art n assert.ok(await c.page.$('.lo'), `${slot}: Esc keeps the 干员调配 overlay${search ? ' (from the search field)' : ''}`); assert.equal((await c.page.$$('.diy-slot')).length, 4, `${slot}: the four slots stay`); } + // an Esc pressed the moment the picker appears — before the next animation frame — closes it too: its listener comes + // with the picker's own commit (a plain effect attached it a frame later and the overlay skipped the key: the + // picker stayed open — the 0.2.2 full browser pass) + const opened = await c.page.evaluate(async (slot, sel) => { + document.querySelector(`.diy-slot[data-slot="${slot}"] .diy-slot__fill`).click(); + for (let k = 0; k < 50 && !document.querySelector(sel); k++) await Promise.resolve(); + const open = !!document.querySelector(sel); + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); + return open; + }, SLOT, picker); + assert.ok(opened, 'the picker opened'); + await c.page.waitForSelector(picker, { hidden: true, timeout: 3000 }); + assert.ok(await c.page.$('.lo'), 'an immediate Esc keeps the 干员调配 overlay'); assert.equal(await saved(), before, 'cancelling a picker does not change the saved picks'); await c.page.keyboard.press('Escape'); await c.page.waitForFunction(() => !document.querySelector('.lo'), { timeout: 3000 });