Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/banner-leading-visual-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': major
---

Banner: Restrict custom leading visuals to the info and upsell variants
Comment on lines +1 to +5
28 changes: 16 additions & 12 deletions packages/react/src/Banner/Banner.figma.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,20 @@ 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}) => (
<Banner
hideTitle={hideTitle}
title={title}
description={description}
icon={icon}
variant={variant}
onDismiss={dismissible}
primaryAction={primaryAction}
secondaryAction={secondaryAction}
/>
),
example: ({dismissible, variant, icon, secondaryAction, primaryAction, description, title, hideTitle}) => {
// icon is only supported for the info and upsell variants
const variantAndVisualProps = variant === 'info' || variant === 'upsell' ? {variant, icon} : {variant}

return (
<Banner
hideTitle={hideTitle}
title={title}
description={description}
onDismiss={dismissible}
primaryAction={primaryAction}
secondaryAction={secondaryAction}
{...variantAndVisualProps}
/>
)
},
})
20 changes: 15 additions & 5 deletions packages/react/src/Banner/Banner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,19 @@ const iconMap = {
}

export const Playground: StoryObj<typeof Banner> = {
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 (
<PageLayout>
Expand All @@ -52,7 +62,7 @@ export const Playground: StoryObj<typeof Banner> = {
secondaryAction={
secondaryAction ? <Banner.SecondaryAction>{secondaryAction}</Banner.SecondaryAction> : null
}
leadingVisual={leadingVisualElement}
{...variantAndVisualProps}
{...rest}
/>
</PageLayout.Pane>
Expand All @@ -65,7 +75,7 @@ export const Playground: StoryObj<typeof Banner> = {
secondaryAction={
secondaryAction ? <Banner.SecondaryAction>{secondaryAction}</Banner.SecondaryAction> : null
}
leadingVisual={leadingVisualElement}
{...variantAndVisualProps}
{...rest}
/>
</PageLayout.Content>
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/Banner/Banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,15 @@ describe('Banner', () => {
rerender(<Banner title="test" description="test-description" variant="upsell" icon={<CustomIcon />} />)
expect(screen.getByTestId('icon')).toBeInTheDocument()

// @ts-expect-error unsupported variant used to verify runtime fallback behavior
rerender(<Banner title="test" description="test-description" variant="critical" icon={<CustomIcon />} />)
expect(screen.queryByTestId('icon')).toBe(null)

// @ts-expect-error unsupported variant used to verify runtime fallback behavior
rerender(<Banner title="test" description="test-description" variant="success" icon={<CustomIcon />} />)
expect(screen.queryByTestId('icon')).toBe(null)

// @ts-expect-error unsupported variant used to verify runtime fallback behavior
rerender(<Banner title="test" description="test-description" variant="warning" icon={<CustomIcon />} />)
expect(screen.queryByTestId('icon')).toBe(null)
})
Expand All @@ -258,12 +261,15 @@ describe('Banner', () => {
rerender(<Banner title="test" description="test-description" variant="upsell" leadingVisual={<CustomIcon />} />)
expect(screen.getByTestId('leading-visual')).toBeInTheDocument()

// @ts-expect-error unsupported variant used to verify runtime fallback behavior
rerender(<Banner title="test" description="test-description" variant="critical" leadingVisual={<CustomIcon />} />)
expect(screen.queryByTestId('leading-visual')).toBe(null)

// @ts-expect-error unsupported variant used to verify runtime fallback behavior
rerender(<Banner title="test" description="test-description" variant="success" leadingVisual={<CustomIcon />} />)
expect(screen.queryByTestId('leading-visual')).toBe(null)

// @ts-expect-error unsupported variant used to verify runtime fallback behavior
rerender(<Banner title="test" description="test-description" variant="warning" leadingVisual={<CustomIcon />} />)
expect(screen.queryByTestId('leading-visual')).toBe(null)
})
Expand Down
49 changes: 30 additions & 19 deletions packages/react/src/Banner/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import classes from './Banner.module.css'
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'

export type BannerVariant = 'critical' | 'info' | 'success' | 'upsell' | 'warning'
type BannerVariantsWithCustomVisual = 'info' | 'upsell'

type BannerContextValue = {
titleId: string
}

const BannerContext = React.createContext<BannerContextValue | undefined>(undefined)

export type BannerProps = React.ComponentPropsWithoutRef<'section'> & {
interface BannerBaseProps extends Omit<React.ComponentPropsWithoutRef<'section'>, 'title'> {
/**
* Provide an optional label to override the default name for the Banner
* landmark region
Expand All @@ -41,17 +42,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.
Expand All @@ -74,11 +64,6 @@ export type BannerProps = React.ComponentPropsWithoutRef<'section'> & {
*/
title?: React.ReactNode

/**
* Specify the type of the Banner
*/
variant?: BannerVariant

/**
* Specify the layout of the Banner. Compact layout will reduce the padding.
*/
Expand All @@ -95,7 +80,33 @@ export type BannerProps = React.ComponentPropsWithoutRef<'section'> & {
flush?: boolean
}

const iconForVariant: Record<BannerVariant, React.ReactNode> = {
type VariantAndLeadingVisualProps =
| {
/**
* Specify the type of the Banner, default = info
*/
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<BannerVariant, BannerVariantsWithCustomVisual>
icon?: never
leadingVisual?: never
}

export type BannerProps = BannerBaseProps & VariantAndLeadingVisualProps

const defaultIconForVariant: Record<BannerVariant, React.ReactNode> = {
critical: <StopIcon />,
info: <InfoIcon />,
success: <CheckCircleIcon />,
Expand Down Expand Up @@ -170,7 +181,7 @@ export const Banner = React.forwardRef<HTMLElement, BannerProps>(function Banner
data-flush={flush ? '' : undefined}
>
<div data-component="Banner.Icon" className={classes.BannerIcon}>
{visual && supportsCustomIcon ? visual : iconForVariant[variant]}
{visual && supportsCustomIcon ? visual : defaultIconForVariant[variant]}
</div>
<div className={classes.BannerContainer}>
<div data-component="Banner.Content" className={classes.BannerContent}>
Expand Down
Loading