mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 22:54:36 +00:00
Add modal URL router (#modal=name&tab=key) (#3924)
## Description Adds a modal URL router so modals can be opened, deep-linked, and bookmarked via the hash. URLs of the form `#modal=<name>&tab=<key>&...` open the named modal and pass remaining keys as args to `onOpen`. The reverse direction also syncs: opening a modal via the UI updates the URL, closing it clears the hash, and switching tabs updates `&tab=`. Builds on the BaseModal refactor from #3923. ### What's new **`ModalRouter.ts`** — small registry + two-way sync helper. - `register(name, { tag, pageId? })` declares a modal as router-managed - `routeFromHash()` parses `#modal=...` and dispatches to `modal.open(args)` - `syncOpened/syncClosed/syncTab` push state back into the URL via `history.replaceState` (no history entries) - A `routingFromUrl` flag prevents URL→modal→URL feedback loops - Unknown modal names silently strip the hash **`BaseModal`** — opt-in URL sync via a `routerName` property. - When set, BaseModal calls into `modalRouter.syncOpened/syncClosed/syncTab` from `open` / `close` / `setActiveTab` - Modals that own their own URL state (lobby modals) just leave `routerName` undefined **`Main.ts`** — registers all routable modals and wires the router. - `handleUrl()`: adds a `modalRouter.routeFromHash()` branch after the path-based lobby join - `onHashUpdate`: when the hash is router-managed, routes via the router instead of tearing down lobby state ### Routable modals 13 inline modals: store, settings, leaderboard, clan, account, help, news, language, single-player, ranked, troubleshooting, territory-patterns, flag-input. Excluded by design: join-lobby, host-lobby (own URL state via `/game/<id>`), matchmaking (no URL state). ### Example uses - Deep link to store flags tab: `/#modal=store&tab=flags` - Settings keybinds tab: `/#modal=settings&tab=keybinds` - Cosmetics.ts now redirects to `#modal=store&tab=packs` when a hard-currency purchase fails for insufficient plutonium (after the alert), so users can top up directly ### URL behavior - `replaceState` everywhere — no history entries added when modals open / close / switch tabs - Browser back/forward still works for the existing path-based game flow - `hashchange` events are router-aware so external hash changes (back button, manual edit) correctly switch between routed modals without tearing down lobby state ## Please complete the following: - [x] I have added screenshots for all UI updates _(no visual changes; smoke-tested in dev)_ - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file _(no new user-visible strings)_ - [ ] I have added relevant tests to the test directory _(no test coverage; manually tested URL load, UI open, tab switch, close, hashchange, insufficient-plutonium redirect)_ - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { property, query, state } from "lit/decorators.js";
|
||||
import { modalRouter } from "../ModalRouter";
|
||||
import "./baseComponents/Modal";
|
||||
import type { OModalTab } from "./baseComponents/Modal";
|
||||
|
||||
@@ -57,6 +58,13 @@ export abstract class BaseModal extends LitElement {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional router name. When set, BaseModal syncs URL state on open/close/
|
||||
* tab change as `#modal=<routerName>&tab=<key>&...`. Modals that own their
|
||||
* own URL state (e.g. lobby modals) should leave this undefined.
|
||||
*/
|
||||
protected routerName?: string;
|
||||
|
||||
/** Render slot="header" content. Default: no header slot. */
|
||||
protected renderHeaderSlot(): TemplateResult | null {
|
||||
return null;
|
||||
@@ -149,19 +157,24 @@ export abstract class BaseModal extends LitElement {
|
||||
if (tabs.length && this.activeTab === "") {
|
||||
this.activeTab = tabs[0].key;
|
||||
}
|
||||
if (
|
||||
typeof args?.tab === "string" &&
|
||||
tabs.some((t) => t.key === args.tab)
|
||||
) {
|
||||
this.activeTab = args.tab;
|
||||
}
|
||||
const requestedTab =
|
||||
typeof args?.tab === "string" && tabs.some((t) => t.key === args.tab)
|
||||
? args.tab
|
||||
: null;
|
||||
|
||||
const wasOpen = this.isModalOpen;
|
||||
if (!wasOpen) {
|
||||
if (requestedTab) this.activeTab = requestedTab;
|
||||
this.registerEscapeHandler();
|
||||
this.onOpen(args);
|
||||
if (this.activeTab) this.onTabEnter(this.activeTab);
|
||||
} else {
|
||||
this.onOpen(args);
|
||||
// Already open: route tab changes through setActiveTab so URL syncs.
|
||||
if (requestedTab && requestedTab !== this.activeTab) {
|
||||
this.setActiveTab(requestedTab);
|
||||
}
|
||||
}
|
||||
this.onOpen(args);
|
||||
if (this.activeTab) this.onTabEnter(this.activeTab);
|
||||
|
||||
if (wasOpen) return;
|
||||
|
||||
@@ -176,6 +189,10 @@ export abstract class BaseModal extends LitElement {
|
||||
} else {
|
||||
this.modalEl?.open();
|
||||
}
|
||||
|
||||
if (this.routerName) {
|
||||
modalRouter.syncOpened(this.routerName, args);
|
||||
}
|
||||
} finally {
|
||||
this.opening = false;
|
||||
}
|
||||
@@ -193,6 +210,10 @@ export abstract class BaseModal extends LitElement {
|
||||
} else {
|
||||
this.modalEl?.close();
|
||||
}
|
||||
|
||||
if (this.routerName) {
|
||||
modalRouter.syncClosed(this.routerName);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Tab management ----
|
||||
@@ -204,6 +225,9 @@ export abstract class BaseModal extends LitElement {
|
||||
if (this.activeTab === key) return;
|
||||
this.activeTab = key;
|
||||
this.onTabEnter(key);
|
||||
if (this.routerName) {
|
||||
modalRouter.syncTab(this.routerName, key);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Internals ----
|
||||
|
||||
@@ -9,6 +9,8 @@ import { modalHeader } from "./ui/ModalHeader";
|
||||
|
||||
@customElement("ranked-modal")
|
||||
export class RankedModal extends BaseModal {
|
||||
protected routerName = "ranked";
|
||||
|
||||
@state() private elo: number | string = "...";
|
||||
@state() private userMeResponse: UserMeResponse | false = false;
|
||||
@state() private errorMessage: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user