Rework fluentslider component and write tests (#2682)

## Description:

After 2 months of vacancy(my bad sorry), i have returned to end this
mess of a PR stain that i left to the codebase.
The issue is fixed, my written tests are passing, and i hand-tested and
it worked out.
Also I had to transforms Lit's ES modules into commonJS format so jest
can execute for my test,
which will indirectly enable other Lit components to be able for
testing(only my test for now)
refer to #2148 for UI stuff, nothing changed there.

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

jackochess
This commit is contained in:
VectorSophie
2025-12-24 06:24:37 -08:00
committed by GitHub
parent 0793153f4e
commit 00babf4289
6 changed files with 463 additions and 37 deletions
+11 -17
View File
@@ -25,6 +25,7 @@ import {
import { generateID } from "../core/Util";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
import "./components/FluentSlider";
import "./components/LobbyTeamView";
import "./components/Maps";
import { JoinLobbyEvent } from "./Main";
@@ -331,25 +332,17 @@ export class HostLobbyModal extends LitElement {
${translateText("host_modal.options_title")}
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
<div class="option-card">
<fluent-slider
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
this.bots === 0
? translateText("host_modal.bots_disabled")
: this.bots
}
</div>
</label>
.value=${this.bots}
labelKey="host_modal.bots"
disabledKey="host_modal.bots_disabled"
@value-changed=${this.handleBotsChange}
></fluent-slider>
</div>
${
!(
@@ -661,7 +654,8 @@ export class HostLobbyModal extends LitElement {
// Modified to include debouncing
private handleBotsChange(e: Event) {
const value = parseInt((e.target as HTMLInputElement).value);
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
+11 -16
View File
@@ -21,6 +21,7 @@ import { generateID } from "../core/Util";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
import "./components/FluentSlider";
import "./components/Maps";
import { fetchCosmetics } from "./Cosmetics";
import { FlagInput } from "./FlagInput";
@@ -236,24 +237,17 @@ export class SinglePlayerModal extends LitElement {
${translateText("single_modal.options_title")}
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
<div class="option-card">
<fluent-slider
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("single_modal.bots")}</span>${this
.bots === 0
? translateText("single_modal.bots_disabled")
: this.bots}
</div>
</label>
.value=${this.bots}
labelKey="single_modal.bots"
disabledKey="single_modal.bots_disabled"
@value-changed=${this.handleBotsChange}
></fluent-slider>
</div>
${!(
this.gameMode === GameMode.Team &&
@@ -448,7 +442,8 @@ export class SinglePlayerModal extends LitElement {
}
private handleBotsChange(e: Event) {
const value = parseInt((e.target as HTMLInputElement).value);
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
+152
View File
@@ -0,0 +1,152 @@
import { LitElement, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { translateText } from "../Utils";
@customElement("fluent-slider")
export class FluentSlider extends LitElement {
static styles = css`
:host {
display: block;
width: 100%;
font-family: inherit;
}
.slider-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
text-align: center;
}
.option-card-title {
font-size: 14px; /* match other cards */
color: #aaa; /* light gray text */
text-align: center;
margin: 0 0 4px 0;
font-weight: normal;
}
input[type="range"] {
width: 100%;
max-width: 100%;
background-color: transparent;
}
input[type="number"] {
width: 60px;
background-color: #2d3748;
color: #aaa; /* match label color */
border: 1px solid #4a5568;
text-align: center;
border-radius: 4px;
font-weight: normal;
font-family: inherit;
}
span.editable {
cursor: pointer;
min-width: 60px;
display: inline-block;
text-align: center;
color: #aaa; /* match label color */
font-weight: normal;
user-select: none;
}
`;
@property({ type: Number }) value = 0;
@property({ type: Number }) min = 0;
@property({ type: Number }) max = 400;
@property({ type: Number }) step = 1;
@property({ type: String }) labelKey = "";
@property({ type: String }) disabledKey = "";
@state() private isEditing = false;
@query("input[type='number']") private numberInput!: HTMLInputElement;
private dispatchValueChange() {
this.dispatchEvent(
new CustomEvent("value-changed", {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);
}
private handleSliderInput(e: Event) {
const target = e.target as HTMLInputElement;
this.value = target.valueAsNumber;
}
private handleSliderChange(e: Event) {
const target = e.target as HTMLInputElement;
this.value = target.valueAsNumber;
this.dispatchValueChange();
}
private handleNumberChange(e: Event) {
const target = e.target as HTMLInputElement;
let val = target.valueAsNumber;
if (isNaN(val)) {
val = this.min;
}
if (val < this.min) val = this.min;
if (val > this.max) val = this.max;
this.value = val;
this.dispatchValueChange();
}
private handleNumberKeyDown(e: KeyboardEvent) {
if (e.key === "Enter") this.isEditing = false;
}
private enableEditing() {
this.isEditing = true;
this.updateComplete.then(() => this.numberInput?.focus());
}
render() {
return html`
<div class="slider-container">
<input
type="range"
.min=${this.min}
.max=${this.max}
.step=${this.step}
.valueAsNumber=${this.value}
@input=${this.handleSliderInput}
@change=${this.handleSliderChange}
/>
<div class="option-card-title">
<span>${this.labelKey ? translateText(this.labelKey) : ""}</span>
${this.isEditing
? html`<input
type="number"
.min=${this.min}
.max=${this.max}
.valueAsNumber=${this.value}
@input=${this.handleNumberChange}
@blur=${() => (this.isEditing = false)}
@keydown=${this.handleNumberKeyDown}
/>`
: html`<span
class="editable"
role="button"
tabindex="0"
@click=${this.enableEditing}
@keydown=${(e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
this.enableEditing();
e.preventDefault();
}
}}
>
${this.value === 0 && this.disabledKey
? translateText(this.disabledKey)
: this.value}
</span>`}
</div>
</div>
`;
}
}