mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(timeline): keep each mean's dot and value one unbreakable unit
The dot before a mean sat on the x-height middle, a little below the text, and a wrapping line could leave it at the end of one line while its value moved to the next. Each dot and value is now one inline-flex unit that centres the dot on the text line and never wraps inside; the row breaks only between units, a long name is cut short inside its own unit, and the leading mean sign rides in the first unit. The legend and chip dots no longer shrink beside long text.
This commit is contained in:
1 parent
b787bc3176
commit
023115b05c
5 files changed
+72
-13
No files matched your search
@@ -551,6 +551,43 @@ describe("value line colours", () => {
|
||||
expect(text(html)).toBe("129/82 mmHg, 82,6 kg");
|
||||
expect(html).toContain('class="sr-only">, </span>');
|
||||
});
|
||||
|
||||
it("keeps each dot and its value in one unit that never breaks", () => {
|
||||
const html = render(
|
||||
<MeanPartsLine
|
||||
lead="Ø"
|
||||
parts={[
|
||||
{ key: "BLOOD_PRESSURE_SYS", text: "129/82 mmHg" },
|
||||
{ key: "WEIGHT", text: "82,6 kg" },
|
||||
{ key: "STRAIN", text: "Daily strain with a very long name 12" },
|
||||
]}
|
||||
seriesColor={seriesColor}
|
||||
/>,
|
||||
);
|
||||
// Each unit opens with its own tag; everything up to the next unit's tag
|
||||
// (the lead, the dot, the value) is inside it.
|
||||
const units = html.split(/(?=<span[^>]*data-slot="timeline-mean-part")/);
|
||||
expect(units).toHaveLength(3);
|
||||
for (const unit of units) {
|
||||
const open = unit.slice(0, unit.indexOf(">"));
|
||||
expect(open).toContain("inline-flex");
|
||||
expect(open).toContain("items-center");
|
||||
expect(open).toContain("whitespace-nowrap");
|
||||
const dot = unit.match(
|
||||
/<span[^>]*data-slot="timeline-series-dot"[^>]*>/,
|
||||
)?.[0];
|
||||
expect(dot).toContain("shrink-0");
|
||||
expect(dot).not.toContain("align-middle");
|
||||
// The value follows the dot inside the same unit.
|
||||
expect(text(unit.slice(unit.indexOf(dot!)))).toMatch(/\S/);
|
||||
}
|
||||
// The lead rides in the first unit, so "Ø" never ends a line alone.
|
||||
expect(text(units[0]).startsWith("Ø")).toBe(true);
|
||||
// A long name is cut short inside its unit.
|
||||
expect(units[2]).toContain(
|
||||
'<span class="min-w-0 truncate">Daily strain with a very long name 12</span>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("life events in the capture picker", () => {
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
} from "./timeline-dates";
|
||||
import { useItemWords } from "./item-words";
|
||||
import { itemsInPeriod } from "./timeline-geometry";
|
||||
import { MeanPartsLine, laneDotStyle } from "./timeline-chart";
|
||||
import { MEAN_PARTS_ROW, MeanPartsLine, laneDotStyle } from "./timeline-chart";
|
||||
import {
|
||||
bucketValues,
|
||||
labelledMeanParts,
|
||||
@@ -201,12 +201,12 @@ export function SelectionBar({
|
||||
})}
|
||||
{means.values.length > 0 && (
|
||||
<span
|
||||
className="text-muted-foreground text-xs tabular-nums"
|
||||
className={MEAN_PARTS_ROW}
|
||||
data-slot="timeline-selection-means"
|
||||
data-bucket={means.start}
|
||||
>
|
||||
{meanBefore}
|
||||
<MeanPartsLine
|
||||
lead={meanBefore || undefined}
|
||||
parts={labelledMeanParts(means.values, seriesFormat, {
|
||||
label: seriesLabel,
|
||||
bloodPressure: t("timeline.values.bloodPressure"),
|
||||
@@ -215,7 +215,7 @@ export function SelectionBar({
|
||||
})}
|
||||
seriesColor={seriesColor}
|
||||
/>
|
||||
{meanAfter}
|
||||
{meanAfter ? <span>{meanAfter}</span> : null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1117,13 +1117,22 @@ function SeriesTable({
|
||||
* ● 82,6 kg". The values stand apart by space, not by a separator glyph;
|
||||
* a screen reader hears a comma between them. The dot is decoration; the
|
||||
* text reads the same without it.
|
||||
*
|
||||
* Each dot and its value are one unit that never breaks: a line wraps only
|
||||
* between units (the caller's container is a wrapping flex row), so a dot
|
||||
* cannot stay behind at the end of a line while its value moves to the
|
||||
* next. The dot sits on the middle of the text line, and a name too long for
|
||||
* the row is cut short inside its unit. `lead` (the "Ø" in front of the
|
||||
* selection bar's means) rides in the first unit for the same reason.
|
||||
*/
|
||||
export function MeanPartsLine({
|
||||
parts,
|
||||
seriesColor,
|
||||
lead,
|
||||
}: {
|
||||
parts: readonly MeanPart[];
|
||||
seriesColor: (key: string) => string;
|
||||
lead?: React.ReactNode;
|
||||
}) {
|
||||
return parts.map((part, i) => (
|
||||
<span
|
||||
@@ -1133,16 +1142,17 @@ export function MeanPartsLine({
|
||||
// The readings behind a mean: a hover title, and the same words for a
|
||||
// screen reader, but not on the line itself.
|
||||
title={part.detail}
|
||||
className={i > 0 ? "ml-3" : undefined}
|
||||
className="inline-flex max-w-full min-w-0 items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
{i === 0 && lead ? <span className="shrink-0">{lead}</span> : null}
|
||||
{i > 0 ? <span className="sr-only">, </span> : null}
|
||||
<span
|
||||
data-slot="timeline-series-dot"
|
||||
className="mr-1 inline-block size-2 rounded-full align-middle"
|
||||
className="size-2 shrink-0 rounded-full"
|
||||
style={{ background: seriesColor(part.key) }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{part.text}
|
||||
<span className="min-w-0 truncate">{part.text}</span>
|
||||
{part.detail ? (
|
||||
<span className="sr-only" data-slot="timeline-mean-detail">
|
||||
{`, ${part.detail}`}
|
||||
@@ -1152,6 +1162,10 @@ export function MeanPartsLine({
|
||||
));
|
||||
}
|
||||
|
||||
/** The row the units of a `MeanPartsLine` wrap in: breaks only between them. */
|
||||
export const MEAN_PARTS_ROW =
|
||||
"text-muted-foreground inline-flex max-w-full flex-wrap items-center gap-x-3 gap-y-1 text-xs tabular-nums";
|
||||
|
||||
/** Inline style for a lane colour dot in HTML (chips, chronicle rails). */
|
||||
export function laneDotStyle(lane: TimelineLaneKey): React.CSSProperties {
|
||||
return { background: LANE_COLOR[lane] };
|
||||
|
||||
@@ -50,7 +50,12 @@ import {
|
||||
formatMonthYear,
|
||||
} from "./timeline-dates";
|
||||
import { LANE_COLOR } from "./timeline-geometry";
|
||||
import { MeanPartsLine, bucketText, laneDotStyle } from "./timeline-chart";
|
||||
import {
|
||||
MEAN_PARTS_ROW,
|
||||
MeanPartsLine,
|
||||
bucketText,
|
||||
laneDotStyle,
|
||||
} from "./timeline-chart";
|
||||
import {
|
||||
chronicleMeans,
|
||||
meanParts,
|
||||
@@ -247,7 +252,7 @@ export function TimelineChronicle({
|
||||
className="bg-muted text-foreground text-2xs inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-medium"
|
||||
>
|
||||
<span
|
||||
className="size-1.5 rounded-full"
|
||||
className="size-1.5 shrink-0 rounded-full"
|
||||
style={laneDotStyle(chip.lane)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -295,7 +300,7 @@ export function TimelineChronicle({
|
||||
</h3>
|
||||
{bucketMeans && parts.length > 0 && (
|
||||
<span
|
||||
className="text-muted-foreground text-xs tabular-nums"
|
||||
className={MEAN_PARTS_ROW}
|
||||
data-slot="timeline-chronicle-means"
|
||||
data-bucket={bucketMeans.start}
|
||||
>
|
||||
|
||||
@@ -603,14 +603,14 @@ export function TimelineLegend({
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className="bg-muted-foreground h-1.5 w-4 rounded-full"
|
||||
className="bg-muted-foreground h-1.5 w-4 shrink-0 rounded-full"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("timeline.legendSpan")}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className="bg-muted-foreground size-2 rounded-full"
|
||||
className="bg-muted-foreground size-2 shrink-0 rounded-full"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("timeline.legendEvent")}
|
||||
@@ -622,7 +622,10 @@ export function TimelineLegend({
|
||||
data-slot="timeline-legend-mean"
|
||||
data-bucket={bucket}
|
||||
>
|
||||
<span className="bg-foreground h-0.5 w-4" aria-hidden="true" />
|
||||
<span
|
||||
className="bg-foreground h-0.5 w-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(TIMELINE_LEGEND_MEAN_KEY[bucket])}
|
||||
</span>
|
||||
<span
|
||||
|
||||
Reference in new issue
Block a user