Skip to content

Commit 3265579

Browse files
authored
feat(components): add Alert (#1363)
* feat(components): add `Alert` * fix: style tweaks
1 parent 6657323 commit 3265579

File tree

9 files changed

+288
-8
lines changed

9 files changed

+288
-8
lines changed

.changeset/old-rabbits-shake.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@launchpad-ui/components": patch
3+
"@launchpad-ui/tokens": patch
4+
---
5+
6+
Add `Alert`
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import { render, screen, userEvent } from '../../../test/utils';
4+
import { Alert, Heading, Text } from '../src';
5+
6+
describe('Alert', () => {
7+
it('renders', () => {
8+
render(
9+
<Alert>
10+
<Heading>Heading</Heading>
11+
<Text>Content</Text>
12+
</Alert>,
13+
);
14+
expect(screen.getByRole('alert')).toBeVisible();
15+
});
16+
17+
it('can be dismissed', async () => {
18+
const spy = vi.fn();
19+
const user = userEvent.setup();
20+
render(
21+
<Alert isDismissable onDismiss={spy}>
22+
<Heading>Heading</Heading>
23+
<Text>Content</Text>
24+
</Alert>,
25+
);
26+
27+
await user.click(screen.getByRole('button'));
28+
expect(spy).toHaveBeenCalledTimes(1);
29+
expect(screen.queryByRole('alert')).toBeNull();
30+
});
31+
});

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@react-aria/toast": "3.0.0-beta.14",
4040
"@react-aria/utils": "3.25.1",
4141
"@react-stately/toast": "3.0.0-beta.5",
42+
"@react-stately/utils": "3.10.2",
4243
"@react-types/shared": "3.24.1",
4344
"class-variance-authority": "0.7.0",
4445
"react-aria": "3.34.1",

packages/components/src/Alert.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { VariantProps } from 'class-variance-authority';
2+
import type { ForwardedRef, HTMLAttributes } from 'react';
3+
4+
import { StatusIcon } from '@launchpad-ui/icons';
5+
import { useControlledState } from '@react-stately/utils';
6+
import { cva } from 'class-variance-authority';
7+
import { forwardRef } from 'react';
8+
import { HeadingContext, Provider } from 'react-aria-components';
9+
10+
import { IconButton } from './IconButton';
11+
12+
import styles from './styles/Alert.module.css';
13+
14+
const alert = cva(styles.base, {
15+
variants: {
16+
status: {
17+
error: styles.error,
18+
info: styles.info,
19+
success: styles.success,
20+
warning: styles.warning,
21+
},
22+
variant: {
23+
default: styles.default,
24+
subtle: styles.subtle,
25+
},
26+
},
27+
defaultVariants: {
28+
status: 'info',
29+
variant: 'default',
30+
},
31+
});
32+
33+
interface AlertVariants extends VariantProps<typeof alert> {}
34+
35+
interface AlertProps extends HTMLAttributes<HTMLDivElement>, AlertVariants {
36+
isDismissable?: boolean;
37+
isOpen?: boolean;
38+
onDismiss?: () => void;
39+
}
40+
41+
const _Alert = (
42+
{
43+
className,
44+
children,
45+
status = 'info',
46+
variant = 'default',
47+
isDismissable,
48+
isOpen,
49+
onDismiss,
50+
...props
51+
}: AlertProps,
52+
ref: ForwardedRef<HTMLDivElement>,
53+
) => {
54+
const [open, setOpen] = useControlledState(isOpen, true, (val) => !val && onDismiss?.());
55+
56+
return open ? (
57+
<div ref={ref} {...props} role="alert" className={alert({ status, variant, className })}>
58+
<StatusIcon kind={status || 'info'} className={styles.icon} />
59+
<div className={styles.content}>
60+
<Provider values={[[HeadingContext, { className: styles.heading }]]}>{children}</Provider>
61+
</div>
62+
{isDismissable && (
63+
<IconButton
64+
aria-label="Close"
65+
icon="cancel"
66+
variant="minimal"
67+
size="small"
68+
onPress={() => setOpen(false)}
69+
/>
70+
)}
71+
</div>
72+
) : null;
73+
};
74+
75+
const Alert = forwardRef(_Alert);
76+
77+
export { Alert };
78+
export type { AlertProps };

packages/components/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import './styles/themes.css';
22

3+
export type { AlertProps } from './Alert';
34
export type { BreadcrumbsProps, BreadcrumbProps } from './Breadcrumbs';
45
export type { ButtonProps } from './Button';
56
export type { ButtonGroupProps } from './ButtonGroup';
@@ -56,6 +57,7 @@ export type { ToggleButtonProps } from './ToggleButton';
5657
export type { ToggleIconButtonProps } from './ToggleIconButton';
5758
export type { TooltipProps, TooltipTriggerProps } from './Tooltip';
5859

60+
export { Alert } from './Alert';
5961
export { Breadcrumbs, Breadcrumb } from './Breadcrumbs';
6062
export { Button } from './Button';
6163
export { ButtonGroup } from './ButtonGroup';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
.base {
2+
display: flex;
3+
border-radius: var(--lp-border-radius-regular);
4+
gap: var(--lp-spacing-400);
5+
6+
& [role='group'] {
7+
margin-top: var(--lp-spacing-300);
8+
}
9+
10+
&.error .icon {
11+
color: var(--lp-color-fill-feedback-error);
12+
}
13+
14+
&.info .icon {
15+
color: var(--lp-color-fill-feedback-info);
16+
}
17+
18+
&.success .icon {
19+
color: var(--lp-color-fill-feedback-success);
20+
}
21+
22+
&.warning .icon {
23+
color: var(--lp-color-fill-feedback-warning);
24+
}
25+
}
26+
27+
.default {
28+
align-items: flex-start;
29+
padding: var(--lp-spacing-400);
30+
31+
&.error {
32+
background-color: var(--lp-color-bg-feedback-error);
33+
}
34+
35+
&.info {
36+
background-color: var(--lp-color-bg-feedback-info);
37+
}
38+
39+
&.success {
40+
background-color: var(--lp-color-bg-feedback-success);
41+
}
42+
43+
&.warning {
44+
background-color: var(--lp-color-bg-feedback-warning);
45+
}
46+
47+
&:has( .heading) {
48+
& .icon {
49+
transform: translateY(2px);
50+
}
51+
}
52+
}
53+
54+
.subtle {
55+
align-items: center;
56+
padding: 0;
57+
background-color: transparent;
58+
}
59+
60+
.content {
61+
flex: 1;
62+
display: flex;
63+
flex-direction: column;
64+
font: var(--lp-text-body-2-regular);
65+
}
66+
67+
.heading {
68+
font: var(--lp-text-heading-2-semibold);
69+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
3+
import { Alert, Button, ButtonGroup, Heading, Text } from '../src';
4+
5+
const meta: Meta<typeof Alert> = {
6+
component: Alert,
7+
title: 'Components/Status/Alert',
8+
parameters: {
9+
status: {
10+
type: import.meta.env.STORYBOOK_PACKAGE_STATUS__COMPONENTS,
11+
},
12+
},
13+
};
14+
15+
export default meta;
16+
17+
type Story = StoryObj<typeof Alert>;
18+
19+
export const Info: Story = {
20+
args: {
21+
children: (
22+
<>
23+
<Heading>Heading</Heading>
24+
<Text>Content</Text>
25+
</>
26+
),
27+
},
28+
};
29+
30+
export const ErrorAlert: Story = {
31+
args: {
32+
children: (
33+
<>
34+
<Heading>Heading</Heading>
35+
<Text>Content</Text>
36+
</>
37+
),
38+
status: 'error',
39+
},
40+
name: 'Error',
41+
};
42+
43+
export const Success: Story = {
44+
args: {
45+
children: (
46+
<>
47+
<Heading>Heading</Heading>
48+
<Text>Content</Text>
49+
</>
50+
),
51+
status: 'success',
52+
},
53+
};
54+
55+
export const Warning: Story = {
56+
args: {
57+
children: (
58+
<>
59+
<Heading>Heading</Heading>
60+
<Text>Content</Text>
61+
</>
62+
),
63+
status: 'warning',
64+
},
65+
};
66+
67+
export const Subtle: Story = {
68+
args: {
69+
children: <Text>Content</Text>,
70+
variant: 'subtle',
71+
isDismissable: true,
72+
onDismiss: () => undefined,
73+
},
74+
};
75+
76+
export const Actions: Story = {
77+
args: {
78+
children: (
79+
<>
80+
<Heading>Heading</Heading>
81+
<Text>Content</Text>
82+
<ButtonGroup>
83+
<Button>Label</Button>
84+
<Button variant="minimal">Label</Button>
85+
</ButtonGroup>
86+
</>
87+
),
88+
isDismissable: true,
89+
},
90+
};

packages/tokens/src/color-aliases.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
"$value": "{color.gray.800}"
88
},
99
"error": {
10-
"$value": "{color.system.red.50}",
11-
"dark": "{color.system.red.900}"
10+
"$value": "{color.system.red.0}",
11+
"dark": "{color.system.red.950}"
1212
},
1313
"info": {
14-
"$value": "{color.blue.50}",
15-
"dark": "{color.blue.900}"
14+
"$value": "{color.blue.0}",
15+
"dark": "{color.blue.950}"
1616
},
1717
"success": {
18-
"$value": "{color.system.green.100}",
19-
"dark": "{color.system.green.900}"
18+
"$value": "{color.system.green.0}",
19+
"dark": "{color.system.green.950}"
2020
},
2121
"warning": {
22-
"$value": "{color.system.yellow.100}",
23-
"dark": "{color.system.yellow.900}"
22+
"$value": "{color.system.yellow.0}",
23+
"dark": "{color.system.yellow.950}"
2424
}
2525
},
2626
"interactive": {

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)