mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 09:14:09 +00:00
replace leave lobby popup with custom popup (#4449)
## Description: old: <img width="1009" height="491" alt="image" src="https://github.com/user-attachments/assets/0b95877c-dac7-4025-bdfa-62ab6879d208" /> new: <img width="1017" height="561" alt="image" src="https://github.com/user-attachments/assets/cfb49b31-eb46-4d64-bd9e-3f25bb7cd0fb" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n
This commit is contained in:
@@ -531,11 +531,9 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
// Clear clipboard so the host doesn't accidentally share a dead link
|
// Clear clipboard so the host doesn't accidentally share a dead link
|
||||||
void navigator.clipboard.writeText("").catch(() => {});
|
void navigator.clipboard.writeText("").catch(() => {});
|
||||||
});
|
});
|
||||||
if (this.modalEl) {
|
// BaseModal.firstUpdated() owns modalEl.onClose so the o-modal close path
|
||||||
this.modalEl.onClose = () => {
|
// (backdrop / close button) runs confirmBeforeClose(). Don't override it
|
||||||
this.close();
|
// here — doing so would bypass the leave-lobby confirmation.
|
||||||
};
|
|
||||||
}
|
|
||||||
this.loadNationCount();
|
this.loadNationCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,8 +550,8 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public confirmBeforeClose(): boolean {
|
public confirmBeforeClose(): boolean | Promise<boolean> {
|
||||||
return confirm(translateText("host_modal.leave_confirmation"));
|
return this.confirmClose(translateText("host_modal.leave_confirmation"));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onClose(): void {
|
protected onClose(): void {
|
||||||
|
|||||||
@@ -336,9 +336,9 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public confirmBeforeClose(): boolean {
|
public confirmBeforeClose(): boolean | Promise<boolean> {
|
||||||
if (!this.currentLobbyId) return true;
|
if (!this.currentLobbyId) return true;
|
||||||
return confirm(translateText("host_modal.leave_confirmation"));
|
return this.confirmClose(translateText("host_modal.leave_confirmation"));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onClose(): void {
|
protected onClose(): void {
|
||||||
|
|||||||
@@ -79,13 +79,30 @@ export function initNavigation() {
|
|||||||
window.showPage = showPage;
|
window.showPage = showPage;
|
||||||
|
|
||||||
// Use event delegation for navigation items (they may be inside Lit components)
|
// Use event delegation for navigation items (they may be inside Lit components)
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", async (e) => {
|
||||||
const target = (e.target as HTMLElement).closest(
|
const target = (e.target as HTMLElement).closest(
|
||||||
".nav-menu-item[data-page]",
|
".nav-menu-item[data-page]",
|
||||||
);
|
);
|
||||||
if (target) {
|
if (target) {
|
||||||
const pageId = (target as HTMLElement).dataset.page;
|
const pageId = (target as HTMLElement).dataset.page;
|
||||||
if (pageId) showPage(pageId);
|
if (!pageId) return;
|
||||||
|
|
||||||
|
// showPage() closes the currently visible modal, so respect its
|
||||||
|
// close-confirmation guard first (e.g. the leave-lobby prompt).
|
||||||
|
const visibleModal = document.querySelector(
|
||||||
|
".page-content:not(.hidden)",
|
||||||
|
) as any;
|
||||||
|
if (
|
||||||
|
visibleModal &&
|
||||||
|
typeof visibleModal.isOpen === "function" &&
|
||||||
|
visibleModal.isOpen() &&
|
||||||
|
typeof visibleModal.confirmBeforeClose === "function" &&
|
||||||
|
!(await visibleModal.confirmBeforeClose())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showPage(pageId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +112,7 @@ export function initNavigation() {
|
|||||||
const mainEl = document.querySelector("main");
|
const mainEl = document.querySelector("main");
|
||||||
|
|
||||||
if (mainEl) {
|
if (mainEl) {
|
||||||
mainEl.addEventListener("click", (e: Event) => {
|
mainEl.addEventListener("click", async (e: Event) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const isPlayPageHidden = document
|
const isPlayPageHidden = document
|
||||||
.getElementById("page-play")
|
.getElementById("page-play")
|
||||||
@@ -118,7 +135,7 @@ export function initNavigation() {
|
|||||||
// Check confirmation guard before closing
|
// Check confirmation guard before closing
|
||||||
if (
|
if (
|
||||||
typeof openModal.confirmBeforeClose === "function" &&
|
typeof openModal.confirmBeforeClose === "function" &&
|
||||||
!openModal.confirmBeforeClose()
|
!(await openModal.confirmBeforeClose())
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { property, query, state } from "lit/decorators.js";
|
|||||||
import { modalRouter } from "../ModalRouter";
|
import { modalRouter } from "../ModalRouter";
|
||||||
import "./baseComponents/Modal";
|
import "./baseComponents/Modal";
|
||||||
import type { OModalTab } from "./baseComponents/Modal";
|
import type { OModalTab } from "./baseComponents/Modal";
|
||||||
|
import "./ConfirmDialog";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static-ish configuration for the <o-modal> shell.
|
* Static-ish configuration for the <o-modal> shell.
|
||||||
@@ -39,6 +40,12 @@ export abstract class BaseModal extends LitElement {
|
|||||||
@state() protected activeTab = "";
|
@state() protected activeTab = "";
|
||||||
@property({ type: Boolean }) inline = false;
|
@property({ type: Boolean }) inline = false;
|
||||||
|
|
||||||
|
// Pending close confirmation; when set, renders the inline confirm dialog.
|
||||||
|
@state() private closeConfirm: {
|
||||||
|
message: string;
|
||||||
|
resolve: (ok: boolean) => void;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
// Re-entrancy guard: showPage() (for inline modals) re-invokes .open()
|
// Re-entrancy guard: showPage() (for inline modals) re-invokes .open()
|
||||||
// with no args after we call it. We must not re-run onOpen(undefined)
|
// with no args after we call it. We must not re-run onOpen(undefined)
|
||||||
// from that nested call, which would clobber state set by the outer call.
|
// from that nested call, which would clobber state set by the outer call.
|
||||||
@@ -91,12 +98,30 @@ export abstract class BaseModal extends LitElement {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Guard called before closing via Escape key or click-outside.
|
* Guard called before closing via Escape key or click-outside.
|
||||||
* Return false to prevent the modal from closing.
|
* Return false (or a promise resolving false) to prevent closing.
|
||||||
*/
|
*/
|
||||||
public confirmBeforeClose(): boolean {
|
public confirmBeforeClose(): boolean | Promise<boolean> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a styled confirm dialog and resolve to the user's choice. Call from
|
||||||
|
* confirmBeforeClose() to gate closing behind a confirm/cancel prompt.
|
||||||
|
*/
|
||||||
|
protected confirmClose(message: string): Promise<boolean> {
|
||||||
|
// Don't stack a second prompt if one is already open.
|
||||||
|
if (this.closeConfirm) return Promise.resolve(false);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.closeConfirm = { message, resolve };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private settleCloseConfirm(ok: boolean) {
|
||||||
|
const pending = this.closeConfirm;
|
||||||
|
this.closeConfirm = null;
|
||||||
|
pending?.resolve(ok);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Rendering ----
|
// ---- Rendering ----
|
||||||
|
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
@@ -133,6 +158,14 @@ export abstract class BaseModal extends LitElement {
|
|||||||
${headerSlot ? html`<div slot="header">${headerSlot}</div>` : null}
|
${headerSlot ? html`<div slot="header">${headerSlot}</div>` : null}
|
||||||
${body}
|
${body}
|
||||||
</o-modal>
|
</o-modal>
|
||||||
|
${this.closeConfirm
|
||||||
|
? html`<confirm-dialog
|
||||||
|
.message=${this.closeConfirm.message}
|
||||||
|
variant="warning"
|
||||||
|
@confirm=${() => this.settleCloseConfirm(true)}
|
||||||
|
@cancel=${() => this.settleCloseConfirm(false)}
|
||||||
|
></confirm-dialog>`
|
||||||
|
: null}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +232,9 @@ export abstract class BaseModal extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public close(args?: Record<string, unknown>): void {
|
public close(args?: Record<string, unknown>): void {
|
||||||
|
// If closing was triggered elsewhere while a confirm prompt is pending,
|
||||||
|
// dismiss it (removes the dialog and resolves the awaiting guard as false).
|
||||||
|
this.settleCloseConfirm(false);
|
||||||
this.unregisterEscapeHandler();
|
this.unregisterEscapeHandler();
|
||||||
this.onClose(args);
|
this.onClose(args);
|
||||||
|
|
||||||
@@ -234,9 +270,13 @@ export abstract class BaseModal extends LitElement {
|
|||||||
|
|
||||||
protected firstUpdated(): void {
|
protected firstUpdated(): void {
|
||||||
if (this.modalEl) {
|
if (this.modalEl) {
|
||||||
this.modalEl.onClose = () => {
|
this.modalEl.onClose = async () => {
|
||||||
if (this.isModalOpen) {
|
if (this.isModalOpen) {
|
||||||
if (!this.confirmBeforeClose()) {
|
const confirmed = await this.confirmBeforeClose();
|
||||||
|
// Bail if a parallel close() settled things while we awaited —
|
||||||
|
// otherwise we'd re-open an already-closed modal.
|
||||||
|
if (!this.isModalOpen) return;
|
||||||
|
if (!confirmed) {
|
||||||
// Re-open the underlying o-modal since it already closed itself
|
// Re-open the underlying o-modal since it already closed itself
|
||||||
this.modalEl?.open();
|
this.modalEl?.open();
|
||||||
return;
|
return;
|
||||||
@@ -248,14 +288,17 @@ export abstract class BaseModal extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
this.settleCloseConfirm(false);
|
||||||
this.unregisterEscapeHandler();
|
this.unregisterEscapeHandler();
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyDown = (e: KeyboardEvent) => {
|
private handleKeyDown = async (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape" && this.isModalOpen) {
|
if (e.key === "Escape" && this.isModalOpen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.confirmBeforeClose()) {
|
const confirmed = await this.confirmBeforeClose();
|
||||||
|
// Bail if a parallel close() already closed us while we awaited.
|
||||||
|
if (!confirmed || !this.isModalOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ describe("LeaderboardModal", () => {
|
|||||||
expect(modal.tagName.toLowerCase()).toBe("leaderboard-modal");
|
expect(modal.tagName.toLowerCase()).toBe("leaderboard-modal");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should close on Escape when open", () => {
|
it("should close on Escape when open", async () => {
|
||||||
const mockModalEl = { open: vi.fn(), close: vi.fn() };
|
const mockModalEl = { open: vi.fn(), close: vi.fn() };
|
||||||
Object.defineProperty(modal, "modalEl", {
|
Object.defineProperty(modal, "modalEl", {
|
||||||
get: () => mockModalEl,
|
get: () => mockModalEl,
|
||||||
@@ -317,6 +317,9 @@ describe("LeaderboardModal", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
||||||
|
// handleKeyDown awaits confirmBeforeClose() before closing, so the close
|
||||||
|
// is deferred to a later microtask — flush it before asserting.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect((modal as unknown as { isModalOpen: boolean }).isModalOpen).toBe(
|
expect((modal as unknown as { isModalOpen: boolean }).isModalOpen).toBe(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user