Skip to content

Commit

Permalink
feat: support 'notification' alert style + action elements
Browse files Browse the repository at this point in the history
  • Loading branch information
cjhensen committed Oct 4, 2023
1 parent 63e8346 commit 3285ced
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 6 deletions.
34 changes: 34 additions & 0 deletions packages/alert/__tests__/Alert.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
62 changes: 57 additions & 5 deletions packages/alert/src/Alert.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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 = ({
Expand All @@ -66,6 +83,7 @@ const Alert = ({
compact,
isInline,
kind = 'info',
flairLevel = 'default',
size = 'medium',
wide,
dismissible,
Expand All @@ -74,6 +92,8 @@ const Alert = ({
header,
dismissed,
'data-test-id': testId = 'alert',
primaryButton,
link,
...rest
}: AlertProps) => {
const [dismissedState, setDismissedState] = useControlledState(dismissed, false, (val) =>
Expand All @@ -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) {
Expand All @@ -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 && (
<StatusIcon
Expand All @@ -116,7 +138,37 @@ const Alert = ({
{header}
</h4>
)}
<div>{children}</div>
<div>
<div>{children}</div>
{(primaryButton || link) && (
<ButtonGroup>
{primaryButton && (
<Button
{...primaryButton}
kind={
primaryButton.kind ||
(flairLevel === 'strong' || kind !== 'notification'
? 'default'
: 'defaultFlair')
}
className={cx(primaryButton.className, styles['PrimaryButton'])}
/>
)}

{link && (
<Button
kind="link"
asChild
onClick={link?.onClick}
className={styles['LinkButton']}
icon={<Icon name="open-in-new" size="small" />}
>
<a href={link.href}>{link.text}</a>
</Button>
)}
</ButtonGroup>
)}
</div>
</div>
{dismissible && (
<IconButton
Expand Down
82 changes: 82 additions & 0 deletions packages/alert/src/styles/Alert.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
:root {
--lp-component-alert-notification-flair-strong-color-text: var(--lp-color-white-0);
--lp-component-alert-notification-flair-strong-color-fill: var(--lp-color-white-0);
--lp-component-alert-notification-flair-strong-button-color-bg: var(--lp-color-white-0);
--lp-component-alert-notification-flair-strong-button-color-bg-hover: var(--lp-color-gray-100);
--lp-component-alert-notification-flair-strong-button-color-bg-focus: var(--lp-color-gray-200);
--lp-component-alert-notification-flair-strong-button-color-bg-active: var(--lp-color-gray-200);
--lp-component-alert-notification-flair-strong-button-button-color-border: var(
--lp-color-white-0
);
--lp-component-alert-notification-flair-strong-button-color-text: var(--lp-color-black-300);
--lp-component-alert-notification-flair-strong-button-color-text-hover: var(--lp-color-black-300);
--lp-component-alert-notification-flair-strong-button-color-text-focus: var(--lp-color-black-300);
--lp-component-alert-notification-flair-strong-button-color-text-active: var(
--lp-color-black-300
);
}

.Alert {
position: relative;
display: flex;
Expand Down Expand Up @@ -82,6 +100,70 @@
fill: var(--lp-color-fill-feedback-error);
}

.Alert.Alert--notification.Alert--flair-default {
background: rgb(163 79 222 / 0.15);
}

.Alert.Alert--notification.Alert--flair-strong,
.Alert.Alert--notification.Alert--flair-strong .Alert-heading {
color: var(--lp-component-alert-notification-flair-strong-color-text);
}

.Alert.Alert--notification.Alert--flair-strong .Alert-close svg {
fill: var(--lp-component-alert-notification-flair-strong-color-fill);
}

.Alert.Alert--notification.Alert--flair-strong {
background: linear-gradient(41.76deg, #a34fde -17.05%, #3dd6f5 147.06%),
linear-gradient(0deg, rgb(255 255 255 / 0.3), rgba(255 255 255 0.3));
}

.Alert.Alert--notification.Alert--flair-default.Alert--bordered::before {
content: '';
position: absolute;
inset: 0;
border-radius: 2px;
border: 1px solid transparent;
background: linear-gradient(131.04deg, #a34fde -15.82%, #405bff 118.85%) border-box;
mask:
linear-gradient(#fff 0 0) padding-box,
linear-gradient(#fff 0 0);
mask-composite: xor;
}

.Alert.Alert--notification.Alert--flair-strong.Alert--bordered {
border: 1px solid rgb(255 255 255 / 0.3);
}

.Alert.Alert--notification.Alert--flair-strong .PrimaryButton {
background: var(--lp-component-alert-notification-flair-strong-button-color-bg);
border-color: var(--lp-component-alert-notification-flair-strong-button-button-color-border);
color: var(--lp-component-alert-notification-flair-strong-button-color-text);
}

.Alert.Alert--notification.Alert--flair-strong .PrimaryButton:hover {
background: var(--lp-component-alert-notification-flair-strong-button-color-bg-hover);
color: var(--lp-component-alert-notification-flair-strong-button-color-text-hover);
}

.Alert.Alert--notification.Alert--flair-strong .PrimaryButton:focus {
background: var(--lp-component-alert-notification-flair-strong-button-color-bg-focus);
color: var(--lp-component-alert-notification-flair-strong-button-color-text-focus);
}

.Alert.Alert--notification.Alert--flair-strong .PrimaryButton:active {
background: var(--lp-component-alert-notification-flair-strong-button-color-bg-active);
color: var(--lp-component-alert-notification-flair-strong-button-color-text-active);
}

.Alert.Alert--notification .LinkButton {
text-decoration: none;
}

.Alert.Alert--notification.Alert--flair-strong .LinkButton {
color: var(--lp-component-alert-notification-flair-strong-color-text);
}

.Alert.Alert--info.Alert--bordered {
border: var(--lp-border-width-200) solid var(--lp-color-border-feedback-info);
background-color: var(--lp-color-bg-feedback-info);
Expand Down
22 changes: 22 additions & 0 deletions packages/alert/stories/Alert.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export default {
category: 'Presentation',
},
},
flairLevel: {
table: {
category: 'Presentation',
},
},
size: {
table: {
category: 'Presentation',
Expand Down Expand Up @@ -204,3 +209,20 @@ export const WithControlledDismissed: Story = {
return <Component />;
},
};

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: <div>Description about the cool thing you want people to know about</div>,
dismissible: true,
},
};
6 changes: 5 additions & 1 deletion packages/icons/src/StatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { IconProps } from './Icon';
import { Icon } from './Icon';

type StatusIconProps = Omit<IconProps, 'name'> & {
kind: 'info' | 'success' | 'warning' | 'error';
kind: 'info' | 'success' | 'warning' | 'error' | 'notification';
};

const StatusIcon = ({ kind, size = 'medium', ...rest }: StatusIconProps) => {
Expand All @@ -27,6 +27,10 @@ const StatusIcon = ({ kind, size = 'medium', ...rest }: StatusIconProps) => {
name = 'info';
ariaLabel = 'Info';
break;
case 'notification':
name = 'notifications';
ariaLabel = 'Notification';
break;
}

return <Icon aria-label={`${ariaLabel} icon`} role="img" size={size} {...rest} name={name} />;
Expand Down

0 comments on commit 3285ced

Please sign in to comment.