From 6aa1a7d56e999ec4c22ef899640227bf8e45fef2 Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Thu, 26 Dec 2024 16:16:52 -0100
Subject: [PATCH 1/7] feat: Unify Alert and Banner under a single Banner
component with new designs
---
src/banner/banner.module.css | 110 +++++----
src/banner/banner.stories.mdx | 406 +++++++++++++++++++++-------------
src/banner/banner.test.tsx | 241 ++++++++++++++------
src/banner/banner.tsx | 240 ++++++++++++++------
src/banner/promo-image.tsx | 13 ++
src/icons/banner-icon.tsx | 100 +++++++++
6 files changed, 780 insertions(+), 330 deletions(-)
create mode 100644 src/banner/promo-image.tsx
create mode 100644 src/icons/banner-icon.tsx
diff --git a/src/banner/banner.module.css b/src/banner/banner.module.css
index dd410c364..e2a7f19cb 100644
--- a/src/banner/banner.module.css
+++ b/src/banner/banner.module.css
@@ -1,70 +1,96 @@
:root {
- --reactist-banner-title-font-size: 14px;
- --reactist-banner-title-line-height: 21px;
+ --reactist-banner-background-color: #fcfaf8;
+ --reactist-banner-border-color: #e6e6e6;
+ --reactist-divider-color: var(--reactist-divider-secondary);
- --reactist-banner-description-font-size: 12px;
- --reactist-banner-description-line-height: 20px;
+ --reactist-banner-main-copy-font-size: 14px;
+ --reactist-banner-main-copy-line-height: 21px;
+ --reactist-banner-main-copy-spacing: -0.15px;
+ --reactist-banner-main-copy-color: #202020;
- --reactist-banner-info-border: #eeeeee;
- --reactist-banner-info-background: #fafafa;
- --reactist-banner-info-title: #202020;
- --reactist-banner-info-description: #666666;
-
- --reactist-banner-promotion-border: #fae8d6;
- --reactist-banner-promotion-background: #fffaf4;
- --reactist-banner-promotion-title: #202020;
- --reactist-banner-promotion-description: #666666;
+ --reactist-banner-secondary-copy-font-size: 12px;
+ --reactist-banner-secondary-copy-line-height: 20px;
+ --reactist-banner-secondary-copy-color: #666666;
}
.banner {
- border-style: solid;
- border-width: 1px;
- padding-top: var(--reactist-spacing-small);
- padding-bottom: var(--reactist-spacing-small);
- padding-left: var(--reactist-spacing-large);
- padding-right: var(--reactist-spacing-large);
- letter-spacing: -0.15px;
+ container-name: banner;
+ container-type: inline-size;
+ background-color: var(--reactist-banner-background-color);
font-family: var(--reactist-font-family);
+ border: 1px solid var(--reactist-banner-border-color);
+ overflow: hidden;
+ min-height: 64px;
+}
+.banner:has(.image) {
+ width: min-content;
+ container-type: normal;
}
-.banner-info {
- background-color: var(--reactist-banner-info-background);
- border-color: var(--reactist-banner-info-border);
+.content {
+ padding: var(--reactist-spacing-large);
}
-.banner-promotion {
- background-color: var(--reactist-banner-promotion-background);
- border-color: var(--reactist-banner-promotion-border);
+.title,
+.description {
+ font-size: var(--reactist-banner-main-copy-font-size);
+ line-height: var(--reactist-banner-main-copy-line-height);
+ letter-spacing: var(--reactist-banner-main-copy-spacing);
+ color: var(--reactist-banner-main-copy-color);
}
.title {
- font-size: var(--reactist-banner-title-font-size);
- line-height: var(--reactist-banner-title-line-height);
font-weight: var(--reactist-font-weight-strong);
}
-.title-without-description {
- font-weight: var(--reactist-font-weight-regular);
+.description.secondary {
+ font-size: var(--reactist-banner-secondary-copy-font-size);
+ line-height: var(--reactist-banner-secondary-copy-line-height);
+ color: var(--reactist-banner-secondary-copy-color);
+ letter-spacing: initial;
}
-.title-info {
- color: var(--reactist-banner-info-title);
+.image {
+ border-bottom: 1px solid var(--reactist-banner-divider-color);
}
-.title-promotion {
- color: var(--reactist-banner-promotion-title);
+.image img,
+.icon svg {
+ display: block;
}
-.description {
- font-size: var(--reactist-banner-description-font-size);
- line-height: var(--reactist-banner-description-line-height);
- font-weight: var(--reactist-font-weight-regular);
+.icon .closeButton {
+ display: none;
}
-.description-info {
- color: var(--reactist-banner-info-description);
+.copy {
+ padding: calc(var(--reactist-spacing-xsmall) / 2) 0;
}
+.copy .inlineLink:first-of-type {
+ margin-left: var(--reactist-spacing-xsmall);
+}
+
+@container banner (width < 235px) {
+ .content,
+ .staticContent {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .icon {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .icon .closeButton {
+ display: flex;
+ }
+ .icon .closeButton:only-child {
+ margin-left: auto;
+ }
-.description-promotion {
- color: var(--reactist-banner-promotion-description);
+ .actions .closeButton {
+ display: none;
+ }
}
diff --git a/src/banner/banner.stories.mdx b/src/banner/banner.stories.mdx
index a1bac33ef..89f325286 100644
--- a/src/banner/banner.stories.mdx
+++ b/src/banner/banner.stories.mdx
@@ -4,6 +4,7 @@ import { Text } from '../text'
import { Stack } from '../stack'
import { Banner } from './banner'
import { Button } from '../button'
+import { PromoImage } from './promo-image'
-export function Icon(theme) {
+export function ArchiveIcon() {
return (
-
+
-
- )
-}
-
-export function StarIcon(theme) {
- return (
-
-
-
- )
-}
-
-export function ArchiveIcon(theme) {
- if (theme === 'dark')
- return (
-
-
-
- )
- return (
-
-
-
- )
-}
-
-export function EyeIcon(theme) {
- if (theme === 'dark')
- return (
-
-
-
- )
- return (
-
-
)
@@ -128,7 +40,7 @@ export function PlaygroundTemplate({ tone, title, description, action }) {
}
title={title}
description={description}
action={action}
@@ -137,59 +49,183 @@ export function PlaygroundTemplate({ tone, title, description, action }) {
)
}
-export function BannerExamples({ theme }) {
+export function BannerIconExamples({ theme }) {
return (
- This workspace has used 5 of its 5 project limit. Upgrade to Pro for
- more.
- >
- }
- action={getButton('Upgrade')}
+ tone="neutral"
+ icon={ }
+ description="This is a neutral message"
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function BannerActionExamples({ theme }) {
+ return (
+
+
+ }
+ description="A read-only banner without any action"
+ />
+ }
+ description="A banner with a dismiss action"
+ onClose={() => ({})}
+ />
+ }
+ description="A banner with a primary CTA"
+ action={{
+ type: 'button',
+ label: 'Action',
+ variant: 'primary',
+ }}
+ />
+ }
+ description="A banner with a tertiary CTA"
+ action={{
+ type: 'button',
+ label: 'Action',
+ variant: 'tertiary',
+ }}
+ />
+ }
+ description="A banner with a primary CTA and a dismiss option"
+ action={{
+ type: 'button',
+ label: 'Action',
+ variant: 'primary',
+ }}
+ onClose={() => ({})}
+ />
+ }
+ description="A banner with a tertiary CTA and a dismiss option"
+ action={{
+ type: 'button',
+ label: 'Action',
+ variant: 'tertiary',
+ }}
+ onClose={() => ({})}
/>
- Members can view but not edit.
- >
- }
- action={getButton('Unarchive project')}
+ tone="neutral"
+ icon={ }
+ description="A banner with a inline link."
+ inlineLinks={[{ label: 'Learn more', href: '#' }]}
/>
- Members can view but not edit.
- >
- }
+ tone="neutral"
+ icon={ }
+ title="This is a sample title"
+ description="A banner with a inline link in secondary copy."
+ inlineLinks={[{ label: 'Learn more', href: '#' }]}
/>
}
+ title="This is a sample title"
+ description="A banner with multiple inline links."
+ inlineLinks={[
+ { label: 'Learn more', href: '#' },
+ { label: 'Send feedback', href: '#' },
+ ]}
+ />
+ }
+ title="This is a sample title"
+ description="Here’s the message below the title."
+ />
+ }
+ title="This is a sample title"
+ description="Here’s the message below the title."
+ action={{
+ type: 'button',
+ label: 'Action',
+ variant: 'primary',
+ }}
+ onClose={() => ({})}
/>
)
}
-# Banner
+export function BannerCopyExamples({ theme }) {
+ return (
+
+
+ }
+ description="This is a some body text. It can span over two lines or more."
+ />
+ }
+ title="This is a sample title"
+ description="Here’s the message below the title. The copy can span over more than one line."
+ />
+
+
+ )
+}
+
+export function BannerImageExamples({ theme }) {
+ return (
+
+
+ }
+ inlineLinks={[{ label: 'Learn more', href: '#' }]}
+ />
+ }
+ inlineLinks={[{ label: 'Learn more', href: '#' }]}
+ />
+ }
+ inlineLinks={[{ label: 'Learn more', href: '#' }]}
+ onClose={() => ({})}
+ />
+
+
+ )
+}
-A simple banner component meant to be used to _inform_ the user of promotional content or disclaimers.
+# Banner
-If you're intending to _alert_ the user of a certain error or warning condition, consider using the `Alert` component instead.
+A simple banner component meant to be used to _inform_ the user of promotional content, disclaimers, warnings, as well as success and error states.
+
+
+
+
+
+### Actions
+
+Banners can have an optional dismiss action, or a primary or tertiary CTA, but never both. Additionally, banners can have an optional inline links near the description.
+
+These actions can be combined, such as a primary CTA with a dismiss option, or a primary CTA with a dismiss option and an inline link.
+
+
+
+
+
+
+
+### Copy
+
+Banners have two content options: a regular text that can span over multiple lines, or a bold header and a description with a secondary text color.
+
+
+
+
+
+
+
+### Image
+
+Banners can include images placed at the top, separated from the copy by a divider. The banner adapts to the image, ensuring it fills the full width and height of the banner.
+
+Image banners do not feature icons but can include body text or a combination of a header and description. They also support optional inline links and a dismiss action.
+
-
+
@@ -242,17 +334,17 @@ The following CSS custom properties are available to customize the banner compon
```css
:root {
- // tone="info"
- --reactist-banner-info-background: #eeeeee;
- --reactist-banner-info-border: #fafafa;
- --reactist-banner-info-title: #202020;
- --reactist-banner-info-description: #666666;
-
- // tone="promotion"
- --reactist-banner-promotion-border: #fae8d6;
- --reactist-banner-promotion-background: #fffaf4;
- --reactist-banner-promotion-title: #202020;
- --reactist-banner-promotion-description: #666666;
+ --reactist-banner-background-color: #fcfaf8;
+ --reactist-banner-border-color: #e6e6e6;
+
+ --reactist-banner-main-copy-font-size: 14px;
+ --reactist-banner-main-copy-line-height: 21px;
+ --reactist-banner-main-copy-spacing: -0.15px;
+ --reactist-banner-main-copy-color: #202020;
+
+ --reactist-banner-secondary-copy-font-size: 12px;
+ --reactist-banner-secondary-copy-line-height: 20px;
+ --reactist-banner-secondary-copy-color: #666666;
}
```
@@ -261,22 +353,26 @@ The following CSS custom properties are available to customize the banner compon
export function DarkModeTemplate() {
return (
-
+ } description="This is a neutral message" />
+ }
+ description="This is a neutral message"
+ onClose={() => ({})}
+ />
)
}
diff --git a/src/banner/banner.test.tsx b/src/banner/banner.test.tsx
index 49ef7efcd..2c72e4ff5 100644
--- a/src/banner/banner.test.tsx
+++ b/src/banner/banner.test.tsx
@@ -1,127 +1,244 @@
import * as React from 'react'
-import { render, screen } from '@testing-library/react'
+import { render, screen, within } from '@testing-library/react'
import { axe } from 'jest-axe'
-import { Banner, BannerTone } from './banner'
+import { Banner, SystemBannerTone } from './banner'
+import userEvent from '@testing-library/user-event'
describe('Banner', () => {
it('renders as a
element', () => {
- render( )
- expect(screen.getByTestId('test-banner').tagName).toBe('DIV')
+ render( )
+ expect(screen.getByRole('status').tagName).toBe('DIV')
})
- it('renders the title inside the badge element', () => {
- render( )
- expect(screen.getByTestId('test-banner')).toHaveTextContent('New')
+ it('renders the title inside the banner element', () => {
+ render( )
+ expect(screen.getByRole('status')).toHaveTextContent('Info title')
+ })
+
+ it('by default does not render an icon for neutral tone', () => {
+ expect(screen.queryByTestId('banner-icon-neutral')).not.toBeInTheDocument()
+ expect(screen.queryByRole('img')).not.toBeInTheDocument()
+ })
+
+ it('renders a custom icon for neutral tone', () => {
+ render(Custom Icon} description="Info" />)
+ expect(screen.getByText('Custom Icon')).toBeInTheDocument()
+ })
+
+ it('renders an image for neutral tone', () => {
+ render( } description="Info" />)
+ expect(screen.getByAltText('Custom Image')).toBeInTheDocument()
})
test.each([
- ['info' as BannerTone, 'title-info'],
- ['promotion' as BannerTone, 'title-promotion'],
- ])('renders a different CSS class according to the tone', (tone, expectedCSSClass) => {
- render(
- <>
-
- >,
- )
- expect(screen.getByText('Info')).toHaveClass(expectedCSSClass)
+ ['info' as SystemBannerTone, 'info'],
+ ['upgrade' as SystemBannerTone, 'upgrade'],
+ ['experiment' as SystemBannerTone, 'experiment'],
+ ['warning' as SystemBannerTone, 'warning'],
+ ['error' as SystemBannerTone, 'error'],
+ ['success' as SystemBannerTone, 'success'],
+ ])('renders a different icon according to the tone', (tone, expectedTone) => {
+ render( )
+ expect(
+ within(screen.getByRole('status')).getByTestId(`banner-icon-${expectedTone}`),
+ ).toBeInTheDocument()
})
it('passes through aria-related attributes', () => {
- render(
- ,
- )
- expect(screen.getByTestId('test-banner')).toHaveAttribute('aria-hidden', 'true')
+ render( )
+ expect(screen.queryByRole('status')).not.toBeInTheDocument()
+ expect(screen.getByRole('status', { hidden: true })).toBeInTheDocument()
})
it('passes through data-* attributes', () => {
- // Even though the use of data-testid already proves that the test passes, it may be important
- // to assert that any data-* attribute is forwarded as well.
render(
,
)
- expect(screen.getByTestId('test-banner')).toHaveAttribute('data-gtm-id', 'track-id')
+ expect(screen.getByRole('status')).toHaveAttribute('data-gtm-id', 'track-id')
})
- it('does not passes through other attributes such as className, or exceptionallySetClassName', () => {
+ it('does not pass through other attributes such as className, or exceptionallySetClassName', () => {
render(
,
)
- expect(screen.getByTestId('test-banner')).not.toHaveClass('test-one')
- expect(screen.getByTestId('test-banner')).not.toHaveClass('test-two')
+ expect(screen.getByRole('status')).not.toHaveClass('test-one')
+ expect(screen.getByRole('status')).not.toHaveClass('test-two')
})
- it('renders more than one banner with no a11y violations', async () => {
- const { container } = render(
- <>
-
-
- >,
+ it('honors rich text in the title', () => {
+ render(
+
+ This is really important
+ >
+ }
+ description="Description"
+ />,
+ )
+ expect(screen.getByRole('status').innerHTML).toContain(
+ 'This is really important ',
)
- const results = await axe(container)
- expect(results).toHaveNoViolations()
})
- it('honors rich text', () => {
+ it('honors rich text in the description', () => {
render(
This is really important
>
}
- data-testid="test-banner"
/>,
)
- expect(screen.getByTestId('test-banner').innerHTML).toContain(
+ expect(screen.getByRole('status').innerHTML).toContain(
'This is really important ',
)
})
it('uses the title as the accessible name', () => {
- render( )
+ render( )
expect(screen.getByRole('status', { name: 'Hello World' })).toBeInTheDocument()
})
- it('uses the description as the accessible description', () => {
+ it("uses the description as the accessible name if there isn't a title", () => {
+ render( )
+ expect(screen.getByRole('status', { name: 'Hello World' })).toBeInTheDocument()
+ })
+
+ it('renders action button', () => {
+ const onClickSpy = jest.fn()
+ render(
+ ,
+ )
+ userEvent.click(screen.getByRole('button', { name: 'Click Me' }))
+ expect(onClickSpy).toHaveBeenCalled()
+ })
+
+ it('renders action link', () => {
render(
,
)
- expect(screen.getByRole('status', { name: 'Hello World' })).toHaveAccessibleDescription(
- 'Welcome to the world, Linus!',
+ expect(screen.getByRole('link', { name: 'Click Me' })).toHaveAttribute(
+ 'href',
+ 'http://localhost',
)
})
- it('does not have an accessible description if description is missing', () => {
- render( )
- expect(
- screen.getByRole('status', { name: 'Hello World' }),
- ).not.toHaveAccessibleDescription()
+ it('renders inline link', () => {
+ render(
+ ,
+ )
+ expect(screen.getByRole('link', { name: 'Learn more' })).toBeInTheDocument()
+ })
+
+ it('renders multiple inline links', () => {
+ render(
+ Info
}
+ inlineLinks={[
+ { label: 'Learn more', href: '#' },
+ { label: 'Send feedback', href: '#' },
+ ]}
+ />,
+ )
+ expect(screen.getByRole('link', { name: 'Learn more' })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Send feedback' })).toBeInTheDocument()
+ expect(screen.getByRole('status')).toHaveTextContent('Learn more · Send feedback')
+ })
+
+ it('renders close button', () => {
+ const onClose = jest.fn()
+ const { rerender } = render( )
+ expect(screen.queryByRole('button', { name: 'Close banner' })).not.toBeInTheDocument()
+
+ rerender( )
+ // close button is rendered twice because depending on banner size it can be in two places,
+ // but only one is visible at a time (the other is set to display: none with CSS)
+ expect(screen.getAllByRole('button', { name: 'Close banner' })).toHaveLength(2)
+
+ rerender(
+ ,
+ )
+ // close button is rendered twice because depending on banner size it can be in two places,
+ // but only one is visible at a time (the other is set to display: none with CSS)
+ expect(screen.getAllByRole('button', { name: 'Custom close label' })).toHaveLength(2)
+ })
+
+ it('calls onClose when close button is clicked', () => {
+ const onClose = jest.fn()
+ render(
+ ,
+ )
+ // close button is rendered twice because depending on banner size it can be in two places,
+ // but only one is visible at a time (the other is set to display: none with CSS)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ userEvent.click(screen.getAllByRole('button', { name: 'Custom close label' })[0]!)
+ expect(onClose).toHaveBeenCalledTimes(1)
+ })
+
+ it('renders more than one banner with no a11y violations', async () => {
+ const { container } = render(
+ <>
+
+ Custom Icon}
+ onClose={jest.fn()}
+ inlineLinks={[{ label: 'Link 1', href: '#' }]}
+ />
+
+ >,
+ )
+ const results = await axe(container)
+ expect(results).toHaveNoViolations()
})
})
diff --git a/src/banner/banner.tsx b/src/banner/banner.tsx
index dd67ce052..c5cc9ce75 100644
--- a/src/banner/banner.tsx
+++ b/src/banner/banner.tsx
@@ -1,106 +1,204 @@
import * as React from 'react'
import { Box } from '../box'
-import { Columns, Column } from '../columns'
import { useId } from '../utils/common-helpers'
import styles from './banner.module.css'
+import { Button, IconButton } from '../button'
+import { CloseIcon } from '../icons/close-icon'
+import { BannerIcon } from '../icons/banner-icon'
+import { TextLink } from '../text-link'
-export type BannerTone = 'info' | 'promotion'
+/**
+ * Represents the tone of a banner.
+ * 'neutral' accepts a custom icon, the rest do not.
+ * @default 'neutral'
+ */
+export type BannerTone = 'neutral' | SystemBannerTone
-type BannerProps = {
+/**
+ * Predefined system tones for banners.
+ * Each tone has its own preset icon.
+ */
+export type SystemBannerTone = 'info' | 'upgrade' | 'experiment' | 'warning' | 'error' | 'success'
+
+type BaseAction = {
+ variant: 'primary' | 'tertiary'
+ label: string
+}
+type ActionButton = BaseAction & { type: 'button' } & Omit<
+ React.ButtonHTMLAttributes,
+ 'className'
+ >
+type ActionLink = BaseAction & { type: 'link' } & Omit<
+ React.AnchorHTMLAttributes,
+ 'className'
+ >
+/**
+ * Represents an action that can be taken from the banner.
+ * Can be either a button or a link, sharing common properties from BaseAction.
+ */
+type Action = ActionButton | ActionLink
+
+/**
+ * Configuration for inline links within the banner description.
+ * Extends TextLink component props with a required label.
+ */
+type InlineLink = { label: string } & React.ComponentProps
+
+type WithCloseButton = {
+ closeLabel?: string
+ onClose: () => void
+}
+type WithoutCloseButton = {
+ closeLabel?: never
+ onClose?: never
+}
+/**
+ * Controls the close button behavior.
+ * If none is provided, the banner will not have a close button.
+ */
+type CloseButton = WithCloseButton | WithoutCloseButton
+
+type BaseBanner = {
id?: string
+ title?: React.ReactNode
+ description: Exclude
+ action?: Action
+ inlineLinks?: InlineLink[]
+} & CloseButton
- /**
- * The tone of the Banner. Affects the background color and the outline.
- */
- tone: BannerTone
-
- /**
- * The icon that should be added inside the Banner.
- */
- icon: React.ReactElement | string | number
-
- /**
- * The title to be displayed at the top of the Banner.
- */
- title: React.ReactNode
-
- /**
- * An optional description to be displayed inside the Banner.
- */
- description?: React.ReactNode
-
- /**
- * An optional action to displayed inside the Banner.
- */
- action?: React.ReactElement | string | number
+/**
+ * Configuration for neutral tone banners.
+ * Can include either an image, an icon, or neither, but never both.
+ */
+type NeutralBanner = BaseBanner & {
+ tone: Extract
+} & (
+ | { image: React.ReactElement; icon?: never }
+ | { icon: React.ReactElement; image?: never }
+ | { image?: never; icon?: never }
+ )
+
+/**
+ * Configuration for system tone banners.
+ * Cannot include custom images or icons as they use preset ones.
+ */
+type SystemBanner = BaseBanner & {
+ tone: SystemBannerTone
+ image?: never
+ icon?: never
}
+type BannerProps = NeutralBanner | SystemBanner
+
const Banner = React.forwardRef(function Banner(
- { id, tone, icon, title, description, action, ...props }: BannerProps,
+ {
+ id,
+ tone,
+ title,
+ description,
+ action,
+ icon,
+ image,
+ inlineLinks,
+ closeLabel,
+ onClose,
+ ...props
+ }: BannerProps,
ref,
) {
const titleId = useId()
const descriptionId = useId()
+
+ const closeButton = onClose ? (
+ }
+ aria-label={closeLabel ?? 'Close banner'}
+ />
+ ) : null
+
return (
-
-
- {icon}
-
-
-
- {description ? (
-
- {title}
-
- ) : (
-
+ {image ? {image} : null}
+
+
+
+
+ {tone === 'neutral' ? icon : }
+ {closeButton}
+
+
+
+ {title ? (
+
{title}
- )}
- {description ? (
-
- {description}
-
) : null}
+
+ {description}
+ {inlineLinks?.map(({ label, ...props }, index) => {
+ return (
+
+
+ {label}
+
+ {index < inlineLinks.length - 1 ? · : ''}
+
+ )
+ })}
+
-
- {action ? {action} : null}
-
+
+
+ {action || closeButton ? (
+
+ {action?.type === 'button' ? : null}
+ {action?.type === 'link' ? : null}
+ {closeButton}
+
+ ) : null}
+
)
})
+function ActionButton({ type, label, ...props }: ActionButton) {
+ return {label}
+}
+
+function ActionLink({ type, label, variant, ...props }: ActionLink) {
+ return (
+ }
+ >
+ {label}
+
+ )
+}
+
export { Banner }
export type { BannerProps }
diff --git a/src/banner/promo-image.tsx b/src/banner/promo-image.tsx
new file mode 100644
index 000000000..0c74b768d
--- /dev/null
+++ b/src/banner/promo-image.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react'
+
+export function PromoImage({ theme = 'light' }: { theme?: 'light' | 'dark' }) {
+ if (theme === 'dark') {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/icons/banner-icon.tsx b/src/icons/banner-icon.tsx
new file mode 100644
index 000000000..741521981
--- /dev/null
+++ b/src/icons/banner-icon.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react'
+import type { SystemBannerTone } from '../banner/banner'
+
+const bannerIconForTone: Record = {
+ info: BannerInfoIcon,
+ upgrade: BannerUpgradeIcon,
+ experiment: BannerExperimentIcon,
+ warning: BannerWarningIcon,
+ error: BannerErrorIcon,
+ success: BannerSuccessIcon,
+}
+
+function BannerIcon({ tone, ...props }: JSX.IntrinsicElements['svg'] & { tone: SystemBannerTone }) {
+ const Icon = bannerIconForTone[tone]
+ return Icon ? : null
+}
+
+function BannerInfoIcon(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+function BannerUpgradeIcon(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+
+ )
+}
+
+function BannerExperimentIcon(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+function BannerWarningIcon(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+function BannerErrorIcon(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+function BannerSuccessIcon(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+export { BannerIcon }
From 78a342b56c10de76a12367b66d5679235b1fbcdd Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Thu, 26 Dec 2024 16:38:14 -0100
Subject: [PATCH 2/7] chore: remove Alert component
---
src/alert/alert.module.css | 47 ----------
src/alert/alert.stories.mdx | 109 ------------------------
src/alert/alert.test.tsx | 33 -------
src/alert/alert.tsx | 72 ----------------
src/alert/index.ts | 1 -
src/index.ts | 1 -
stories/components/Dropdown.stories.tsx | 15 ++--
stories/components/Input.stories.tsx | 15 ++--
stories/components/Select.stories.tsx | 15 ++--
9 files changed, 30 insertions(+), 278 deletions(-)
delete mode 100644 src/alert/alert.module.css
delete mode 100644 src/alert/alert.stories.mdx
delete mode 100644 src/alert/alert.test.tsx
delete mode 100644 src/alert/alert.tsx
delete mode 100644 src/alert/index.ts
diff --git a/src/alert/alert.module.css b/src/alert/alert.module.css
deleted file mode 100644
index 926df0f82..000000000
--- a/src/alert/alert.module.css
+++ /dev/null
@@ -1,47 +0,0 @@
-.container {
- border-style: solid;
- border-width: 1px;
- color: var(--reactist-content-primary);
- padding: var(--reactist-spacing-small);
-}
-
-.content {
- /* this is to make sure it always has the same minimum height, whether it has a close button or not */
- min-height: var(--reactist-button-small-height);
-}
-
-.icon {
- display: block;
-}
-
-.tone-info {
- background-color: var(--reactist-alert-tone-info-background);
- border-color: var(--reactist-alert-tone-info-border);
-}
-.tone-info .icon {
- color: var(--reactist-alert-tone-info-icon);
-}
-
-.tone-positive {
- background-color: var(--reactist-alert-tone-positive-background);
- border-color: var(--reactist-alert-tone-positive-border);
-}
-.tone-positive .icon {
- color: var(--reactist-alert-tone-positive-icon);
-}
-
-.tone-caution {
- background-color: var(--reactist-alert-tone-caution-background);
- border-color: var(--reactist-alert-tone-caution-border);
-}
-.tone-caution .icon {
- color: var(--reactist-alert-tone-caution-icon);
-}
-
-.tone-critical {
- background-color: var(--reactist-alert-tone-critical-background);
- border-color: var(--reactist-alert-tone-critical-border);
-}
-.tone-critical .icon {
- color: var(--reactist-alert-tone-critical-icon);
-}
diff --git a/src/alert/alert.stories.mdx b/src/alert/alert.stories.mdx
deleted file mode 100644
index 76f020e0e..000000000
--- a/src/alert/alert.stories.mdx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { Meta, Story, Canvas, ArgsTable, Description } from '@storybook/addon-docs'
-import { Box } from '../box'
-import { Text } from '../text'
-import { Stack } from '../stack'
-import { Alert } from './alert'
-
-
-
-export function getContent(content) {
- return content === 'long' ? (
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi non gravida lacus. Sed sit amet congue diam, ac ultrices elit.'
- ) : content === 'short' ? (
- 'Lorem ipsum dolor sit amet.'
- ) : (
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi non gravida lacus.
- Sed sit amet congue diam, ac ultrices elit.
-
-
- Suspendisse at neque leo. Duis facilisis nulla non lectus malesuada, vitae
- scelerisque massa hendrerit. Nulla lacinia luctus risus, dapibus semper turpis
- vestibulum eu.
-
-
- )
-}
-
-# Alert
-
-export function AlertWrapper({ tone, content }) {
- return (
- undefined}>
- {getContent(content)}
-
- )
-}
-
-A simple Alert component.
-
-
-
-
-
- {['info', 'positive', 'caution', 'critical'].map((tone) => (
-
- ))}
-
-
-
-
-
-## Playground
-
-export function Template({ tone, content, closeLabel }) {
- const text = getContent(content)
- return (
-
- {text}
- undefined}>
- {text}
-
-
- )
-}
-
-
-
- {Template.bind({})}
-
-
-
----
-
-
-
diff --git a/src/alert/alert.test.tsx b/src/alert/alert.test.tsx
deleted file mode 100644
index de48fd0e2..000000000
--- a/src/alert/alert.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import * as React from 'react'
-import { render, screen } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { axe } from 'jest-axe'
-import { Alert, AlertProps } from './alert'
-
-describe('Alert', () => {
- it('allows to be dismissed', () => {
- function Example(props: Omit) {
- const [show, setShow] = React.useState(true)
- return show ? (
- setShow(false)} />
- ) : null
- }
- render(Info message )
- expect(screen.getByRole('alert')).toHaveTextContent('Info message')
- userEvent.click(screen.getByRole('button', { name: 'Close alert' }))
- expect(screen.queryByRole('alert')).not.toBeInTheDocument()
- })
-
- it('renders with no a11y violations', async () => {
- const { container } = render(
- <>
- Info message
- undefined}>
- Another info message
-
- >,
- )
- const results = await axe(container)
- expect(results).toHaveNoViolations()
- })
-})
diff --git a/src/alert/alert.tsx b/src/alert/alert.tsx
deleted file mode 100644
index bce2f8fc3..000000000
--- a/src/alert/alert.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as React from 'react'
-import { getClassNames } from '../utils/responsive-props'
-import { Box } from '../box'
-import { IconButton } from '../button'
-import { Columns, Column } from '../columns'
-
-import { AlertIcon } from '../icons/alert-icon'
-import { CloseIcon } from '../icons/close-icon'
-
-import styles from './alert.module.css'
-
-import type { AlertTone } from '../utils/common-types'
-
-type AllOrNone = T | { [K in keyof T]?: never }
-
-type AlertCloseProps = AllOrNone<{
- closeLabel: string
- onClose: () => void
-}>
-
-type AlertProps = {
- id?: string
- children: React.ReactNode
- tone: AlertTone
-} & AlertCloseProps
-
-function Alert({ id, children, tone, closeLabel, onClose }: AlertProps) {
- return (
-
-
-
-
-
-
-
- {children}
-
-
- {onClose != null && closeLabel != null ? (
-
- }
- style={{
- // @ts-expect-error not sure how to make TypeScript understand custom CSS properties
- '--reactist-btn-hover-fill': 'transparent',
- }}
- />
-
- ) : null}
-
-
- )
-}
-
-export { Alert }
-export type { AlertProps }
diff --git a/src/alert/index.ts b/src/alert/index.ts
deleted file mode 100644
index 17dc897db..000000000
--- a/src/alert/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './alert'
diff --git a/src/index.ts b/src/index.ts
index f6b8fe9b6..00234a048 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,7 +10,6 @@ export * from './hidden'
export * from './hidden-visually'
// alerts, notifications, etc.
-export * from './alert'
export * from './banner'
export * from './loading'
export * from './notice'
diff --git a/stories/components/Dropdown.stories.tsx b/stories/components/Dropdown.stories.tsx
index 03345c21c..70040de42 100644
--- a/stories/components/Dropdown.stories.tsx
+++ b/stories/components/Dropdown.stories.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import Button from '../../src/components/deprecated-button'
import Dropdown from '../../src/components/deprecated-dropdown'
-import { Alert } from '../../src/alert'
+import { Banner } from '../../src/banner'
import { Stack } from '../../src/stack'
import LinkTo from '@storybook/addon-links/react'
@@ -18,10 +18,15 @@ export default {
export const DropdownStory = () => (
-
- Deprecated: While not a 1:1 replacement, consider using{' '}
- Menu as an alternative
-
+
+ Deprecated: While not a 1:1 replacement, consider using{' '}
+ Menu as an alternative
+ >
+ }
+ />
diff --git a/stories/components/Input.stories.tsx b/stories/components/Input.stories.tsx
index 0e8f32db3..2964450df 100644
--- a/stories/components/Input.stories.tsx
+++ b/stories/components/Input.stories.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import Input from '../../src/components/deprecated-input'
-import { Alert } from '../../src/alert'
+import { Banner } from '../../src/banner'
import './styles/input_story.less'
import LinkTo from '@storybook/addon-links/react'
@@ -20,10 +20,15 @@ export default {
export const InputStory = () => (
-
- Deprecated: Please use{' '}
- TextField instead
-
+
+ Deprecated: Please use{' '}
+ TextField instead
+ >
+ }
+ />
This component is a dumb wrapper around the
<input />
element which justs add a class name to give it is
diff --git a/stories/components/Select.stories.tsx b/stories/components/Select.stories.tsx
index e35217a72..b552fc4f0 100644
--- a/stories/components/Select.stories.tsx
+++ b/stories/components/Select.stories.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import Select from '../../src/components/deprecated-select'
-import { Alert } from '../../src/alert'
+import { Banner } from '../../src/banner'
import { Stack } from '../../src/stack'
import LinkTo from '@storybook/addon-links/react'
@@ -31,10 +31,15 @@ export function SelectStory() {
return (
-
- Deprecated: Please use{' '}
- SelectField instead
-
+
+ Deprecated: Please use{' '}
+ SelectField instead
+ >
+ }
+ />
From 99818dfdd47d68d03f9926763a95ff419173f617 Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Tue, 7 Jan 2025 09:58:04 +0100
Subject: [PATCH 3/7] chore: update banner base action type
---
src/banner/banner.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/banner/banner.tsx b/src/banner/banner.tsx
index c5cc9ce75..039b7b84c 100644
--- a/src/banner/banner.tsx
+++ b/src/banner/banner.tsx
@@ -3,7 +3,7 @@ import { Box } from '../box'
import { useId } from '../utils/common-helpers'
import styles from './banner.module.css'
-import { Button, IconButton } from '../button'
+import { Button, ButtonProps, IconButton } from '../button'
import { CloseIcon } from '../icons/close-icon'
import { BannerIcon } from '../icons/banner-icon'
import { TextLink } from '../text-link'
@@ -24,7 +24,7 @@ export type SystemBannerTone = 'info' | 'upgrade' | 'experiment' | 'warning' | '
type BaseAction = {
variant: 'primary' | 'tertiary'
label: string
-}
+} & Pick
type ActionButton = BaseAction & { type: 'button' } & Omit<
React.ButtonHTMLAttributes,
'className'
From d363b960eb33c73ceed8fbc8122943c6183c8a9b Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Tue, 7 Jan 2025 10:13:26 +0100
Subject: [PATCH 4/7] chore: rename story promo image file
---
src/banner/{promo-image.tsx => story-promo-image.tsx} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/banner/{promo-image.tsx => story-promo-image.tsx} (100%)
diff --git a/src/banner/promo-image.tsx b/src/banner/story-promo-image.tsx
similarity index 100%
rename from src/banner/promo-image.tsx
rename to src/banner/story-promo-image.tsx
From 70613424bfdb69946192aabf4ffce850c87e4450 Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Tue, 7 Jan 2025 10:16:31 +0100
Subject: [PATCH 5/7] chore: change 'tone' prop to 'type'
---
src/banner/banner.stories.mdx | 66 ++++++++++++++++-----------------
src/banner/banner.test.tsx | 70 +++++++++++++++++------------------
src/banner/banner.tsx | 24 ++++++------
src/icons/banner-icon.tsx | 10 ++---
4 files changed, 85 insertions(+), 85 deletions(-)
diff --git a/src/banner/banner.stories.mdx b/src/banner/banner.stories.mdx
index 89f325286..643a9c359 100644
--- a/src/banner/banner.stories.mdx
+++ b/src/banner/banner.stories.mdx
@@ -4,7 +4,7 @@ import { Text } from '../text'
import { Stack } from '../stack'
import { Banner } from './banner'
import { Button } from '../button'
-import { PromoImage } from './promo-image'
+import { PromoImage } from './story-promo-image'
+
{buttonText}
)
}
-export function PlaygroundTemplate({ tone, title, description, action }) {
+export function PlaygroundTemplate({ type, title, description, action }) {
return (
}
title={title}
description={description}
@@ -54,17 +54,17 @@ export function BannerIconExamples({ theme }) {
}
description="This is a neutral message"
/>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
)
@@ -75,18 +75,18 @@ export function BannerActionExamples({ theme }) {
}
description="A read-only banner without any action"
/>
}
description="A banner with a dismiss action"
onClose={() => ({})}
/>
}
description="A banner with a primary CTA"
action={{
@@ -96,7 +96,7 @@ export function BannerActionExamples({ theme }) {
}}
/>
}
description="A banner with a tertiary CTA"
action={{
@@ -106,7 +106,7 @@ export function BannerActionExamples({ theme }) {
}}
/>
}
description="A banner with a primary CTA and a dismiss option"
action={{
@@ -117,7 +117,7 @@ export function BannerActionExamples({ theme }) {
onClose={() => ({})}
/>
}
description="A banner with a tertiary CTA and a dismiss option"
action={{
@@ -128,20 +128,20 @@ export function BannerActionExamples({ theme }) {
onClose={() => ({})}
/>
}
description="A banner with a inline link."
inlineLinks={[{ label: 'Learn more', href: '#' }]}
/>
}
title="This is a sample title"
description="A banner with a inline link in secondary copy."
inlineLinks={[{ label: 'Learn more', href: '#' }]}
/>
}
title="This is a sample title"
description="A banner with multiple inline links."
@@ -151,13 +151,13 @@ export function BannerActionExamples({ theme }) {
]}
/>
}
title="This is a sample title"
description="Here’s the message below the title."
/>
}
title="This is a sample title"
description="Here’s the message below the title."
@@ -178,12 +178,12 @@ export function BannerCopyExamples({ theme }) {
}
description="This is a some body text. It can span over two lines or more."
/>
}
title="This is a sample title"
description="Here’s the message below the title. The copy can span over more than one line."
@@ -198,20 +198,20 @@ export function BannerImageExamples({ theme }) {
}
inlineLinks={[{ label: 'Learn more', href: '#' }]}
/>
}
inlineLinks={[{ label: 'Learn more', href: '#' }]}
/>
}
@@ -232,8 +232,8 @@ A simple banner component meant to be used to _inform_ the user of promotional c
name="Playground"
parameters={{ docs: { source: { type: 'code' } } }}
argTypes={{
- tone: {
- options: ['info', 'promotion'],
+ type: {
+ options: ['info', 'error'],
control: { type: 'inline-radio' },
defaultValue: 'info',
},
@@ -258,7 +258,7 @@ A simple banner component meant to be used to _inform_ the user of promotional c
### Icons
-Banners can feature different icons or no icon at all. When the tone is neutral, a custom icon can be provided; otherwise, preset icons corresponding to the respective tone will be used.
+Banners can feature different icons or no icon at all. When the type is neutral, a custom icon can be provided; otherwise, preset icons corresponding to the respective type will be used.
- } description="This is a neutral message" />
+ } description="This is a neutral message" />
}
description="This is a neutral message"
onClose={() => ({})}
diff --git a/src/banner/banner.test.tsx b/src/banner/banner.test.tsx
index 2c72e4ff5..b86daaf5b 100644
--- a/src/banner/banner.test.tsx
+++ b/src/banner/banner.test.tsx
@@ -1,51 +1,51 @@
import * as React from 'react'
import { render, screen, within } from '@testing-library/react'
import { axe } from 'jest-axe'
-import { Banner, SystemBannerTone } from './banner'
+import { Banner, SystemBannerType } from './banner'
import userEvent from '@testing-library/user-event'
describe('Banner', () => {
it('renders as a
element', () => {
- render( )
+ render( )
expect(screen.getByRole('status').tagName).toBe('DIV')
})
it('renders the title inside the banner element', () => {
- render( )
+ render( )
expect(screen.getByRole('status')).toHaveTextContent('Info title')
})
- it('by default does not render an icon for neutral tone', () => {
+ it('by default does not render an icon for neutral type', () => {
expect(screen.queryByTestId('banner-icon-neutral')).not.toBeInTheDocument()
expect(screen.queryByRole('img')).not.toBeInTheDocument()
})
- it('renders a custom icon for neutral tone', () => {
- render(Custom Icon} description="Info" />)
+ it('renders a custom icon for neutral type', () => {
+ render(Custom Icon} description="Info" />)
expect(screen.getByText('Custom Icon')).toBeInTheDocument()
})
- it('renders an image for neutral tone', () => {
- render( } description="Info" />)
+ it('renders an image for neutral type', () => {
+ render( } description="Info" />)
expect(screen.getByAltText('Custom Image')).toBeInTheDocument()
})
test.each([
- ['info' as SystemBannerTone, 'info'],
- ['upgrade' as SystemBannerTone, 'upgrade'],
- ['experiment' as SystemBannerTone, 'experiment'],
- ['warning' as SystemBannerTone, 'warning'],
- ['error' as SystemBannerTone, 'error'],
- ['success' as SystemBannerTone, 'success'],
- ])('renders a different icon according to the tone', (tone, expectedTone) => {
- render( )
+ ['info' as SystemBannerType, 'info'],
+ ['upgrade' as SystemBannerType, 'upgrade'],
+ ['experiment' as SystemBannerType, 'experiment'],
+ ['warning' as SystemBannerType, 'warning'],
+ ['error' as SystemBannerType, 'error'],
+ ['success' as SystemBannerType, 'success'],
+ ])('renders a different icon according to the type', (type, expectedtype) => {
+ render( )
expect(
- within(screen.getByRole('status')).getByTestId(`banner-icon-${expectedTone}`),
+ within(screen.getByRole('status')).getByTestId(`banner-icon-${expectedtype}`),
).toBeInTheDocument()
})
it('passes through aria-related attributes', () => {
- render( )
+ render( )
expect(screen.queryByRole('status')).not.toBeInTheDocument()
expect(screen.getByRole('status', { hidden: true })).toBeInTheDocument()
})
@@ -53,7 +53,7 @@ describe('Banner', () => {
it('passes through data-* attributes', () => {
render(
{
it('does not pass through other attributes such as className, or exceptionallySetClassName', () => {
render(
{
it('honors rich text in the title', () => {
render(
This is really important
@@ -96,7 +96,7 @@ describe('Banner', () => {
it('honors rich text in the description', () => {
render(
This is really important
@@ -110,12 +110,12 @@ describe('Banner', () => {
})
it('uses the title as the accessible name', () => {
- render( )
+ render( )
expect(screen.getByRole('status', { name: 'Hello World' })).toBeInTheDocument()
})
it("uses the description as the accessible name if there isn't a title", () => {
- render( )
+ render( )
expect(screen.getByRole('status', { name: 'Hello World' })).toBeInTheDocument()
})
@@ -123,7 +123,7 @@ describe('Banner', () => {
const onClickSpy = jest.fn()
render(
{
it('renders action link', () => {
render(
{
it('renders inline link', () => {
render(
,
@@ -170,7 +170,7 @@ describe('Banner', () => {
it('renders multiple inline links', () => {
render(
Info
}
inlineLinks={[
{ label: 'Learn more', href: '#' },
@@ -185,17 +185,17 @@ describe('Banner', () => {
it('renders close button', () => {
const onClose = jest.fn()
- const { rerender } = render( )
+ const { rerender } = render( )
expect(screen.queryByRole('button', { name: 'Close banner' })).not.toBeInTheDocument()
- rerender( )
+ rerender( )
// close button is rendered twice because depending on banner size it can be in two places,
// but only one is visible at a time (the other is set to display: none with CSS)
expect(screen.getAllByRole('button', { name: 'Close banner' })).toHaveLength(2)
rerender(
{
const onClose = jest.fn()
render(
{
it('renders more than one banner with no a11y violations', async () => {
const { container } = render(
<>
-
+
Custom Icon}
onClose={jest.fn()}
inlineLinks={[{ label: 'Link 1', href: '#' }]}
/>
-
+
>,
)
const results = await axe(container)
diff --git a/src/banner/banner.tsx b/src/banner/banner.tsx
index 039b7b84c..021f20c53 100644
--- a/src/banner/banner.tsx
+++ b/src/banner/banner.tsx
@@ -9,17 +9,17 @@ import { BannerIcon } from '../icons/banner-icon'
import { TextLink } from '../text-link'
/**
- * Represents the tone of a banner.
+ * Represents the type of a banner.
* 'neutral' accepts a custom icon, the rest do not.
* @default 'neutral'
*/
-export type BannerTone = 'neutral' | SystemBannerTone
+export type BannerType = 'neutral' | SystemBannerType
/**
- * Predefined system tones for banners.
- * Each tone has its own preset icon.
+ * Predefined system types for banners.
+ * Each type has its own preset icon.
*/
-export type SystemBannerTone = 'info' | 'upgrade' | 'experiment' | 'warning' | 'error' | 'success'
+export type SystemBannerType = 'info' | 'upgrade' | 'experiment' | 'warning' | 'error' | 'success'
type BaseAction = {
variant: 'primary' | 'tertiary'
@@ -68,11 +68,11 @@ type BaseBanner = {
} & CloseButton
/**
- * Configuration for neutral tone banners.
+ * Configuration for neutral banners.
* Can include either an image, an icon, or neither, but never both.
*/
type NeutralBanner = BaseBanner & {
- tone: Extract
+ type: Extract
} & (
| { image: React.ReactElement; icon?: never }
| { icon: React.ReactElement; image?: never }
@@ -80,11 +80,11 @@ type NeutralBanner = BaseBanner & {
)
/**
- * Configuration for system tone banners.
+ * Configuration for system banners.
* Cannot include custom images or icons as they use preset ones.
*/
type SystemBanner = BaseBanner & {
- tone: SystemBannerTone
+ type: SystemBannerType
image?: never
icon?: never
}
@@ -94,7 +94,7 @@ type BannerProps = NeutralBanner | SystemBanner
const Banner = React.forwardRef(function Banner(
{
id,
- tone,
+ type,
title,
description,
action,
@@ -141,7 +141,7 @@ const Banner = React.forwardRef(function Banner(
- {tone === 'neutral' ? icon : }
+ {type === 'neutral' ? icon : }
{closeButton}
@@ -153,7 +153,7 @@ const Banner = React.forwardRef(function Banner(
) : null}
{description}
{inlineLinks?.map(({ label, ...props }, index) => {
diff --git a/src/icons/banner-icon.tsx b/src/icons/banner-icon.tsx
index 741521981..b0f6ed566 100644
--- a/src/icons/banner-icon.tsx
+++ b/src/icons/banner-icon.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
-import type { SystemBannerTone } from '../banner/banner'
+import type { SystemBannerType } from '../banner/banner'
-const bannerIconForTone: Record = {
+const bannerIconForType: Record = {
info: BannerInfoIcon,
upgrade: BannerUpgradeIcon,
experiment: BannerExperimentIcon,
@@ -10,9 +10,9 @@ const bannerIconForTone: Record = {
success: BannerSuccessIcon,
}
-function BannerIcon({ tone, ...props }: JSX.IntrinsicElements['svg'] & { tone: SystemBannerTone }) {
- const Icon = bannerIconForTone[tone]
- return Icon ? : null
+function BannerIcon({ type, ...props }: JSX.IntrinsicElements['svg'] & { type: SystemBannerType }) {
+ const Icon = bannerIconForType[type]
+ return Icon ? : null
}
function BannerInfoIcon(props: JSX.IntrinsicElements['svg']) {
From c81890b8885c3dbafa739f61baf98a376ea7a120 Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Tue, 7 Jan 2025 10:21:24 +0100
Subject: [PATCH 6/7] chore: adjust story
---
src/banner/banner.stories.mdx | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/banner/banner.stories.mdx b/src/banner/banner.stories.mdx
index 643a9c359..60fe2c233 100644
--- a/src/banner/banner.stories.mdx
+++ b/src/banner/banner.stories.mdx
@@ -233,10 +233,19 @@ A simple banner component meant to be used to _inform_ the user of promotional c
parameters={{ docs: { source: { type: 'code' } } }}
argTypes={{
type: {
- options: ['info', 'error'],
+ options: [
+ 'neutral',
+ 'info',
+ 'upgrade',
+ 'experiment',
+ 'warning',
+ 'error',
+ 'success',
+ ],
control: { type: 'inline-radio' },
defaultValue: 'info',
},
+ image: { control: false },
icon: { control: false },
title: {
control: { type: 'text' },
From 038b2763bdf295e9b28a29cf0720887f28fec247 Mon Sep 17 00:00:00 2001
From: Pedro Alves
Date: Tue, 7 Jan 2025 10:51:07 +0100
Subject: [PATCH 7/7] chore: update story with more dark theme variants
---
src/banner/banner.stories.mdx | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/banner/banner.stories.mdx b/src/banner/banner.stories.mdx
index 60fe2c233..c1bd6721c 100644
--- a/src/banner/banner.stories.mdx
+++ b/src/banner/banner.stories.mdx
@@ -376,6 +376,12 @@ export function DarkModeTemplate() {
gap="large"
>
} description="This is a neutral message" />
+
+
+
+
+
+
}