mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(insights): match the header target gear to the Coach icon size
The target gear painted a heavier 44 px box on mobile against the Coach icon's flat 40 px, so the header cluster read as two mismatched icons. Settle the gear on a flat 40 px box that equals the Coach sparkle on every breakpoint, and restore the 44 px WCAG 2.5.5 touch target through a transparent ::before overlay rather than a larger painted box. The shared icon-button size stays untouched, so other icon affordances are unaffected.
This commit is contained in:
1 parent
252921b563
commit
93bdbd8e0c
2 files changed
+104
-4
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* `<TargetAdjustButton>` unit tests.
|
||||
*
|
||||
* The header gear opens the per-metric target editor. It self-gates on a
|
||||
* registered editable target (`canAdjust`), so a metric with no numeric
|
||||
* band shows nothing. The test convention is `renderToStaticMarkup` (no
|
||||
* `@testing-library/react`), so `useTargetAdjust` is mocked to drive the
|
||||
* gated / ungated branches and the click handler is exercised via the
|
||||
* mocked `requestAdjust` reference.
|
||||
*
|
||||
* The load-bearing visual contract: the gear must read as the *same*
|
||||
* size as the sibling `<CoachLaunchButton variant="icon">` (a flat 40 px
|
||||
* `size-icon` box), with the WCAG 2.5.5 touch target restored through an
|
||||
* invisible `::before` overlay rather than a larger painted box. The
|
||||
* class assertions pin that the gear paints a flat `size-10` (40 px) and
|
||||
* carries the transparent hit-area overlay, and never the old
|
||||
* `size-11`/`sm:size-10` box that made it visually heavier than the
|
||||
* Coach icon.
|
||||
*/
|
||||
|
||||
const useTargetAdjustMock = vi.fn();
|
||||
vi.mock("@/lib/insights/target-adjust-context", () => ({
|
||||
useTargetAdjust: () => useTargetAdjustMock(),
|
||||
}));
|
||||
|
||||
const { TargetAdjustButton } = await import("../target-adjust-button");
|
||||
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<TargetAdjustButton>", () => {
|
||||
it("renders nothing when no provider is mounted", () => {
|
||||
useTargetAdjustMock.mockReturnValue(null);
|
||||
const html = render(<TargetAdjustButton />);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders nothing when no editable target has registered", () => {
|
||||
useTargetAdjustMock.mockReturnValue({
|
||||
canAdjust: false,
|
||||
requestAdjust: vi.fn(),
|
||||
register: vi.fn(),
|
||||
});
|
||||
const html = render(<TargetAdjustButton />);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders the gear once an editable target is registered", () => {
|
||||
useTargetAdjustMock.mockReturnValue({
|
||||
canAdjust: true,
|
||||
requestAdjust: vi.fn(),
|
||||
register: vi.fn(),
|
||||
});
|
||||
const html = render(<TargetAdjustButton />);
|
||||
expect(html).toContain('data-slot="target-adjust-trigger"');
|
||||
// The accessible label reuses the existing adjust-target copy.
|
||||
expect(html).toContain('aria-label="Adjust target range"');
|
||||
});
|
||||
|
||||
it("paints a flat 40px box matching the Coach icon, not the old heavier size-11", () => {
|
||||
useTargetAdjustMock.mockReturnValue({
|
||||
canAdjust: true,
|
||||
requestAdjust: vi.fn(),
|
||||
register: vi.fn(),
|
||||
});
|
||||
const html = render(<TargetAdjustButton />);
|
||||
// Flat 40 px visual box, identical to the sibling Coach sparkle icon.
|
||||
expect(html).toContain("size-10");
|
||||
// The pre-parity heavier mobile box is gone.
|
||||
expect(html).not.toContain("size-11");
|
||||
});
|
||||
|
||||
it("restores the touch target with an invisible hit-area overlay rather than a bigger box", () => {
|
||||
useTargetAdjustMock.mockReturnValue({
|
||||
canAdjust: true,
|
||||
requestAdjust: vi.fn(),
|
||||
register: vi.fn(),
|
||||
});
|
||||
const html = render(<TargetAdjustButton />);
|
||||
// A transparent `::before` overlay extends the hit area past the 40 px
|
||||
// visual box to clear the WCAG 2.5.5 floor without enlarging the paint.
|
||||
expect(html).toContain("before:absolute");
|
||||
expect(html).toContain("before:-inset-1.5");
|
||||
});
|
||||
});
|
||||
@@ -48,10 +48,17 @@ export function TargetAdjustButton({ className }: { className?: string }) {
|
||||
title={label}
|
||||
onClick={() => adjust.requestAdjust()}
|
||||
className={cn(
|
||||
// The `icon` size is 40 px; bump to the 44 px WCAG 2.5.5 floor on
|
||||
// mobile and settle back to the 40 px that matches the sibling
|
||||
// Coach icon from `sm` up, so the cluster reads as one on desktop.
|
||||
"text-muted-foreground hover:text-foreground size-11 sm:size-10",
|
||||
// The `icon` size is a flat 40 px — identical to the sibling Coach
|
||||
// sparkle icon — so the gear + Coach read as one even-weighted icon
|
||||
// cluster top-right of the heading on every breakpoint.
|
||||
//
|
||||
// The 40 px visual box is below the WCAG 2.5.5 (44 px) floor, so a
|
||||
// transparent `::before` overlay extends the *hit* area by 6 px on
|
||||
// every side (40 + 2×6 = 52 px) without enlarging the painted box.
|
||||
// This keeps the touch target comfortable while the gear stays
|
||||
// visually equal to the Coach icon.
|
||||
"text-muted-foreground hover:text-foreground relative size-10",
|
||||
"before:absolute before:-inset-1.5 before:content-['']",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user