mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(records): let the smoking history fields take keystrokes
The quit month only accepted text that was already a whole YYYY-MM month and the start year clamped every keystroke into 1900..now, so neither could be typed into. Both keep a draft while typing and commit a valid value. Dependabot also stops proposing Node majors for the image. Refs #1334
This commit is contained in:
1 parent
25770912ce
commit
40200b55a7
3 files changed
+126
-18
No files matched your search
@@ -2,6 +2,20 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Typing into the smoking history.** "When you stopped" only took text
|
||||
that was already a whole YYYY-MM month, so every keystroke was dropped
|
||||
and only a paste got in. The start year clamped each keystroke into its
|
||||
range, so the first digit turned into 1900. Both fields now keep what is
|
||||
typed and commit it once it is a valid value, and leaving the month field
|
||||
pads "2019-3" to "2019-03". (#1334)
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependabot no longer proposes a new Node major for the image. The major
|
||||
is tied to `engines`, `.nvmrc` and `.node-version` and moves by hand.
|
||||
|
||||
## [1.47.0] — 2026-11-04
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { expect, test } from "./setup/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* The tobacco section's typed fields take what a person types one key at a
|
||||
* time. Through v1.47.0 neither did: "When you stopped" only accepted text
|
||||
* that was already a whole YYYY-MM month, so every keystroke was thrown away
|
||||
* and only a paste got in, and the start year clamped each keystroke into
|
||||
* 1900..this year, so the first digit became 1900.
|
||||
*
|
||||
* The test types and leaves the fields without saving, so the shared
|
||||
* account keeps whatever answers it had.
|
||||
*/
|
||||
test.describe("life circumstances typed fields", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test("start year and quit month take keystrokes", async ({ page }) => {
|
||||
await page.goto("/settings/anamnesis", { waitUntil: "domcontentloaded" });
|
||||
const section = page.locator('[data-slot="life-section-tobacco"]');
|
||||
await section.locator("summary").click();
|
||||
|
||||
await section.locator("#life-tobacco-status").selectOption("FORMER");
|
||||
|
||||
const start = section.locator("#life-tobacco-start");
|
||||
await start.fill("");
|
||||
await start.pressSequentially("1985");
|
||||
await expect(start).toHaveValue("1985");
|
||||
await start.blur();
|
||||
await expect(start).toHaveValue("1985");
|
||||
|
||||
const quit = section.locator("#life-tobacco-quit");
|
||||
await quit.fill("");
|
||||
await quit.pressSequentially("2019-03");
|
||||
await expect(quit).toHaveValue("2019-03");
|
||||
await quit.blur();
|
||||
await expect(quit).toHaveValue("2019-03");
|
||||
|
||||
// A single-digit month is padded when the field is left.
|
||||
await quit.fill("");
|
||||
await quit.pressSequentially("2018-7");
|
||||
await expect(quit).toHaveValue("2018-7");
|
||||
await quit.blur();
|
||||
await expect(quit).toHaveValue("2018-07");
|
||||
});
|
||||
});
|
||||
@@ -281,6 +281,11 @@ function NumberField({
|
||||
onChange: (value: number | null) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
// What the person is typing. A year passes through "1", "19" and "198" on
|
||||
// its way to "1985", so clamping each keystroke turned the first digit into
|
||||
// the minimum and the field could not be typed into. A value is committed
|
||||
// once it is in range, and leaving the field puts the committed one back.
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
return (
|
||||
<Field id={id} label={label}>
|
||||
<Input
|
||||
@@ -290,21 +295,74 @@ function NumberField({
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value ?? ""}
|
||||
value={draft ?? (value === null ? "" : String(value))}
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value;
|
||||
setDraft(raw);
|
||||
if (raw === "") return onChange(null);
|
||||
const parsed = Number(raw);
|
||||
if (Number.isFinite(parsed)) {
|
||||
onChange(Math.min(max, Math.max(min, parsed)));
|
||||
if (Number.isFinite(parsed) && parsed >= min && parsed <= max) {
|
||||
onChange(parsed);
|
||||
}
|
||||
}}
|
||||
onBlur={() => setDraft(null)}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A month typed as YYYY-MM. Only a complete month is committed, so the field
|
||||
* keeps what is being typed on its own: holding it to the committed value
|
||||
* threw away every keystroke until the text was a whole month, and only a
|
||||
* paste got through. Leaving the field pads "2019-3" to "2019-03" and puts
|
||||
* the committed month back over anything that is not one.
|
||||
*/
|
||||
function QuitMonthInput({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
id: string;
|
||||
value: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
placeholder="YYYY-MM"
|
||||
inputMode="numeric"
|
||||
maxLength={7}
|
||||
value={draft ?? value ?? ""}
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value;
|
||||
setDraft(raw);
|
||||
const month = completeMonth(raw);
|
||||
if (month !== undefined) onChange(month);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (draft !== null) {
|
||||
const short = /^(\d{4})-([1-9])$/.exec(draft.trim());
|
||||
if (short) onChange(`${short[1]}-0${short[2]}`);
|
||||
}
|
||||
setDraft(null);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** A whole month, null for an emptied field, undefined while still typing. */
|
||||
function completeMonth(raw: string): string | null | undefined {
|
||||
const text = raw.trim();
|
||||
if (text === "") return null;
|
||||
return /^\d{4}-(0[1-9]|1[0-2])$/.test(text) ? text : undefined;
|
||||
}
|
||||
|
||||
function MultiField<T extends string>({
|
||||
id,
|
||||
label,
|
||||
@@ -544,23 +602,13 @@ function SectionBody({
|
||||
id={id("quit")}
|
||||
label={t("records.life.fields.quitMonth")}
|
||||
>
|
||||
<Input
|
||||
<QuitMonthInput
|
||||
id={id("quit")}
|
||||
placeholder="YYYY-MM"
|
||||
inputMode="numeric"
|
||||
maxLength={7}
|
||||
value={v.quitMonth ?? ""}
|
||||
value={v.quitMonth}
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value.trim();
|
||||
patch("TOBACCO_DETAIL", {
|
||||
quitMonth: /^\d{4}-(0[1-9]|1[0-2])$/.test(raw)
|
||||
? raw
|
||||
: raw === ""
|
||||
? null
|
||||
: v.quitMonth,
|
||||
});
|
||||
}}
|
||||
onChange={(quitMonth) =>
|
||||
patch("TOBACCO_DETAIL", { quitMonth })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
) : null}
|
||||
|
||||
Reference in new issue
Block a user