From 610def0cda14d18270e16c33913a735d0ef557be Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 13:42:45 +0200 Subject: [PATCH 1/7] Restrict Banner custom visual types --- .changeset/banner-leading-visual-types.md | 5 ++ packages/react/src/Banner/Banner.figma.tsx | 38 +++++++---- packages/react/src/Banner/Banner.stories.tsx | 65 +++++++++++++------ packages/react/src/Banner/Banner.test.tsx | 6 ++ packages/react/src/Banner/Banner.tsx | 46 +++++++++---- .../react/src/Banner/Banner.types.test.tsx | 37 +++++++++++ 6 files changed, 153 insertions(+), 44 deletions(-) create mode 100644 .changeset/banner-leading-visual-types.md create mode 100644 packages/react/src/Banner/Banner.types.test.tsx diff --git a/.changeset/banner-leading-visual-types.md b/.changeset/banner-leading-visual-types.md new file mode 100644 index 00000000000..2cbf00dadba --- /dev/null +++ b/.changeset/banner-leading-visual-types.md @@ -0,0 +1,5 @@ +--- +'@primer/react': major +--- + +Banner: Restrict custom leading visuals to the info and upsell variants \ No newline at end of file diff --git a/packages/react/src/Banner/Banner.figma.tsx b/packages/react/src/Banner/Banner.figma.tsx index bc06283cdd3..f850151064e 100644 --- a/packages/react/src/Banner/Banner.figma.tsx +++ b/packages/react/src/Banner/Banner.figma.tsx @@ -35,16 +35,30 @@ const componentProps = { figma.connect(Banner, 'https://www.figma.com/design/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=34303-2712&m=dev', { props: componentProps, - example: ({dismissible, variant, icon, secondaryAction, primaryAction, description, title, hideTitle}) => ( - - ), + example: ({dismissible, variant, icon, secondaryAction, primaryAction, description, title, hideTitle}) => { + const supportsCustomVisual = variant === 'info' || variant === 'upsell' + + return supportsCustomVisual ? ( + + ) : ( + + ) + }, }) diff --git a/packages/react/src/Banner/Banner.stories.tsx b/packages/react/src/Banner/Banner.stories.tsx index 986e5d5ee5f..aa3f6b7f259 100644 --- a/packages/react/src/Banner/Banner.stories.tsx +++ b/packages/react/src/Banner/Banner.stories.tsx @@ -41,33 +41,58 @@ export const Playground: StoryObj = { render: ({onDismiss, primaryAction, secondaryAction, leadingVisual, ...rest}) => { // Map the string selection to the actual icon component const leadingVisualElement = leadingVisual && iconMap[leadingVisual as keyof typeof iconMap] + const supportsCustomVisual = rest.variant === undefined || rest.variant === 'info' || rest.variant === 'upsell' return ( - {primaryAction} : null} - secondaryAction={ - secondaryAction ? {secondaryAction} : null - } - leadingVisual={leadingVisualElement} - {...rest} - /> + {supportsCustomVisual ? ( + {primaryAction} : null} + secondaryAction={ + secondaryAction ? {secondaryAction} : null + } + leadingVisual={leadingVisualElement} + {...rest} + /> + ) : ( + {primaryAction} : null} + secondaryAction={ + secondaryAction ? {secondaryAction} : null + } + {...rest} + /> + )} - {primaryAction} : null} - secondaryAction={ - secondaryAction ? {secondaryAction} : null - } - leadingVisual={leadingVisualElement} - {...rest} - /> + {supportsCustomVisual ? ( + {primaryAction} : null} + secondaryAction={ + secondaryAction ? {secondaryAction} : null + } + leadingVisual={leadingVisualElement} + {...rest} + /> + ) : ( + {primaryAction} : null} + secondaryAction={ + secondaryAction ? {secondaryAction} : null + } + {...rest} + /> + )} ) diff --git a/packages/react/src/Banner/Banner.test.tsx b/packages/react/src/Banner/Banner.test.tsx index 73d771ad36a..de2a76c0547 100644 --- a/packages/react/src/Banner/Banner.test.tsx +++ b/packages/react/src/Banner/Banner.test.tsx @@ -238,12 +238,15 @@ describe('Banner', () => { rerender(} />) expect(screen.getByTestId('icon')).toBeInTheDocument() + // @ts-expect-error unsupported variant used to verify runtime fallback behavior rerender(} />) expect(screen.queryByTestId('icon')).toBe(null) + // @ts-expect-error unsupported variant used to verify runtime fallback behavior rerender(} />) expect(screen.queryByTestId('icon')).toBe(null) + // @ts-expect-error unsupported variant used to verify runtime fallback behavior rerender(} />) expect(screen.queryByTestId('icon')).toBe(null) }) @@ -258,12 +261,15 @@ describe('Banner', () => { rerender(} />) expect(screen.getByTestId('leading-visual')).toBeInTheDocument() + // @ts-expect-error unsupported variant used to verify runtime fallback behavior rerender(} />) expect(screen.queryByTestId('leading-visual')).toBe(null) + // @ts-expect-error unsupported variant used to verify runtime fallback behavior rerender(} />) expect(screen.queryByTestId('leading-visual')).toBe(null) + // @ts-expect-error unsupported variant used to verify runtime fallback behavior rerender(} />) expect(screen.queryByTestId('leading-visual')).toBe(null) }) diff --git a/packages/react/src/Banner/Banner.tsx b/packages/react/src/Banner/Banner.tsx index 0fb19589f75..1f2f8bf2ad4 100644 --- a/packages/react/src/Banner/Banner.tsx +++ b/packages/react/src/Banner/Banner.tsx @@ -11,13 +11,16 @@ import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../uti export type BannerVariant = 'critical' | 'info' | 'success' | 'upsell' | 'warning' +type BannerVariantWithCustomVisual = 'info' | 'upsell' +type BannerVariantWithDefaultVisual = Exclude + type BannerContextValue = { titleId: string } const BannerContext = React.createContext(undefined) -export type BannerProps = React.ComponentPropsWithoutRef<'section'> & { +interface BannerBaseProps extends Omit, 'title'> { /** * Provide an optional label to override the default name for the Banner * landmark region @@ -41,17 +44,6 @@ export type BannerProps = React.ComponentPropsWithoutRef<'section'> & { */ hideTitle?: boolean - /** - * Provide a custom icon for the Banner. This is only available when `variant` is `info` or `upsell` - * @deprecated Use `leadingVisual` instead - */ - icon?: React.ReactNode - - /** - * Provide a custom leading visual for the Banner. This is only available when `variant` is `info` or `upsell` - */ - leadingVisual?: React.ReactNode - /** * Optionally provide a handler to be called when the banner is dismissed. * Providing this prop will show a dismiss button. @@ -95,6 +87,36 @@ export type BannerProps = React.ComponentPropsWithoutRef<'section'> & { flush?: boolean } +type BannerCustomVisualProps = { + /** + * Provide a custom icon for the Banner. This is only available when `variant` is `info` or `upsell` + * @deprecated Use `leadingVisual` instead + */ + icon?: React.ReactNode + + /** + * Provide a custom leading visual for the Banner. This is only available when `variant` is `info` or `upsell` + */ + leadingVisual?: React.ReactNode + + /** + * Specify the type of the Banner + */ + variant?: BannerVariantWithCustomVisual +} + +type BannerDefaultVisualProps = { + icon?: never + leadingVisual?: never + + /** + * Specify the type of the Banner + */ + variant: BannerVariantWithDefaultVisual +} + +export type BannerProps = BannerBaseProps & (BannerCustomVisualProps | BannerDefaultVisualProps) + const iconForVariant: Record = { critical: , info: , diff --git a/packages/react/src/Banner/Banner.types.test.tsx b/packages/react/src/Banner/Banner.types.test.tsx new file mode 100644 index 00000000000..0c4172a2c53 --- /dev/null +++ b/packages/react/src/Banner/Banner.types.test.tsx @@ -0,0 +1,37 @@ +import {Banner} from '../Banner' + +export function supportsLeadingVisualForInfoAndUpsellVariants() { + return ( + <> + } /> + } /> + } /> + + ) +} + +export function doesNotSupportLeadingVisualForDefaultVisualVariants() { + return ( + <> + {/* @ts-expect-error leadingVisual is only supported for info and upsell variants */} + } /> + {/* @ts-expect-error leadingVisual is only supported for info and upsell variants */} + } /> + {/* @ts-expect-error leadingVisual is only supported for info and upsell variants */} + } /> + + ) +} + +export function doesNotSupportIconForDefaultVisualVariants() { + return ( + <> + {/* @ts-expect-error icon is only supported for info and upsell variants */} + } /> + {/* @ts-expect-error icon is only supported for info and upsell variants */} + } /> + {/* @ts-expect-error icon is only supported for info and upsell variants */} + } /> + + ) +} From d2c750b210ee49588a8a49028840ae0539b26739 Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 13:50:20 +0200 Subject: [PATCH 2/7] Remove Banner types test --- .../react/src/Banner/Banner.types.test.tsx | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 packages/react/src/Banner/Banner.types.test.tsx diff --git a/packages/react/src/Banner/Banner.types.test.tsx b/packages/react/src/Banner/Banner.types.test.tsx deleted file mode 100644 index 0c4172a2c53..00000000000 --- a/packages/react/src/Banner/Banner.types.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import {Banner} from '../Banner' - -export function supportsLeadingVisualForInfoAndUpsellVariants() { - return ( - <> - } /> - } /> - } /> - - ) -} - -export function doesNotSupportLeadingVisualForDefaultVisualVariants() { - return ( - <> - {/* @ts-expect-error leadingVisual is only supported for info and upsell variants */} - } /> - {/* @ts-expect-error leadingVisual is only supported for info and upsell variants */} - } /> - {/* @ts-expect-error leadingVisual is only supported for info and upsell variants */} - } /> - - ) -} - -export function doesNotSupportIconForDefaultVisualVariants() { - return ( - <> - {/* @ts-expect-error icon is only supported for info and upsell variants */} - } /> - {/* @ts-expect-error icon is only supported for info and upsell variants */} - } /> - {/* @ts-expect-error icon is only supported for info and upsell variants */} - } /> - - ) -} From 09595f478c6a5228583f3b7b1e41e32ced0866e4 Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 13:55:23 +0200 Subject: [PATCH 3/7] Simplify Banner Playground story --- packages/react/src/Banner/Banner.stories.tsx | 54 ++++---------------- 1 file changed, 10 insertions(+), 44 deletions(-) diff --git a/packages/react/src/Banner/Banner.stories.tsx b/packages/react/src/Banner/Banner.stories.tsx index aa3f6b7f259..5a00a486663 100644 --- a/packages/react/src/Banner/Banner.stories.tsx +++ b/packages/react/src/Banner/Banner.stories.tsx @@ -41,58 +41,24 @@ export const Playground: StoryObj = { render: ({onDismiss, primaryAction, secondaryAction, leadingVisual, ...rest}) => { // Map the string selection to the actual icon component const leadingVisualElement = leadingVisual && iconMap[leadingVisual as keyof typeof iconMap] + // leadingVisual is only supported for the info and upsell variants const supportsCustomVisual = rest.variant === undefined || rest.variant === 'info' || rest.variant === 'upsell' + const sharedProps = { + onDismiss: onDismiss ? action('onDismiss') : undefined, + primaryAction: primaryAction ? {primaryAction} : null, + secondaryAction: secondaryAction ? {secondaryAction} : null, + ...(supportsCustomVisual ? {leadingVisual: leadingVisualElement} : {}), + ...rest, + } as React.ComponentProps return ( - {supportsCustomVisual ? ( - {primaryAction} : null} - secondaryAction={ - secondaryAction ? {secondaryAction} : null - } - leadingVisual={leadingVisualElement} - {...rest} - /> - ) : ( - {primaryAction} : null} - secondaryAction={ - secondaryAction ? {secondaryAction} : null - } - {...rest} - /> - )} + - {supportsCustomVisual ? ( - {primaryAction} : null} - secondaryAction={ - secondaryAction ? {secondaryAction} : null - } - leadingVisual={leadingVisualElement} - {...rest} - /> - ) : ( - {primaryAction} : null} - secondaryAction={ - secondaryAction ? {secondaryAction} : null - } - {...rest} - /> - )} + ) From 5554c320447911a26822a61c353f117db81716ee Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 14:14:03 +0200 Subject: [PATCH 4/7] Refine Banner visual prop types --- packages/react/src/Banner/Banner.stories.tsx | 31 ++++++---- packages/react/src/Banner/Banner.tsx | 60 +++++++++----------- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/packages/react/src/Banner/Banner.stories.tsx b/packages/react/src/Banner/Banner.stories.tsx index 5a00a486663..986e5d5ee5f 100644 --- a/packages/react/src/Banner/Banner.stories.tsx +++ b/packages/react/src/Banner/Banner.stories.tsx @@ -41,24 +41,33 @@ export const Playground: StoryObj = { render: ({onDismiss, primaryAction, secondaryAction, leadingVisual, ...rest}) => { // Map the string selection to the actual icon component const leadingVisualElement = leadingVisual && iconMap[leadingVisual as keyof typeof iconMap] - // leadingVisual is only supported for the info and upsell variants - const supportsCustomVisual = rest.variant === undefined || rest.variant === 'info' || rest.variant === 'upsell' - const sharedProps = { - onDismiss: onDismiss ? action('onDismiss') : undefined, - primaryAction: primaryAction ? {primaryAction} : null, - secondaryAction: secondaryAction ? {secondaryAction} : null, - ...(supportsCustomVisual ? {leadingVisual: leadingVisualElement} : {}), - ...rest, - } as React.ComponentProps return ( - + {primaryAction} : null} + secondaryAction={ + secondaryAction ? {secondaryAction} : null + } + leadingVisual={leadingVisualElement} + {...rest} + /> - + {primaryAction} : null} + secondaryAction={ + secondaryAction ? {secondaryAction} : null + } + leadingVisual={leadingVisualElement} + {...rest} + /> ) diff --git a/packages/react/src/Banner/Banner.tsx b/packages/react/src/Banner/Banner.tsx index 1f2f8bf2ad4..57b282bc0a8 100644 --- a/packages/react/src/Banner/Banner.tsx +++ b/packages/react/src/Banner/Banner.tsx @@ -10,9 +10,7 @@ import classes from './Banner.module.css' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' export type BannerVariant = 'critical' | 'info' | 'success' | 'upsell' | 'warning' - -type BannerVariantWithCustomVisual = 'info' | 'upsell' -type BannerVariantWithDefaultVisual = Exclude +type BannerVariantsWithCustomVisual = 'info' | 'upsell' type BannerContextValue = { titleId: string @@ -87,37 +85,33 @@ interface BannerBaseProps extends Omit flush?: boolean } -type BannerCustomVisualProps = { - /** - * Provide a custom icon for the Banner. This is only available when `variant` is `info` or `upsell` - * @deprecated Use `leadingVisual` instead - */ - icon?: React.ReactNode - - /** - * Provide a custom leading visual for the Banner. This is only available when `variant` is `info` or `upsell` - */ - leadingVisual?: React.ReactNode - - /** - * Specify the type of the Banner - */ - variant?: BannerVariantWithCustomVisual -} - -type BannerDefaultVisualProps = { - icon?: never - leadingVisual?: never - - /** - * Specify the type of the Banner - */ - variant: BannerVariantWithDefaultVisual -} +type LeadingVisualProps = + | { + /** + * Specify the type of the Banner + */ + variant?: BannerVariantsWithCustomVisual + + /** + * Provide a custom leading visual for the Banner. This is only available when `variant` is `info` or `upsell` + */ + leadingVisual?: React.ReactNode + + /** + * Provide a custom icon for the Banner. This is only available when `variant` is `info` or `upsell` + * @deprecated Use `leadingVisual` instead + */ + icon?: React.ReactNode + } + | { + variant: Exclude + icon?: never + leadingVisual?: never + } -export type BannerProps = BannerBaseProps & (BannerCustomVisualProps | BannerDefaultVisualProps) +export type BannerProps = BannerBaseProps & LeadingVisualProps -const iconForVariant: Record = { +const defaultIconForVariant: Record = { critical: , info: , success: , @@ -192,7 +186,7 @@ export const Banner = React.forwardRef(function Banner data-flush={flush ? '' : undefined} >
- {visual && supportsCustomIcon ? visual : iconForVariant[variant]} + {visual && supportsCustomIcon ? visual : defaultIconForVariant[variant]}
From 585358b78f0b46d47c7951a0f878b772a2e11c26 Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 14:42:17 +0200 Subject: [PATCH 5/7] Narrow Banner Playground variant for custom visuals --- packages/react/src/Banner/Banner.stories.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/react/src/Banner/Banner.stories.tsx b/packages/react/src/Banner/Banner.stories.tsx index 986e5d5ee5f..d7db79a8c4a 100644 --- a/packages/react/src/Banner/Banner.stories.tsx +++ b/packages/react/src/Banner/Banner.stories.tsx @@ -38,9 +38,19 @@ const iconMap = { } export const Playground: StoryObj = { - render: ({onDismiss, primaryAction, secondaryAction, leadingVisual, ...rest}) => { - // Map the string selection to the actual icon component - const leadingVisualElement = leadingVisual && iconMap[leadingVisual as keyof typeof iconMap] + render: ({ + onDismiss, + primaryAction, + secondaryAction, + leadingVisual, + variant, + icon: _deprecatedIconIgnored, + ...rest + }) => { + const variantAndVisualProps = + variant === 'info' || variant === 'upsell' + ? {variant, leadingVisual: iconMap[leadingVisual as keyof typeof iconMap]} + : {variant} return ( @@ -52,7 +62,7 @@ export const Playground: StoryObj = { secondaryAction={ secondaryAction ? {secondaryAction} : null } - leadingVisual={leadingVisualElement} + {...variantAndVisualProps} {...rest} /> @@ -65,7 +75,7 @@ export const Playground: StoryObj = { secondaryAction={ secondaryAction ? {secondaryAction} : null } - leadingVisual={leadingVisualElement} + {...variantAndVisualProps} {...rest} /> From fef88faf51e032e228d6fe948f8fd533c130599b Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 14:42:44 +0200 Subject: [PATCH 6/7] Clean up Banner visual prop type definition --- packages/react/src/Banner/Banner.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/react/src/Banner/Banner.tsx b/packages/react/src/Banner/Banner.tsx index 57b282bc0a8..4f73f2fa454 100644 --- a/packages/react/src/Banner/Banner.tsx +++ b/packages/react/src/Banner/Banner.tsx @@ -64,11 +64,6 @@ interface BannerBaseProps extends Omit */ title?: React.ReactNode - /** - * Specify the type of the Banner - */ - variant?: BannerVariant - /** * Specify the layout of the Banner. Compact layout will reduce the padding. */ @@ -85,10 +80,10 @@ interface BannerBaseProps extends Omit flush?: boolean } -type LeadingVisualProps = +type VariantAndLeadingVisualProps = | { /** - * Specify the type of the Banner + * Specify the type of the Banner, default = info */ variant?: BannerVariantsWithCustomVisual @@ -109,7 +104,7 @@ type LeadingVisualProps = leadingVisual?: never } -export type BannerProps = BannerBaseProps & LeadingVisualProps +export type BannerProps = BannerBaseProps & VariantAndLeadingVisualProps const defaultIconForVariant: Record = { critical: , From 9f72c7981f176d54950525089a177c876d31b619 Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 13 Jul 2026 14:43:50 +0200 Subject: [PATCH 7/7] Simplify Banner Code Connect example --- packages/react/src/Banner/Banner.figma.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/react/src/Banner/Banner.figma.tsx b/packages/react/src/Banner/Banner.figma.tsx index f850151064e..e91a20af1eb 100644 --- a/packages/react/src/Banner/Banner.figma.tsx +++ b/packages/react/src/Banner/Banner.figma.tsx @@ -36,28 +36,18 @@ const componentProps = { figma.connect(Banner, 'https://www.figma.com/design/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=34303-2712&m=dev', { props: componentProps, example: ({dismissible, variant, icon, secondaryAction, primaryAction, description, title, hideTitle}) => { - const supportsCustomVisual = variant === 'info' || variant === 'upsell' + // icon is only supported for the info and upsell variants + const variantAndVisualProps = variant === 'info' || variant === 'upsell' ? {variant, icon} : {variant} - return supportsCustomVisual ? ( + return ( - ) : ( - ) },