From 7b4f0daa22aada15e47691a35cc4a7d9fa2d78ba Mon Sep 17 00:00:00 2001 From: Devadakene Date: Tue, 28 Jul 2026 20:31:33 +0100 Subject: [PATCH] design: add bottom-nav badge scheme --- .../components/navigation/role-nav.test.ts | 33 +++++++++++++++++++ src/app/components/navigation/role-nav.ts | 23 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/app/components/navigation/role-nav.test.ts diff --git a/src/app/components/navigation/role-nav.test.ts b/src/app/components/navigation/role-nav.test.ts new file mode 100644 index 0000000..d7ed7cd --- /dev/null +++ b/src/app/components/navigation/role-nav.test.ts @@ -0,0 +1,33 @@ +import { formatBadgeCount, getBadgeAriaLabel } from "./role-nav"; + +describe("role-nav badge scheme", () => { + describe("formatBadgeCount", () => { + it("returns string representation of counts under 100", () => { + expect(formatBadgeCount(0)).toBe("0"); + expect(formatBadgeCount(5)).toBe("5"); + expect(formatBadgeCount(99)).toBe("99"); + }); + + it("caps counts greater than 99 at '99+'", () => { + expect(formatBadgeCount(100)).toBe("99+"); + expect(formatBadgeCount(1000)).toBe("99+"); + expect(formatBadgeCount(9999)).toBe("99+"); + }); + }); + + describe("getBadgeAriaLabel", () => { + it("announces dot badge correctly", () => { + expect(getBadgeAriaLabel({ type: "dot" })).toBe("New updates available"); + }); + + it("announces singular count correctly", () => { + expect(getBadgeAriaLabel({ type: "count", value: 1 })).toBe("1 new update"); + }); + + it("announces plural counts correctly", () => { + expect(getBadgeAriaLabel({ type: "count", value: 5 })).toBe("5 new updates"); + expect(getBadgeAriaLabel({ type: "count", value: 100 })).toBe("100 new updates"); + expect(getBadgeAriaLabel({ type: "count", value: 9999 })).toBe("9999 new updates"); + }); + }); +}); diff --git a/src/app/components/navigation/role-nav.ts b/src/app/components/navigation/role-nav.ts index 5bc304d..79924fa 100644 --- a/src/app/components/navigation/role-nav.ts +++ b/src/app/components/navigation/role-nav.ts @@ -22,6 +22,10 @@ export const ALL_ROLES: UserRole[] = ["supplier", "buyer", "admin"]; // ─── Nav item shape ─────────────────────────────────────────────────────────── +export type NavBadge = + | { type: "dot" } + | { type: "count"; value: number }; + export interface NavItem { /** Route href */ href: string; @@ -35,6 +39,25 @@ export interface NavItem { icon: string; /** aria-label override for screen readers when label alone is ambiguous */ ariaLabel?: string; + /** Optional badge indicator (dot or count) */ + badge?: NavBadge; +} + +/** + * Formats a badge count for visual display, capping at "99+". + */ +export function formatBadgeCount(value: number): string { + return value > 99 ? "99+" : value.toString(); +} + +/** + * Generates an accessible screen reader announcement for the badge. + */ +export function getBadgeAriaLabel(badge: NavBadge): string { + if (badge.type === "dot") { + return "New updates available"; + } + return `${badge.value} new ${badge.value === 1 ? "update" : "updates"}`; } // ─── Shared primitives (common to every role) ─────────────────────────────────