mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(labs): let a unit typed key by key pass through "1"
The no-unit state was read from the value, so the first keystroke of 10^9/L or 1/min ticked the box and locked the field. Only the box makes the choice now; a typed 1 stays text.
This commit is contained in:
1 parent
70cc753c49
commit
6a8ac98613
2 files changed
+68
-7
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { useState } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { userEvent } from "vitest/browser";
|
||||
|
||||
import { renderWithApp } from "../../../../tests/browser/render";
|
||||
import { LabUnitInput } from "../lab-unit-input";
|
||||
|
||||
/**
|
||||
* The unit field as a form holds it, typed into key by key. A unit that
|
||||
* starts with a 1 (10^9/L, 1/min) passes through "1" on the way, and that
|
||||
* keystroke must not turn into the "no unit" choice.
|
||||
*/
|
||||
function Controlled({ initial = "" }: { initial?: string }) {
|
||||
const [unit, setUnit] = useState(initial);
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="u">Unit</label>
|
||||
<LabUnitInput id="u" value={unit} onChange={setUnit} />
|
||||
<output data-testid="unit">{unit}</output>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe("<LabUnitInput> typed into", () => {
|
||||
it("keeps a unit that starts with a 1", async () => {
|
||||
const { screen } = await renderWithApp(<Controlled />);
|
||||
await userEvent.type(screen.getByLabelText("Unit"), "10^9/L");
|
||||
await expect
|
||||
.element(screen.getByTestId("unit"))
|
||||
.toHaveTextContent("10^9/L");
|
||||
await expect
|
||||
.element(screen.getByRole("checkbox", { name: /no unit/i }))
|
||||
.not.toBeChecked();
|
||||
});
|
||||
|
||||
it("shows a stored unity as the ticked choice and gives the field back on unticking", async () => {
|
||||
const { screen } = await renderWithApp(<Controlled initial="1" />);
|
||||
const box = screen.getByRole("checkbox", { name: /no unit/i });
|
||||
await expect.element(box).toBeChecked();
|
||||
await expect.element(screen.getByLabelText("Unit")).toBeDisabled();
|
||||
await box.click();
|
||||
await expect.element(screen.getByLabelText("Unit")).toBeEnabled();
|
||||
await userEvent.type(screen.getByLabelText("Unit"), "mmol/L");
|
||||
await expect
|
||||
.element(screen.getByTestId("unit"))
|
||||
.toHaveTextContent("mmol/L");
|
||||
});
|
||||
|
||||
it("sets the unity when the box is ticked", async () => {
|
||||
const { screen } = await renderWithApp(<Controlled initial="mg/dL" />);
|
||||
await screen.getByRole("checkbox", { name: /no unit/i }).click();
|
||||
await expect
|
||||
.poll(() => screen.getByTestId("unit").element().textContent)
|
||||
.toBe("1");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useRef } from "react";
|
||||
import { useId, useRef, useState } from "react";
|
||||
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -48,8 +48,9 @@ interface LabUnitInputProps {
|
||||
* The unit of a lab value, with an explicit "no unit" choice for a ratio or an
|
||||
* index. Ticking it sets the unit to the UCUM unity `"1"`, which every write
|
||||
* path accepts as a real unit; leaving the field empty is still refused, so a
|
||||
* unit an extraction dropped is never taken for a value that has none. Typing
|
||||
* `1` by hand is the same choice and ticks the box.
|
||||
* unit an extraction dropped is never taken for a value that has none. Only
|
||||
* the box makes the choice: a unit typed key by key passes through "1" on its
|
||||
* way to `10^9/L`, and that keystroke stays text.
|
||||
*/
|
||||
export function LabUnitInput({
|
||||
id,
|
||||
@@ -64,7 +65,9 @@ export function LabUnitInput({
|
||||
const { t } = useTranslations();
|
||||
const noneId = useId();
|
||||
const hintId = `${noneId}-hint`;
|
||||
const none = allowNoUnit && isDimensionlessUnit(value);
|
||||
// Set when a keystroke, not the box, left the field at "1".
|
||||
const [typedOne, setTypedOne] = useState(false);
|
||||
const none = allowNoUnit && isDimensionlessUnit(value) && !typedOne;
|
||||
// What was typed before the box was ticked, so unticking gives it back.
|
||||
const typed = useRef(none ? "" : value);
|
||||
|
||||
@@ -74,6 +77,7 @@ export function LabUnitInput({
|
||||
value={none ? "" : value}
|
||||
onChange={(e) => {
|
||||
typed.current = e.target.value;
|
||||
setTypedOne(isDimensionlessUnit(e.target.value));
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
placeholder={none ? t("labs.unit.none") : placeholder}
|
||||
@@ -95,15 +99,16 @@ export function LabUnitInput({
|
||||
<Checkbox
|
||||
id={noneId}
|
||||
checked={none}
|
||||
onCheckedChange={(state) =>
|
||||
onCheckedChange={(state) => {
|
||||
setTypedOne(false);
|
||||
onChange(
|
||||
state === true
|
||||
? DIMENSIONLESS_UNIT
|
||||
: isDimensionlessUnit(typed.current)
|
||||
? ""
|
||||
: typed.current,
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
aria-describedby={hintId}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user