diff --git a/.changeset/align-banner-toast-icons.md b/.changeset/align-banner-toast-icons.md new file mode 100644 index 0000000000..1f7bcf0f4b --- /dev/null +++ b/.changeset/align-banner-toast-icons.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": patch +--- + +Align Banner and Toast status icons to the first line of wrapping message text and prevent the icons from shrinking. diff --git a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx index ab14ae6ba8..d93b0a8362 100644 --- a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx @@ -101,6 +101,38 @@ export function BannerWithIconDemo() { ); } +export function BannerIconAlignmentPreview() { + const message = + "A DNS record for puppies.cloudflare.dev already exists in this zone and must be reviewed before continuing."; + + return ( +
+
+

Before

+
+ +

{message}

+
+
+
+

After

+
+ + + +

{message}

+
+
+
+ ); +} + /** Banner with custom React content in description. */ export function BannerCustomContentDemo() { return ( diff --git a/packages/kumo-docs-astro/src/pages/components/banner.mdx b/packages/kumo-docs-astro/src/pages/components/banner.mdx index 2da2ba5b35..2b475acc2b 100644 --- a/packages/kumo-docs-astro/src/pages/components/banner.mdx +++ b/packages/kumo-docs-astro/src/pages/components/banner.mdx @@ -15,7 +15,9 @@ import { BannerAlertDemo, BannerErrorDemo, BannerSecondaryDemo, + BannerTitleOnlyDemo, BannerWithIconDemo, + BannerIconAlignmentPreview, BannerWithActionDemo, BannerWithActionsDemo, BannerCompactDemo, @@ -111,6 +113,29 @@ export default function Example() { +#### Title only + + + + + +#### Icon alignment with wrapping text + +Icons align to the first line of banner text and keep their size when the message wraps. + +} + variant="alert" + description="A DNS record already exists in this zone and must be reviewed before continuing." +/>`} + vrSection="banner-icon-alignment" + vrTitle="Banner Icon Alignment" +> + + + ### With action diff --git a/packages/kumo/src/components/banner/banner.test.tsx b/packages/kumo/src/components/banner/banner.test.tsx index 0faa017a28..7ffece5e6b 100644 --- a/packages/kumo/src/components/banner/banner.test.tsx +++ b/packages/kumo/src/components/banner/banner.test.tsx @@ -139,11 +139,28 @@ describe("Banner", () => { expect(className).toContain("px-3"); expect(className).toContain("py-2"); expect(className).toContain("text-sm"); - // Compact banners align everything on one centered row. - expect(className).toContain("items-center"); + // Compact banners align icons to the first text line when content wraps. + expect(className).toContain("items-start"); // Base-size spacing/alignment must not leak in. expect(className).not.toContain("px-4"); - expect(className).not.toContain("items-start"); + }); + + it("aligns icons to the first text line without shrinking", () => { + render( + } + description="A DNS record already exists in this zone and may wrap onto multiple lines." + />, + ); + + const icon = screen.getByTestId("icon"); + const iconClassName = icon.getAttribute("class") ?? ""; + expect(iconClassName).toContain("size-[1em]"); + expect(iconClassName).toContain("flex-none"); + expect(iconClassName).toContain("custom-icon"); + expect(icon.parentElement?.className).toContain("h-[1lh]"); + expect(icon.parentElement?.className).toContain("flex-none"); }); it("defaults Banner.Action children to xs in an sm banner", () => { diff --git a/packages/kumo/src/components/banner/banner.tsx b/packages/kumo/src/components/banner/banner.tsx index 4e956c7c4f..3866f36d55 100644 --- a/packages/kumo/src/components/banner/banner.tsx +++ b/packages/kumo/src/components/banner/banner.tsx @@ -1,6 +1,8 @@ import { type HTMLAttributes, + type ReactElement, type ReactNode, + cloneElement, forwardRef, isValidElement, } from "react"; @@ -46,7 +48,7 @@ export const KUMO_BANNER_VARIANTS = { description: "Default banner size", }, sm: { - classes: "items-center gap-2 rounded-md px-3 py-2 text-sm", + classes: "items-start gap-2 rounded-md px-3 py-2 text-sm", description: "Compact banner for dialogs and tight spaces", }, }, @@ -63,28 +65,51 @@ export type KumoBannerSize = keyof typeof KUMO_BANNER_VARIANTS.size; /** * Per-size render-site classes not carried by `bannerVariants` (which only emits - * the container classes). `row` is the title↔action flex gap, `icon` the icon - * wrapper height, `description` the description text size, and `action` the size + * the container classes). `row` is the title↔action flex gap, + * `description` the description text size, and `action` the size * that child `Banner.Action`s inherit via {@link BannerActionContext}. */ const BANNER_SIZE_PARTS: Record< KumoBannerSize, - { row: string; icon: string; description: string; action: BannerActionSize } + { row: string; description: string; action: BannerActionSize } > = { base: { row: "gap-3", - icon: "h-[1.375em]", description: "text-sm", action: "sm", }, sm: { row: "gap-2", - icon: "h-[1.25em]", description: "text-sm", action: "xs", }, }; +const renderBannerIcon = (icon: ReactNode, className?: string) => { + const iconElement = isValidElement(icon) + ? (icon as ReactElement<{ className?: string }>) + : null; + const alignedIcon = iconElement + ? cloneElement(iconElement, { + className: cn( + iconElement.props.className, + "size-[1em] flex-none leading-snug", + ), + }) + : icon; + + return ( + + {alignedIcon} + + ); +}; + // The `Banner.Action` CTA compound lives in ./banner-action // and is attached to `Banner` via Object.assign at the bottom of this file. export type { @@ -253,17 +278,7 @@ const BannerRoot = forwardRef(function BannerRoot( className={cn(bannerVariants({ variant, size }), className)} {...props} > - {icon && ( - - {icon} - - )} + {icon && renderBannerIcon(icon, variantConfig.iconClasses)}
(function BannerRoot( className={cn(bannerVariants({ variant, size }), className)} {...props} > - {icon && ( - - {icon} - - )} + {icon && renderBannerIcon(icon, variantConfig.iconClasses)} {content}
diff --git a/packages/kumo/src/components/toast/toast.tsx b/packages/kumo/src/components/toast/toast.tsx index 29e1b2f5ad..d5041161fe 100644 --- a/packages/kumo/src/components/toast/toast.tsx +++ b/packages/kumo/src/components/toast/toast.tsx @@ -443,6 +443,8 @@ function ToastIcon({ variant }: { variant?: KumoToastVariant }) { if (!("icon" in variantConfig)) return null; const Icon = variantConfig.icon; return ( - + + + ); }