Fix wave: dropdown keyboard/focus, appearance resets, inline key editors, content-sized slider
- MenuShell: after placement, focus lands in the search when the menu has one, else the body (tabIndex -1) — the lists' mount-time focus ran during the hidden measuring pass and silently failed, so no dropdown ever opened focused. The body owns the arrow machine for searchless menus: ArrowDown/Up cycle options with wrap (first/last entry from the body, native-select convention), Home/End, Enter activates, preventDefault so arrows never scroll the page. - Appearance: the CSS-var applier only ever setProperty'd — a later write omitting a var left the stale inline value forever (empty background never reset, serif toggle-back looked dead). appliedNamesRef tracks what was set; staleVarNames (pure, exported) returns what to removeProperty. - CommitInput: controlled — after a commit the field snaps to the stored record, so the clamp on a -5 prose size is visible (12), Esc reverts. - Keys: Add-provider dropdown closes on pick; the paste row is inline (label | input | Save | Cancel); existing-row editors go inline with an explicit Cancel beside Save/Delete — folding is visible, not Esc-only. - Graph context: readout is content-sized (tabular-nums) — no fixed-width dead gap when "off"; the dictation strip fills whatever space remains. Gates: tsc clean, 866/866 tests (new staleVarNames suite), lint 0, build ok. Live-verified via Playwright: dropdown focus+arrow cycles+wrap+Enter pick, background set→empty reset, serif round-trip, visible clamp to 12, provider pick closes menu, inline editor + Cancel + Esc fold, slider width 270→277px.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
* control together) and the control itself locks — the row owns the dim.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** A section block: pronounced mono heading + content. */
|
||||
@@ -165,7 +166,10 @@ export function CommitInput({
|
||||
testid,
|
||||
type = "text",
|
||||
}: {
|
||||
/** "" = unset (the factory default). */
|
||||
/** "" = unset (the factory default). Mirrors the STORED record —
|
||||
* after a clamped/normalized commit this flows back and the field
|
||||
* snaps to the stored truth (owner repro: a negative size kept
|
||||
* displaying "-5" while 12 was stored). */
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
onCommit: (next: string) => void;
|
||||
@@ -173,21 +177,32 @@ export function CommitInput({
|
||||
testid?: string;
|
||||
type?: "text" | "number";
|
||||
}) {
|
||||
// The draft exists only while editing; a blur shows the record's value
|
||||
// again — commit-on-blur/Enter (owner ruling), the field honest about
|
||||
// what landed.
|
||||
const [draft, setDraft] = useState(value);
|
||||
const [editing, setEditing] = useState(false);
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
defaultValue={value}
|
||||
value={editing ? draft : value}
|
||||
placeholder={placeholder}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testid}
|
||||
onBlur={(e) => {
|
||||
const next = e.target.value.trim();
|
||||
onChange={(e) => {
|
||||
setEditing(true);
|
||||
setDraft(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!editing) return;
|
||||
setEditing(false);
|
||||
const next = draft.trim();
|
||||
if (next !== value) onCommit(next);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") e.currentTarget.blur();
|
||||
if (e.key === "Escape") {
|
||||
e.currentTarget.value = value;
|
||||
setEditing(false);
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user