mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 17:54:37 +00:00
Refactor & standardize modal tabs (#3864)
## Description: Refactors tab handling out of the individual modal components and into the base o-modal component. Tabs are now declared by passing tabs, activeTab, and onTabChange props, and a new named header slot pins consumer-supplied content above the tabs. This standardizes the modal tab look. <img width="1089" height="290" alt="Screenshot 2026-05-06 at 12 17 33 PM" src="https://github.com/user-attachments/assets/08d5a039-0aef-4aa7-b972-1e43b8723685" /> ## 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 - [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: evan
This commit is contained in:
@@ -2,6 +2,8 @@ import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import tailwindStyles from "../../styles.css?inline";
|
||||
|
||||
export type OModalTab = { key: string; label: string };
|
||||
|
||||
@customElement("o-modal")
|
||||
export class OModal extends LitElement {
|
||||
static styles = [unsafeCSS(tailwindStyles)];
|
||||
@@ -28,6 +30,15 @@ export class OModal extends LitElement {
|
||||
@property({ type: String })
|
||||
public maxWidth = "";
|
||||
|
||||
@property({ type: Array })
|
||||
public tabs: OModalTab[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public activeTab = "";
|
||||
|
||||
@property({ attribute: false })
|
||||
public onTabChange?: (key: string) => void;
|
||||
|
||||
public onClose?: () => void;
|
||||
|
||||
public open() {
|
||||
@@ -60,7 +71,48 @@ export class OModal extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleTabClick(key: string) {
|
||||
this.onTabChange?.(key);
|
||||
}
|
||||
|
||||
private renderTabs() {
|
||||
return html`
|
||||
<div
|
||||
role="tablist"
|
||||
class="flex justify-center border-b border-white/10 px-4 lg:px-6 gap-1 shrink-0"
|
||||
>
|
||||
${this.tabs.map((tab) => {
|
||||
const active = this.activeTab === tab.key;
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
data-key=${tab.key}
|
||||
aria-selected=${active}
|
||||
class="px-4 py-3 text-sm font-bold uppercase tracking-wider transition-all relative cursor-pointer ${active
|
||||
? "text-aquarius"
|
||||
: "text-white/40 hover:text-white/70"}"
|
||||
@click=${() => this.handleTabClick(tab.key)}
|
||||
>
|
||||
${tab.label}
|
||||
${active
|
||||
? html`<div
|
||||
class="absolute bottom-0 left-0 right-0 h-0.5 bg-malibu-blue"
|
||||
></div>`
|
||||
: ""}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const shouldRender = this.isModalOpen || this.inline;
|
||||
if (!shouldRender) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const backdropClass = this.inline
|
||||
? "relative z-10 w-full h-full flex items-stretch bg-transparent"
|
||||
: "fixed inset-0 z-[9999] bg-black/60 flex items-center justify-center overflow-hidden";
|
||||
@@ -73,42 +125,45 @@ export class OModal extends LitElement {
|
||||
const wrapperStyle =
|
||||
!this.inline && this.maxWidth ? `max-width: ${this.maxWidth};` : "";
|
||||
|
||||
const hasTabs = this.tabs.length > 0;
|
||||
const sectionClass = hasTabs
|
||||
? "relative flex-1 min-h-0 flex flex-col text-white bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10 overflow-hidden"
|
||||
: "relative flex-1 min-h-0 flex flex-col text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-hidden";
|
||||
|
||||
return html`
|
||||
${this.isModalOpen
|
||||
? html`
|
||||
<aside
|
||||
class="${backdropClass}"
|
||||
@click=${this.inline ? null : () => this.close()}
|
||||
>
|
||||
<div
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
class="${wrapperClass}"
|
||||
style="${wrapperStyle}"
|
||||
<aside
|
||||
class="${backdropClass}"
|
||||
@click=${this.inline ? null : () => this.close()}
|
||||
>
|
||||
<div
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
class="${wrapperClass}"
|
||||
style="${wrapperStyle}"
|
||||
>
|
||||
${this.inline || this.hideCloseButton
|
||||
? html``
|
||||
: html`<div
|
||||
class="absolute top-5 right-5 z-10 text-white cursor-pointer"
|
||||
@click=${() => this.close()}
|
||||
>
|
||||
${this.inline || this.hideCloseButton
|
||||
? html``
|
||||
: html`<div
|
||||
class="absolute top-5 right-5 z-10 text-white cursor-pointer"
|
||||
@click=${() => this.close()}
|
||||
>
|
||||
✕
|
||||
</div>`}
|
||||
${!this.hideHeader && this.title
|
||||
? html`<div
|
||||
class="px-[1.4rem] py-[1rem] text-2xl font-bold text-white"
|
||||
>
|
||||
${this.title}
|
||||
</div>`
|
||||
: html``}
|
||||
<section
|
||||
class="relative flex-1 min-h-0 p-0 lg:p-[1.4rem] text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-y-auto"
|
||||
>
|
||||
<slot></slot>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
`
|
||||
: html``}
|
||||
✕
|
||||
</div>`}
|
||||
${!this.hideHeader && this.title
|
||||
? html`<div
|
||||
class="px-[1.4rem] py-[1rem] text-2xl font-bold text-white"
|
||||
>
|
||||
${this.title}
|
||||
</div>`
|
||||
: html``}
|
||||
<section class="${sectionClass}">
|
||||
<slot name="header"></slot>
|
||||
${hasTabs ? this.renderTabs() : html``}
|
||||
<div class="flex-1 min-h-0 overflow-y-auto p-0 lg:p-[1.4rem]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { translateText } from "../../Utils";
|
||||
|
||||
export type LeaderboardTab = "players" | "clans";
|
||||
|
||||
@customElement("leaderboard-tabs")
|
||||
export class LeaderboardTabs extends LitElement {
|
||||
@property({ type: String }) activeTab: LeaderboardTab = "players";
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private baseTabClass =
|
||||
"px-6 py-2 rounded-full text-sm font-bold uppercase tracking-wider transition-all cursor-pointer select-none";
|
||||
private activeTabClass = "bg-blue-600 text-white";
|
||||
private inactiveTabClass =
|
||||
"text-white/40 hover:text-white/60 hover:bg-white/5";
|
||||
|
||||
private getTabClass(active: boolean) {
|
||||
return [
|
||||
this.baseTabClass,
|
||||
active ? this.activeTabClass : this.inactiveTabClass,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
@state()
|
||||
private playerClass = this.getTabClass(this.activeTab === "players");
|
||||
@state()
|
||||
private clanClass = this.getTabClass(this.activeTab === "clans");
|
||||
|
||||
private handleTabChange(tab: LeaderboardTab) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<LeaderboardTab>("tab-change", {
|
||||
detail: tab,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
this.playerClass = this.getTabClass(tab === "players");
|
||||
this.clanClass = this.getTabClass(tab === "clans");
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
role="tablist"
|
||||
class="flex gap-2 p-1 bg-white/5 rounded-full border border-white/10 mb-4 w-fit mx-auto mt-4"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="${this.playerClass}"
|
||||
@click=${() => this.handleTabChange("players")}
|
||||
id="player-leaderboard-tab"
|
||||
aria-selected=${this.activeTab === "players"}
|
||||
>
|
||||
${translateText("leaderboard_modal.ranked_tab")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="${this.clanClass}"
|
||||
@click=${() => this.handleTabChange("clans")}
|
||||
id="clan-leaderboard-tab"
|
||||
aria-selected=${this.activeTab === "clans"}
|
||||
>
|
||||
${translateText("leaderboard_modal.clans_tab")}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user