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/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..c1bd6721c 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 './story-promo-image' -export function Icon(theme) { +export function ArchiveIcon() { return ( - + ) } -export function StarIcon(theme) { +export function getButton(buttonText) { return ( - - - + ) } -export function ArchiveIcon(theme) { - if (theme === 'dark') - return ( - - - - ) +export function PlaygroundTemplate({ type, title, description, action }) { return ( - - + } + title={title} + description={description} + action={action} /> - + ) } -export function EyeIcon(theme) { - if (theme === 'dark') - return ( - - - - ) +export function BannerIconExamples({ theme }) { return ( - - - + + + } + description="This is a neutral message" + /> + + + + + + + + + ) } -export function getButton(buttonText) { +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={() => ({})} + /> + } + 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." + 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={() => ({})} + /> + + ) } -export function PlaygroundTemplate({ tone, title, description, action }) { +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 BannerExamples({ theme }) { +export function BannerImageExamples({ theme }) { return ( - This workspace has used 5 of its 5 project limit. Upgrade to Pro for - more. - - } - action={getButton('Upgrade')} + type="neutral" + title="This is a sample title" + description="Here’s the message below the title, sometimes the copy spans over two lines." + image={} + inlineLinks={[{ label: 'Learn more', href: '#' }]} /> - Members can view but not edit. - - } - action={getButton('Unarchive project')} + type="neutral" + description="Here’s the message below the title, sometimes the copy spans over two lines." + image={} + inlineLinks={[{ label: 'Learn more', href: '#' }]} /> - Members can view but not edit. - - } - /> - } + inlineLinks={[{ label: 'Learn more', href: '#' }]} + onClose={() => ({})} /> @@ -187,20 +225,27 @@ export function BannerExamples({ theme }) { # Banner -A simple banner component meant to be used to _inform_ the user of promotional content or disclaimers. - -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 +343,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 +362,32 @@ 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..b86daaf5b 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, SystemBannerType } 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 type', () => { + expect(screen.queryByTestId('banner-icon-neutral')).not.toBeInTheDocument() + expect(screen.queryByRole('img')).not.toBeInTheDocument() + }) + + 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 type', () => { + 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 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-${expectedtype}`), + ).toBeInTheDocument() }) it('passes through aria-related attributes', () => { - render( -