Merge pull request #1141 from MBombeck/ready/pr-1132

feat(labs): let the lab list's range bar use a wide card
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-10-04 16:14:56 +02:00
commit 63a5304e43
4 files changed
+222 -5

No files matched your search

@@ -106,4 +106,63 @@ describe("<LabList> columns", () => {
const html = renderList([reading({})]);
expect(html).toMatch(/class="[^"]*lucide-chevron-right[^"]*lg:invisible/);
});
describe("on a wide list", () => {
const NARROW_TIER =
"lg:@max-5xl:grid-cols-[minmax(0,1fr)_max-content_12rem_72px_auto]";
const WIDE_TIER =
"@5xl:grid-cols-[minmax(0,1fr)_max-content_minmax(12rem,18rem)_72px_auto]";
it("makes the card a container, so the columns follow the list's width, not the window's", () => {
const html = renderList([reading({})]);
expect(html).toMatch(/<div[^>]*class="[^"]*@container[^"]*"/);
});
it("leaves the lg table as it was and adds a wider tier from @5xl, the two excluding each other", () => {
const html = renderList([reading({})]);
expect(html).toContain(NARROW_TIER);
expect(html).toContain(WIDE_TIER);
// Name, badge, bar, trend and chevron: five columns in both tiers.
expect(NARROW_TIER.split("_")).toHaveLength(5);
expect(WIDE_TIER.split("_")).toHaveLength(5);
});
it("keeps the trend column as wide as its 72px sparkline in both tiers", () => {
expect(NARROW_TIER.split("_")[3]).toBe("72px");
expect(WIDE_TIER.split("_")[3]).toBe("72px");
});
it("keeps the name column the flexible one in both tiers", () => {
expect(NARROW_TIER).toContain("[minmax(0,1fr)_");
expect(WIDE_TIER).toContain("[minmax(0,1fr)_");
});
it("lets the range bar fill its column from lg, and caps it on a phone", () => {
const html = renderList([reading({})]);
expect(html).toMatch(
/data-slot="lab-reference-range-bar"[^>]*class="[^"]*max-w-48[^"]*lg:max-w-none/,
);
// The slot no longer pins the bar to 12rem itself.
expect(html).not.toContain("lg:w-48");
});
it("wraps a long analyte name from lg up and truncates it on a phone", () => {
const longName = "A".repeat(20) + " " + "B".repeat(100);
const html = renderList([reading({ analyte: longName })]);
expect(html).toContain(longName);
const cls = html.match(
/data-slot="lab-list-analyte"[^>]*class="([^"]*)"|class="([^"]*)"[^>]*data-slot="lab-list-analyte"/,
);
const classes = (cls?.[1] ?? cls?.[2] ?? "").split(" ");
expect(classes).toContain("truncate");
for (const c of [
"lg:overflow-visible",
"lg:whitespace-normal",
"lg:text-clip",
"lg:[overflow-wrap:anywhere]",
]) {
expect(classes).toContain(c);
}
});
});
});
+18 -4
View File
@@ -93,8 +93,11 @@ function RangeBadgeCell({ status }: { status: LabResultDto["rangeStatus"] }) {
function LabRangeBarSlot({ reading }: { reading: LabResultDto }) {
return (
<div className={cn("w-full lg:w-48", MOBILE_CELL.rangeBar)}>
<div className={cn("w-full", MOBILE_CELL.rangeBar)}>
{/* A phone keeps the bar's own 12rem cap; from `lg` the bar fills its
column, which is 12rem until the list is wide enough to grow it. */}
<LabReferenceRangeBar
className="lg:max-w-none"
value={reading.value}
referenceLow={reading.referenceLow}
referenceHigh={reading.referenceHigh}
@@ -255,15 +258,26 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
{t("labs.showingLatestOf", { shown, total })}
</p>
) : null}
<Card>
<CardContent className="divide-border grid grid-cols-1 divide-y p-0 lg:grid-cols-[minmax(0,1fr)_max-content_12rem_72px_auto] lg:gap-x-3">
{/* `@container` makes the columns follow the width the list has, not the
window's: below `@5xl` (64rem of card) the row is the `lg` table it
always was (the two tiers exclude each other, so the result does not
depend on the order the CSS is emitted in), from there up the range
bar takes part of the extra width instead of all of it going to the
name. The trend column stays 72px in both tiers: the sparkline is a
fixed 72px glyph, and a wider column would only open an empty strip
between it and the chevron. */}
<Card className="@container">
<CardContent className="divide-border grid grid-cols-1 divide-y p-0 lg:gap-x-3 lg:@max-5xl:grid-cols-[minmax(0,1fr)_max-content_12rem_72px_auto] @5xl:grid-cols-[minmax(0,1fr)_max-content_minmax(12rem,18rem)_72px_auto]">
{groups.map((group) => {
const inner = (
<div className={cn("min-w-0 flex-1", MOBILE_CELL.reading)}>
<div className="flex flex-wrap items-baseline gap-x-2 text-sm">
{/* One line with an ellipsis on a phone; from `lg` the name
wraps instead, so a long one (the schema allows 120
characters) always reads in full. */}
<span
data-slot="lab-list-analyte"
className="truncate font-medium"
className="truncate font-medium lg:overflow-visible lg:[overflow-wrap:anywhere] lg:text-clip lg:whitespace-normal"
>
{group.analyte}
</span>
@@ -1,6 +1,7 @@
"use client";
import { RangeBar } from "@/components/targets/range-bar";
import { cn } from "@/lib/utils";
import { useLabNumber } from "./use-lab-format";
@@ -16,11 +17,14 @@ export function LabReferenceRangeBar({
referenceLow,
referenceHigh,
unit,
className,
}: {
value: number | null;
referenceLow: number | null;
referenceHigh: number | null;
unit: string;
/** Extra classes on the bar's wrapper; it caps itself at 12rem by default. */
className?: string;
}) {
const labNumber = useLabNumber();
const hasLow = referenceLow !== null && Number.isFinite(referenceLow);
@@ -48,7 +52,7 @@ export function LabReferenceRangeBar({
return (
<div
data-slot="lab-reference-range-bar"
className="w-full max-w-48 shrink-0"
className={cn("w-full max-w-48 shrink-0", className)}
aria-label={rangeLabel}
>
<RangeBar