mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
refactor(anamnesis): give each lifestyle entry a proper action row
The three entries stacked two full-width buttons under the value — a disabled grey Save above a red-outlined "Remove smoking status" — so each one read as a slab of controls with the same weight as the field it belonged to, and scanning them ran the two labels together. Each entry now ends in one right-aligned action row: the quiet destructive first, the primary Save last, both at the mobile tap-target floor. The remove control drops the field name from its visible label, which the Label directly above already carries, and keeps the full phrase as its accessible name and in the dialog it opens — so three of them side by side stay distinguishable to a screen reader. Six languages. Follows the card action-row convention: primary bottom right, secondary beside it, nothing after the row.
This commit is contained in:
1 parent
d7a95874c2
commit
41faf165ee
8 files changed
+52
-24
No files matched your search
@@ -114,7 +114,12 @@ describe("health-profile fact removal control", () => {
|
||||
.filter(
|
||||
(props) =>
|
||||
props.type === "button" &&
|
||||
props.variant === "outline" &&
|
||||
// The Save control is the entry's PRIMARY action, so it carries the
|
||||
// default variant. Matching on `variant === "outline"` used to be the
|
||||
// discriminator and silently stopped matching anything when the
|
||||
// action row was rebuilt — the button was still there, the selector
|
||||
// was not.
|
||||
props.variant === undefined &&
|
||||
typeof props.onClick === "function",
|
||||
)
|
||||
.map((props) => {
|
||||
@@ -132,12 +137,18 @@ describe("health-profile fact removal control", () => {
|
||||
const { html } = render();
|
||||
|
||||
expect(confirmButtons).toHaveLength(1);
|
||||
// The visible label is short — three entries side by side each carrying
|
||||
// their own field name read as one run of words — but the ACCESSIBLE name
|
||||
// still says which entry it removes, and so does the dialog it opens.
|
||||
expect(confirmButtons[0]).toMatchObject({
|
||||
slot: "health-profile-fact-remove-SMOKING_STATUS",
|
||||
label: "Remove Smoking status",
|
||||
label: "Remove",
|
||||
ariaLabel: "Remove Smoking status",
|
||||
confirmLabel: "Remove Smoking status",
|
||||
title: "Remove Smoking status?",
|
||||
body: "The current value will become not recorded. Its dated history will be retained.",
|
||||
});
|
||||
expect(html).toContain("Remove Smoking status");
|
||||
expect(html).toContain("Remove");
|
||||
expect(apiDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -173,7 +184,7 @@ describe("health-profile fact removal control", () => {
|
||||
const saveButton = renderedButtons.find(
|
||||
(props) =>
|
||||
props.type === "button" &&
|
||||
props.variant === "outline" &&
|
||||
props.variant === undefined &&
|
||||
typeof props.onClick === "function",
|
||||
);
|
||||
|
||||
@@ -207,7 +218,7 @@ describe("health-profile fact removal control", () => {
|
||||
const saveButton = renderedButtons.find(
|
||||
(props) =>
|
||||
props.type === "button" &&
|
||||
props.variant === "outline" &&
|
||||
props.variant === undefined &&
|
||||
typeof props.onClick === "function",
|
||||
);
|
||||
|
||||
|
||||
@@ -228,26 +228,21 @@ export function HealthProfileFactsManager() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 w-full sm:min-h-9"
|
||||
disabled={disabled || !dirty || !value}
|
||||
onClick={() => save.mutate({ kind, value, current })}
|
||||
>
|
||||
{save.isPending && save.variables?.kind === kind && (
|
||||
<Loader2
|
||||
className="size-4 animate-spin motion-reduce:animate-none"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{t("records.profileFacts.save")}
|
||||
</Button>
|
||||
{/* One action row per entry (UI-STANDARDS §11 + the action-row
|
||||
rule): right-aligned, primary last, the quiet destructive
|
||||
beside it. Two full-width stacked blocks — a grey disabled
|
||||
slab over a red-outlined one — carried the same visual weight
|
||||
as the field they belong to and ran into each other when
|
||||
scanned, which is what "Save" directly above "Remove smoking
|
||||
status" read as. */}
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
{current ? (
|
||||
<ConfirmButton
|
||||
label={t("records.profileFacts.remove", {
|
||||
// Short label inside the row; the full phrase stays the
|
||||
// accessible name, because "Remove" on its own is
|
||||
// ambiguous once three of them are on screen.
|
||||
label={t("records.profileFacts.removeShort")}
|
||||
ariaLabel={t("records.profileFacts.remove", {
|
||||
kind: t(KIND_LABEL[kind]),
|
||||
})}
|
||||
title={t("records.profileFacts.removeConfirmTitle", {
|
||||
@@ -262,12 +257,28 @@ export function HealthProfileFactsManager() {
|
||||
remove.isPending && remove.variables?.id === current.id
|
||||
}
|
||||
disabled={disabled}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive min-h-11 w-full sm:min-h-9"
|
||||
className="text-destructive min-h-11 sm:min-h-9"
|
||||
icon={<Trash2 className="size-4" aria-hidden />}
|
||||
slot={`health-profile-fact-remove-${kind}`}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={disabled || !dirty || !value}
|
||||
onClick={() => save.mutate({ kind, value, current })}
|
||||
>
|
||||
{save.isPending && save.variables?.kind === kind && (
|
||||
<Loader2
|
||||
className="size-4 animate-spin motion-reduce:animate-none"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{t("records.profileFacts.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user