Fix per-frame layout jank when focusing a toggle-input-card field (#4314)

## Problem

Focusing the number field of a `toggle-input-card` (Game Timer / Gold
Multiplier / Starting Gold, in both the single-player and host-lobby
modals) cost several ms of layout/paint **every tick** for as long as
the field stayed focused.

## Root cause

The input was rendered **conditionally** — `${this.checked ?
html`…<input>…` : nothing}`. Enabling a toggle therefore **freshly
inserts** the `<input>` into the DOM, and **focusing a just-inserted
input** is what forced the per-frame layout/paint. An input that was
already present in the DOM doesn't do this.

## Fix

Keep the input **permanently mounted** and toggle a `hidden` class when
unchecked, instead of conditionally rendering it. Focusing it is then
always focusing an element that was already there. Because both modals
share `<toggle-input-card>`, this single change fixes both.

Also restores the **autofocus + select** of the field on enable (it had
been removed earlier while chasing this bug) — safe now that the input
isn't freshly inserted.

No other UX change: the toggle behavior, checkmark, styling, and all
three cards behave identically.

## Testing

Hard-reload, then in both the Solo and Host-lobby modals, enable each of
Game Timer / Gold Multiplier / Starting Gold, type a value, and keep the
field focused — smooth, no per-frame jank, and the field autofocuses on
enable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-06-17 08:15:24 -07:00
committed by GitHub
parent bb464538d0
commit 678112492c
+22 -6
View File
@@ -1,4 +1,4 @@
import { LitElement, html, nothing } from "lit";
import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { translateText } from "../Utils";
import { CARD_LABEL_CLASS, INPUT_CLASS, cardClass } from "./InputCardStyles";
@@ -28,6 +28,18 @@ export class ToggleInputCard extends LitElement {
createRenderRoot() {
return this;
}
// Autofocus + select the number input when the card is toggled on. Safe now
// that the input is always mounted (focusing a freshly-inserted one janked).
protected updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("checked")) return;
if (changedProperties.get("checked") === false && this.checked) {
const input = this.querySelector("input");
input?.focus();
input?.select();
}
}
private toOptionalNumber(
value: number | string | undefined,
): number | undefined {
@@ -120,10 +132,16 @@ export class ToggleInputCard extends LitElement {
</span>
</button>
${this.checked
? html`
<!-- Keep the input permanently mounted and just hide it when unchecked.
Rendering it conditionally (\${checked ? input : nothing}) inserts a
fresh input on enable, and focusing a just-inserted input forces
several ms of layout/paint per frame. CSS-hiding an always-present
input avoids that. -->
<div
class="absolute left-3 right-3 top-1/2 -translate-y-1/2 z-10"
class="absolute left-3 right-3 top-1/2 -translate-y-1/2 z-10 ${this
.checked
? ""
: "hidden"}"
>
<input
type=${this.inputType}
@@ -140,8 +158,6 @@ export class ToggleInputCard extends LitElement {
@keydown=${this.onKeyDown}
/>
</div>
`
: nothing}
</div>
`;
}