diff --git a/packages/alert/__tests__/Alert.spec.tsx b/packages/alert/__tests__/Alert.spec.tsx index 856ca2f1a..58732e34f 100644 --- a/packages/alert/__tests__/Alert.spec.tsx +++ b/packages/alert/__tests__/Alert.spec.tsx @@ -85,6 +85,40 @@ describe('Alert', () => { const component = screen.getByTestId('alert'); expect(component).toHaveClass(styles['Alert--wide']); }); + + it('applies flair class when kind is notification', () => { + render(createComponent({ kind: 'notification', 'data-test-id': 'alert' })); + const component = screen.getByTestId('alert'); + expect(component).toHaveClass(styles['Alert--flair-default']); + }); + + it('applies strong flair class when kind is notification and flairLevel is strong', () => { + render( + createComponent({ kind: 'notification', flairLevel: 'strong', 'data-test-id': 'alert' }) + ); + const component = screen.getByTestId('alert'); + expect(component).toHaveClass(styles['Alert--flair-strong']); + }); + }); + + describe('action elements', () => { + it('renders a button when primaryButton prop is passed', () => { + render( + createComponent({ + primaryButton: { children: 'Primary Button', onClick: vi.fn() }, + }) + ); + expect(screen.getByRole('button', { name: 'Primary Button' })).toBeInTheDocument(); + }); + + it('renders a link when passed', () => { + render( + createComponent({ + link: { href: '/', text: 'Link Text', onClick: vi.fn() }, + }) + ); + expect(screen.getByRole('link', { name: 'Link Text' })).toBeInTheDocument(); + }); }); describe('a11y', () => { diff --git a/packages/alert/src/Alert.tsx b/packages/alert/src/Alert.tsx index 9e5fa359d..e37ac5932 100644 --- a/packages/alert/src/Alert.tsx +++ b/packages/alert/src/Alert.tsx @@ -1,6 +1,6 @@ import type { ComponentProps, ReactNode } from 'react'; -import { IconButton } from '@launchpad-ui/button'; +import { IconButton, type ButtonProps, Button, ButtonGroup } from '@launchpad-ui/button'; import { Icon, StatusIcon } from '@launchpad-ui/icons'; import { useControlledState } from '@react-stately/utils'; import { cx } from 'classix'; @@ -23,7 +23,13 @@ type AlertProps = ComponentProps<'div'> & { * displays the style and icon pair associated with the variant. * The default is info. */ - kind?: 'info' | 'success' | 'warning' | 'error'; + kind?: 'info' | 'success' | 'warning' | 'error' | 'notification'; + /** + * Passing in one of `default`, `strong` + * displays the style associated with the variant. + * The default is default. + */ + flairLevel?: 'default' | 'strong'; /** * Passing in one of `small`, `medium` * displays either a small or medium Alert. @@ -58,6 +64,17 @@ type AlertProps = ComponentProps<'div'> & { noIcon?: boolean; header?: ReactNode; + + /** + * Primary action button properties + */ + primaryButton?: ButtonProps; + + link?: { + href: string; + text: string; + onClick?(): void; + }; }; const Alert = ({ @@ -66,6 +83,7 @@ const Alert = ({ compact, isInline, kind = 'info', + flairLevel = 'default', size = 'medium', wide, dismissible, @@ -74,6 +92,8 @@ const Alert = ({ header, dismissed, 'data-test-id': testId = 'alert', + primaryButton, + link, ...rest }: AlertProps) => { const [dismissedState, setDismissedState] = useControlledState(dismissed, false, (val) => @@ -82,13 +102,15 @@ const Alert = ({ const defaultClasses = `${styles.Alert} ${styles[`Alert--${kind}`]}`; const sizeClass = size === 'small' && styles[`Alert--${size}`]; + const flairLevelClass = kind === 'notification' && styles[`Alert--flair-${flairLevel}`]; const classes = cx( defaultClasses, className, isInline ? styles['Alert--inline'] : styles['Alert--bordered'], sizeClass, compact && styles['Alert--compact'], - wide && styles['Alert--wide'] + wide && styles['Alert--wide'], + flairLevelClass ); if (dismissedState) { @@ -100,7 +122,7 @@ const Alert = ({ {...rest} className={classes} data-test-id={testId} - role={['info', 'success'].includes(kind) ? 'status' : 'alert'} + role={['info', 'success', 'notification'].includes(kind) ? 'status' : 'alert'} > {!noIcon && ( )} -
{children}
+
+
{children}
+ {(primaryButton || link) && ( + + {primaryButton && ( + + )} + + )} +
{dismissible && ( ; }, }; + +export const Notification = { + args: { + kind: 'notification', + flairLevel: 'default', + header: 'Heading about a cool thing', + primaryButton: { + children: 'Primary action', + }, + link: { + href: 'https://launchdarkly.com', + text: 'Link to learn more', + }, + children:
Description about the cool thing you want people to know about
, + dismissible: true, + }, +}; diff --git a/packages/icons/src/StatusIcon.tsx b/packages/icons/src/StatusIcon.tsx index 13538bc7b..6aa5eeeb1 100644 --- a/packages/icons/src/StatusIcon.tsx +++ b/packages/icons/src/StatusIcon.tsx @@ -3,7 +3,7 @@ import type { IconProps } from './Icon'; import { Icon } from './Icon'; type StatusIconProps = Omit & { - kind: 'info' | 'success' | 'warning' | 'error'; + kind: 'info' | 'success' | 'warning' | 'error' | 'notification'; }; const StatusIcon = ({ kind, size = 'medium', ...rest }: StatusIconProps) => { @@ -27,6 +27,10 @@ const StatusIcon = ({ kind, size = 'medium', ...rest }: StatusIconProps) => { name = 'info'; ariaLabel = 'Info'; break; + case 'notification': + name = 'notifications'; + ariaLabel = 'Notification'; + break; } return ;