Add map search 🔍 (#4283)

## Description:

Add a search input field to the map picker section header, allowing
users to quickly filter maps by name.

- Place transparent search input on the right side of the "Maps" section
header
- Filter maps by translated name and map ID as the user types
- Hide Featured/All/Favourites tab buttons while search is active
- Show filtered results with a count heading, or a "no results" empty
state
- Clear button appears when search input has text

<img width="857" height="463" alt="Screenshot 2026-06-15 001415"
src="https://github.com/user-attachments/assets/35e1101a-177e-4923-bb1d-34eb683c6f80"
/>

No search results:

<img width="855" height="454" alt="Screenshot 2026-06-15 001433"
src="https://github.com/user-attachments/assets/bf27211d-5891-4739-a92f-0fc44b3c9c61"
/>

## 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:

FloPinguin
This commit is contained in:
FloPinguin
2026-06-15 02:54:43 +02:00
committed by GitHub
parent 6c8ce958b2
commit b997099dfe
3 changed files with 124 additions and 12 deletions
+60 -10
View File
@@ -36,6 +36,7 @@ export class MapPicker extends LitElement {
@property({ type: Boolean }) useRandomMap = false;
@property({ type: Boolean }) showMedals = false;
@property({ type: Boolean }) randomMapDivider = false;
@property({ type: String }) searchQuery = "";
@property({ attribute: false }) mapWins: Map<GameMapType, Set<Difficulty>> =
new Map();
@property({ attribute: false }) onSelectMap?: (map: GameMapType) => void;
@@ -74,6 +75,16 @@ export class MapPicker extends LitElement {
event.preventDefault();
}
private get filteredMaps(): MapInfo[] {
if (!this.searchQuery.trim()) return [];
const query = this.searchQuery.trim().toLowerCase();
return maps.filter((m) => {
const name = translateText(m.translationKey).toLowerCase();
const id = m.id.toLowerCase();
return name.includes(query) || id.includes(query);
});
}
private getWins(mapValue: GameMapType): Set<Difficulty> {
return this.mapWins?.get(mapValue) ?? new Set();
}
@@ -211,6 +222,36 @@ export class MapPicker extends LitElement {
}
}
private renderSearchResults() {
const results = this.filteredMaps;
if (results.length === 0) {
return html`<div
class="w-full flex flex-col items-center justify-center gap-3 py-12 px-4 text-center rounded-xl border border-dashed border-white/10 bg-black/20"
>
<svg
class="w-8 h-8 text-white/30"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
<p class="text-sm text-white/50 leading-relaxed max-w-xs">
${translateText("map_component.no_results")}
</p>
</div>`;
}
return html`<div class="w-full">
${this.renderSectionHeading(
`${translateText("map_component.search_results")} (${results.length})`,
)}
${this.renderMapGrid(results)}
</div>`;
}
private renderTabButton(tab: MapTab, label: string) {
const isActive = this.activeTab === tab;
return html`<button
@@ -227,20 +268,29 @@ export class MapPicker extends LitElement {
}
render() {
const isSearching = this.searchQuery.trim().length > 0;
return html`
<div class="space-y-8">
<div class="w-full">
<div
role="tablist"
aria-label="${translateText("map.map")}"
class="grid grid-cols-3 gap-2 rounded-xl border border-white/10 bg-black/20 p-1"
>
${this.renderTabButton("featured", translateText("map.featured"))}
${this.renderTabButton("all", translateText("map.all"))}
${this.renderTabButton("favorites", translateText("map.favorites"))}
</div>
${isSearching
? null
: html`<div
role="tablist"
aria-label="${translateText("map.map")}"
class="grid grid-cols-3 gap-2 rounded-xl border border-white/10 bg-black/20 p-1"
>
${this.renderTabButton(
"featured",
translateText("map.featured"),
)}
${this.renderTabButton("all", translateText("map.all"))}
${this.renderTabButton(
"favorites",
translateText("map.favorites"),
)}
</div>`}
</div>
${this.renderActiveTab()}
${isSearching ? this.renderSearchResults() : this.renderActiveTab()}
<div
class="w-full ${this.randomMapDivider
? "pt-4 border-t border-white/5"