diff --git a/src/components/labs/__tests__/lab-unit-input.browser.test.tsx b/src/components/labs/__tests__/lab-unit-input.browser.test.tsx
new file mode 100644
index 000000000..d11dff4cf
--- /dev/null
+++ b/src/components/labs/__tests__/lab-unit-input.browser.test.tsx
@@ -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 (
+ <>
+
+
+
+ >
+ );
+}
+
+describe(" typed into", () => {
+ it("keeps a unit that starts with a 1", async () => {
+ const { screen } = await renderWithApp();
+ 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();
+ 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();
+ await screen.getByRole("checkbox", { name: /no unit/i }).click();
+ await expect
+ .poll(() => screen.getByTestId("unit").element().textContent)
+ .toBe("1");
+ });
+});
diff --git a/src/components/labs/lab-unit-input.tsx b/src/components/labs/lab-unit-input.tsx
index cdbc5d1df..7545f791e 100644
--- a/src/components/labs/lab-unit-input.tsx
+++ b/src/components/labs/lab-unit-input.tsx
@@ -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({
+ onCheckedChange={(state) => {
+ setTypedOne(false);
onChange(
state === true
? DIMENSIONLESS_UNIT
: isDimensionlessUnit(typed.current)
? ""
: typed.current,
- )
- }
+ );
+ }}
aria-describedby={hintId}
className="mt-0.5"
/>