Replace native confirm() with an in-site confirmation dialog

Add App.confirm() in common.js: a promise-based dialog styled like the
site's modals, with a title, multi-line message and custom button labels.
Destructive actions use a red OK button and focus Cancel by default;
Escape and backdrop clicks cancel, and Tab stays inside the dialog.

Use it for folding when a check is possible, leaving the table,
confirming showdown winners and deleting tables from the lobby. After the
dialog closes, re-check that the action is still valid (turn not passed,
pot not already awarded, table still listed) before sending it.

Also add a "poker-test" launch config that runs on port 4672 with the
example keys from .env.example, for local browser testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-20 00:34:43 +09:00
1 parent d89ce1dd30
commit 680aa20d30
5 files changed
+122 -11

No files matched your search

+72 -1
View File
@@ -52,6 +52,77 @@
toastTimer = setTimeout(() => { el.hidden = true; }, 2500);
}
// ブラウザ標準の confirm() の代わりの確認ダイアログ。OK なら true で解決する。
// opts: { title, message(改行可), ok, cancel, danger(OK を赤にする) }
// 新しいダイアログを開くと、開いていたものはキャンセル扱いで閉じる
let closeConfirm = null;
function confirmDialog(opts) {
const o = typeof opts === 'string' ? { message: opts } : opts;
if (closeConfirm) closeConfirm(false);
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop confirm-backdrop';
backdrop.innerHTML =
'<div class="modal confirm-modal" role="alertdialog" aria-modal="true">' +
(o.title ? '<h2 class="confirm-title"></h2>' : '') +
'<p class="confirm-message"></p>' +
'<div class="confirm-buttons">' +
'<button type="button" class="btn btn-dark" data-confirm="cancel"></button>' +
`<button type="button" class="btn ${o.danger ? 'btn-red' : 'btn-orange'}" data-confirm="ok"></button>` +
'</div></div>';
const dialog = backdrop.firstChild;
const msgEl = dialog.querySelector('.confirm-message');
msgEl.textContent = o.message || '';
msgEl.id = 'confirm-message-' + randomHex(4);
dialog.setAttribute('aria-describedby', msgEl.id);
if (o.title) {
const titleEl = dialog.querySelector('.confirm-title');
titleEl.textContent = o.title;
titleEl.id = 'confirm-title-' + randomHex(4);
dialog.setAttribute('aria-labelledby', titleEl.id);
}
const okBtn = dialog.querySelector('[data-confirm="ok"]');
const cancelBtn = dialog.querySelector('[data-confirm="cancel"]');
okBtn.textContent = o.ok || 'OK';
cancelBtn.textContent = o.cancel || 'キャンセル';
const prevFocus = document.activeElement;
return new Promise((resolve) => {
function close(result) {
if (closeConfirm !== close) return;
closeConfirm = null;
document.removeEventListener('keydown', onKey, true);
backdrop.remove();
if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus();
resolve(result);
}
function onKey(e) {
if (e.key === 'Escape') {
e.preventDefault();
close(false);
} else if (e.key === 'Tab') {
// フォーカスをダイアログの2つのボタンの中に閉じ込める
e.preventDefault();
(document.activeElement === okBtn ? cancelBtn : okBtn).focus();
}
}
okBtn.addEventListener('click', () => close(true));
cancelBtn.addEventListener('click', () => close(false));
// 背景を押したらキャンセル(ダイアログ内から背景へのドラッグは除く)
backdrop.addEventListener('pointerdown', (e) => {
backdrop.dataset.downOnBackdrop = e.target === backdrop ? '1' : '';
});
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop && backdrop.dataset.downOnBackdrop) close(false);
});
document.addEventListener('keydown', onKey, true);
closeConfirm = close;
document.body.appendChild(backdrop);
// 危険な操作はうっかり Enter で確定しないようキャンセルにフォーカスする
(o.danger ? cancelBtn : okBtn).focus();
});
}
async function api(method, url, body) {
const res = await fetch(url, {
method,
@@ -96,5 +167,5 @@
FINISHING: 'FINISHING',
};
window.App = { Store, esc, chips, toast, api, connectSocket, safeNext, PHASE_LABELS };
window.App = { Store, esc, chips, toast, confirm: confirmDialog, api, connectSocket, safeNext, PHASE_LABELS };
})();