fix(medications): stop the wizard showing each time-of-day suggestion twice

The schedule wizard's time-of-day step rendered the four suggested times
twice: once as the icon chips from the step itself, and again as the
labelled preset row built into the chip component below. Add an opt-out
on the chip component and pass it from the step so only the icon-based
suggestions remain. The period is still conveyed by the icon plus an
aria-label; the offered times and selection logic are unchanged. The
preset row stays on by default for every other caller.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 19:37:58 +02:00
1 parent ddb08b048a
commit d660279c97
3 files changed
+65 -33

No files matched your search

@@ -46,6 +46,13 @@ export interface TimesOfDayChipsProps {
disabled?: boolean;
/** Translation namespace prefix. */
i18nPrefix?: string;
/**
* Render the built-in Morning / Noon / Evening / Night preset row.
* Default `true`. The wizard's Step 7 supplies its own icon-based
* preset row, so it passes `false` to avoid showing each suggested
* time twice (the icon chip from Step 7 and the labelled chip here).
*/
showPresets?: boolean;
}
/** Preset table — wall-clock HH:mm and i18n suffix key. */
@@ -106,6 +113,7 @@ export function TimesOfDayChips({
maxChips = 8,
disabled = false,
i18nPrefix = "medications.scheduling.timesOfDay",
showPresets = true,
}: TimesOfDayChipsProps) {
const { t } = useTranslations();
const inputId = useId();
@@ -165,38 +173,40 @@ export function TimesOfDayChips({
aria-label={t(`${i18nPrefix}.label`)}
>
{/* Preset row */}
<div
role="group"
aria-label={t(`${i18nPrefix}.label`)}
className="flex flex-wrap gap-1.5"
data-slot="times-of-day-presets"
>
{PRESETS.map(({ key, value: preset }) => {
const isOn = sortedValue.includes(preset);
const label = `${t(`${i18nPrefix}.presets.${key}`)} ${preset}`;
return (
<button
key={key}
type="button"
disabled={disabled || (!isOn && atCap)}
aria-pressed={isOn}
aria-label={label}
data-slot="times-of-day-preset"
data-preset={key}
data-active={isOn ? "true" : "false"}
onClick={() => onPresetToggle(preset)}
className={[
"focus-visible:ring-ring inline-flex h-11 items-center gap-1 rounded-md border px-3 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50",
isOn
? "bg-primary text-primary-foreground border-primary"
: "border-border bg-background text-foreground hover:bg-muted",
].join(" ")}
>
{label}
</button>
);
})}
</div>
{showPresets && (
<div
role="group"
aria-label={t(`${i18nPrefix}.label`)}
className="flex flex-wrap gap-1.5"
data-slot="times-of-day-presets"
>
{PRESETS.map(({ key, value: preset }) => {
const isOn = sortedValue.includes(preset);
const label = `${t(`${i18nPrefix}.presets.${key}`)} ${preset}`;
return (
<button
key={key}
type="button"
disabled={disabled || (!isOn && atCap)}
aria-pressed={isOn}
aria-label={label}
data-slot="times-of-day-preset"
data-preset={key}
data-active={isOn ? "true" : "false"}
onClick={() => onPresetToggle(preset)}
className={[
"focus-visible:ring-ring inline-flex h-11 items-center gap-1 rounded-md border px-3 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50",
isOn
? "bg-primary text-primary-foreground border-primary"
: "border-border bg-background text-foreground hover:bg-muted",
].join(" ")}
>
{label}
</button>
);
})}
</div>
)}
{/* Chip list */}
{sortedValue.length === 0 ? (
@@ -225,7 +235,7 @@ export function TimesOfDayChips({
disabled={disabled}
aria-label={`${t(`${i18nPrefix}.remove`)} ${time}`}
onClick={() => onRemove(time)}
className="focus-visible:ring-ring inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-background focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50"
className="focus-visible:ring-ring hover:bg-background inline-flex h-7 w-7 items-center justify-center rounded-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
data-slot="times-of-day-chip-remove"
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
@@ -143,4 +143,22 @@ describe("<TimesOfDayChips> — SSR render", () => {
expect(html).not.toContain('data-slot="times-of-day-presets"');
expect(html).toMatch(/value="09:30"/);
});
it("suppresses the built-in preset row when showPresets is false", () => {
// The wizard's Step 7 supplies its own icon-based preset row; passing
// showPresets={false} keeps each suggested time from rendering twice.
const html = render(
<TimesOfDayChips value={["08:00"]} showPresets={false} onChange={NOOP} />,
);
expect(html).not.toContain('data-slot="times-of-day-presets"');
// The chip list (the actual selected values) still renders.
expect(html).toContain('data-slot="times-of-day-list"');
});
it("renders the preset row by default (showPresets defaults true)", () => {
const html = render(
<TimesOfDayChips value={["08:00"]} onChange={NOOP} />,
);
expect(html).toContain('data-slot="times-of-day-presets"');
});
});
@@ -77,6 +77,10 @@ export function Step7Times({ payload, applyPartial }: StepProps) {
value={payload.timesOfDay}
onChange={(timesOfDay) => applyPartial({ timesOfDay })}
maxChips={8}
// The icon-based preset row above already offers the four
// suggested times; suppress the chips' own labelled preset row so
// each suggestion isn't shown twice.
showPresets={false}
/>
</div>
);