Repository: gravity-ui/uikit
Pull request #724 (2023-06-08 14:21:51)

feat(Alert): added new component Alert
Just new component for interface.
Start review from `docs` section in Storybook

related to DATAUI-1590

gravity-ui-bot: [Preview](https://preview.gravity-ui.com/uikit/724/) is ready.
<!-- Sticky Pull Request Comment -->
IsaevAlexandr: /fixed

Patch:
diff --git a/src/components/Alert/Alert.scss b/src/components/Alert/Alert.scss
new file mode 100644
index 00000000..d0be391e
--- /dev/null
+++ b/src/components/Alert/Alert.scss
@@ -0,0 +1,21 @@
+@use '../variables';
+
+$block: '.#{variables.$ns}alert';
+
+#{$block} {
+    --yc-alert-border-radius: var(--yc-card-border-radius, 8px);
+
+    border-radius: var(--yc-alert-border-radius);
+
+    &_corners_square {
+        --yc-alert-border-radius: 0px;
+    }
+
+    &__text-content {
+        width: 100%;
+    }
+
+    &__actions_minContent {
+        width: min-content;
+    }
+}
diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx
new file mode 100644
index 00000000..dad0942f
--- /dev/null
+++ b/src/components/Alert/Alert.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+
+import {Xmark} from '@gravity-ui/icons';
+
+import {Button} from '../Button';
+import {Card} from '../Card';
+import {Icon} from '../Icon';
+import {colorText} from '../Text';
+import {Flex, spacing} from '../layout';
+
+import {AlertActions} from './AlertActions';
+import {AlertIcon} from './AlertIcon';
+import {AlertTitle} from './AlertTitle';
+import {DEFAULT_ICON_SIZE, bAlert} from './constants';
+import type {AlertProps} from './types';
+
+export const Alert = (props: AlertProps) => {
+    const {
+        theme = 'normal',
+        view = 'filled',
+        layout = 'vertical',
+        message,
+        className,
+        corners,
+        style,
+        onClose,
+        align,
+    } = props;
+
+    const icon = props.icon || <Alert.Icon theme={theme} view={view} />;
+    const title =
+        typeof props.title === 'string' ? <Alert.Title text={props.title} /> : props.title;
+    const actions = Array.isArray(props.actions) ? (
+        <Alert.Actions items={props.actions} parentLayout={layout} />
+    ) : (
+        props.actions
+    );
+
+    return (
+        <Card
+            style={style}
+            className={bAlert({corners}, spacing({py: 4, px: 5}, className))}
+            theme={theme}
+            view={view}
+        >
+            <Flex gap="3" alignItems={align}>
+                {icon}
+                <Flex direction={layout === 'vertical' ? 'column' : 'row'} gap="5" grow>
+                    <Flex gap="2" grow className={bAlert('text-content')}>
+                        <Flex direction="column" gap="1" grow justifyContent={align}>
+                            {title}
+                            {message}
+                        </Flex>
+                    </Flex>
+                    {actions}
+                </Flex>
+                {onClose && (
+                    <Button view="flat" onClick={onClose}>
+                        <Icon
+                            data={Xmark}
+                            size={DEFAULT_ICON_SIZE}
+                            className={colorText({color: 'secondary'})}
+                        />
+                    </Button>
+                )}
+            </Flex>
+        </Card>
+    );
+};
+
+Alert.Icon = AlertIcon;
+Alert.Title = AlertTitle;
+Alert.Actions = AlertActions;
diff --git a/src/components/Alert/AlertActions.tsx b/src/components/Alert/AlertActions.tsx
new file mode 100644
index 00000000..183cd98b
--- /dev/null
+++ b/src/components/Alert/AlertActions.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import {Button} from '../Button';
+import {Flex} from '../layout';
+
+import {bAlert} from './constants';
+import type {AlertActionsProps} from './types';
+
+export const AlertActions = ({
+    items,
+    children,
+    parentLayout = 'vertical',
+    className,
+}: AlertActionsProps) => {
+    return (
+        <Flex
+            className={bAlert('actions', {minContent: parentLayout === 'horizontal'}, className)}
+            direction="row"
+            gap="3"
+            wrap
+            alignItems={parentLayout === 'horizontal' ? 'center' : 'flex-start'}
+        >
+            {items?.map(({handler, text}, i) => (
+                <Button key={i} onClick={handler}>
+                    {text}
+                </Button>
+            )) || children}
+        </Flex>
+    );
+};
diff --git a/src/components/Alert/AlertIcon.tsx b/src/components/Alert/AlertIcon.tsx
new file mode 100644
index 00000000..036bb1eb
--- /dev/null
+++ b/src/components/Alert/AlertIcon.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+
+import {
+    CircleCheck,
+    CircleCheckFill,
+    CircleInfo,
+    CircleInfoFill,
+    CircleXmark,
+    CircleXmarkFill,
+    TriangleExclamation,
+    TriangleExclamationFill,
+} from '@gravity-ui/icons';
+
+import {Icon, IconData} from '../Icon';
+import {colorText} from '../Text';
+
+import {DEFAULT_ICON_SIZE, bAlert} from './constants';
+import type {AlertIconProps, AlertTheme} from './types';
+
+const typeToIcon: Record<
+    AlertTheme,
+    {filled: React.ElementType; outlined: React.ElementType} | null
+> = {
+    danger: {
+        filled: CircleXmarkFill,
+        outlined: CircleXmark,
+    },
+    info: {
+        filled: CircleInfoFill,
+        outlined: CircleInfo,
+    },
+    positive: {
+        filled: CircleCheckFill,
+        outlined: CircleCheck,
+    },
+    warning: {
+        filled: TriangleExclamationFill,
+        outlined: TriangleExclamation,
+    },
+    normal: null,
+};
+
+export const AlertIcon = ({
+    className,
+    theme,
+    view = 'filled',
+    size = DEFAULT_ICON_SIZE,
+}: AlertIconProps) => {
+    const iconByTheme = typeToIcon[theme];
+
+    if (!iconByTheme) {
+        return null;
+    }
+
+    return (
+        <div
+            className={bAlert(
+                'icon',
+                colorText({color: theme === 'normal' ? undefined : theme}, className),
+            )}
+        >
+            <Icon data={iconByTheme[view] as IconData} size={size} />
+        </div>
+    );
+};
diff --git a/src/components/Alert/AlertTitle.tsx b/src/components/Alert/AlertTitle.tsx
new file mode 100644
index 00000000..72499f92
--- /dev/null
+++ b/src/components/Alert/AlertTitle.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import {Text} from '../Text';
+
+import {bAlert} from './constants';
+import type {AlertTitleProps} from './types';
+
+export const AlertTitle = ({text, className}: AlertTitleProps) => {
+    return (
+        <Text variant="subheader-2" className={bAlert('title', className)}>
+            {text}
+        </Text>
+    );
+};
diff --git a/src/components/Alert/__stories__/Alert.mdx b/src/components/Alert/__stories__/Alert.mdx
new file mode 100644
index 00000000..24084be1
--- /dev/null
+++ b/src/components/Alert/__stories__/Alert.mdx
@@ -0,0 +1,144 @@
+import {Meta} from '@storybook/addon-docs';
+import {
+    DefaultUsage,
+    Variants,
+    WithPlaneActions,
+    WithActionsOverride,
+    WithCloseButton,
+    WithHtmlMessageAndTitle,
+    HorizontalLayoutAndCustomIcon,
+} from './examples';
+import * as LayoutStories from './Alert.stories.tsx';
+
+<Meta of={LayoutStories} />
+
+# Alert
+
+User friendly communication by text
+
+```tsx
+import {Alert} from '@gravity-ui/uikit';
+
+<Alert theme="info" title="Good news, everyone!" message="Today is a good day to do something!" />;
+```
+
+<DefaultUsage />
+
+## Properties:
+
+-   `title` - title of Alert component. Override if by `Alert.Title` component;
+    ```tsx
+    <Alert title={<Alert.Title className={'some-class'}>some text</Alert.Title>} />
+    ```
+-   `message` - body of Alert componet. One of `title` of `message` props are required;
+-   `corners` - 'rounded' | 'square';
+-   `layout` - override actions position (default bottom). Variants:
+
+    -   vertical - bottom (by default);
+    -   horizontal - right;
+
+-   `view`:
+    -   filled
+    -   outlined
+-   `theme`. By default `info`. [Variansts example](#variants):
+    -   normal
+    -   info
+    -   positive
+    -   warning
+    -   danger
+-   `hasClose`. Define function and close button will be appearing;
+-   `actions`. Alert actions;
+-   `icon` - slot to override default icon;
+-   `align` - сenter your content in vertical direction. For example, you wanna icon component to be placed in center of card or height of your actions in horizontal direction more than content height;
+
+### With plane actions:
+
+```tsx
+<Alert
+    theme="danger"
+    message="Are you sure?"
+    actions={[
+        {
+            text: 'Do it!',
+        },
+        {
+            text: 'No way!',
+        },
+    ]}
+/>
+```
+
+<WithPlaneActions />
+
+### With actions override and squere corners
+
+```tsx
+<Alert
+    theme="positive"
+    corners="square"
+    title="Are you sure?"
+    onClose={() => alert('Ok!')}
+    view="outlined"
+    actions={
+        <Alert.Actions>
+            <Button view="outlined">Do it!</Button>
+            <Button view="flat">No way!</Button>
+        </Alert.Actions>
+    }
+/>
+```
+
+<WithActionsOverride />
+
+## With close button
+
+```tsx
+<Alert
+    theme="info"
+    title="Hey, Hero!"
+    message="Wanna save the World?"
+    onClose={() => alert('Ok!')}
+    actions={<Button view="action">Do it!</Button>}
+    // If you wont default aligment behaviour
+    // actions={
+    //     <Alert.Actions>
+    //         <Button view="action">Do it!</Button>
+    //     </Alert.Actions>
+    // }
+/>
+```
+
+<WithCloseButton />
+
+## Html in message or title:
+
+```tsx
+<Alert
+    theme="normal"
+    title={<span dangerouslySetInnerHTML={{__html: '<b>Alert title</b>'}} />}
+    message={<div dangerouslySetInnerHTML={{__html: '<i>Some message text</i>'}} />}
+    onClose={() => alert('Ok!')}
+/>
+```
+
+<WithHtmlMessageAndTitle />
+
+## With horizontal layout and custom icon:
+
+```tsx
+<Alert
+    theme="positive"
+    align="center"
+    icon={<Icon data={HandOk} size={16} />}
+    title="Everything is ok, mister!"
+    onClose={() => alert('Ok!')}
+    layout="horizontal"
+    actions={<Button view="outlined">Thanks</Button>}
+/>
+```
+
+<HorizontalLayoutAndCustomIcon />
+
+## Variants
+
+<Variants />
diff --git a/src/components/Alert/__stories__/Alert.stories.tsx b/src/components/Alert/__stories__/Alert.stories.tsx
new file mode 100644
index 00000000..f6f5ca08
--- /dev/null
+++ b/src/components/Alert/__stories__/Alert.stories.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+
+import type {Meta, StoryFn} from '@storybook/react';
+
+import {Button} from '../../Button';
+import {Col, Row} from '../../layout';
+import {Alert} from '../Alert';
+import type {AlertProps} from '../types';
+
+export default {
+    title: 'Components/Alert',
+    component: Alert,
+} as Meta;
+
+const title = 'Where will you go, hero?';
+const message = 'Choose wisely: the end of the fairy tale depends on your decision';
+const right = 'To the right (lose the horse)';
+const center = 'Straight (find a wife)';
+const left = 'To the left (CC 235.2)';
+
+const stories: AlertProps[] = [
+    {
+        title,
+        message,
+        theme: 'danger',
+        view: 'filled',
+        onClose: () => alert('Close button pressed'),
+        actions: <Button>{right}</Button>,
+    },
+    {
+        title: <div dangerouslySetInnerHTML={{__html: '<b>Some html title</b>'}} />,
+        message,
+        corners: 'square',
+        theme: 'danger',
+        view: 'outlined',
+        onClose: () => alert('Close button pressed'),
+    },
+    {
+        message,
+        theme: 'info',
+        view: 'filled',
+        actions: (
+            <Alert.Actions>
+                <Button>{center}</Button>
+            </Alert.Actions>
+        ),
+    },
+    {
+        title,
+        message,
+        theme: 'info',
+        view: 'outlined',
+    },
+    {
+        title,
+        message,
+        theme: 'positive',
+        view: 'outlined',
+        actions: (
+            <Alert.Actions>
+                <Button view="action">{right}</Button>
+                <Button view="outlined">{center}</Button>
+                <Button view="flat">{left}</Button>
+            </Alert.Actions>
+        ),
+    },
+    {
+        title,
+        message,
+        theme: 'positive',
+        view: 'filled',
+
+        actions: [{text: right, handler: console.log}],
+    },
+    {
+        title,
+        message,
+        theme: 'warning',
+        view: 'filled',
+        layout: 'horizontal',
+        corners: 'square',
+        onClose: () => {},
+        actions: (
+            <Alert.Actions parentLayout="horizontal">
+                <Button view="outlined">{left}</Button>
+            </Alert.Actions>
+        ),
+    },
+    {
+        title,
+        message,
+        theme: 'warning',
+        view: 'outlined',
+        layout: 'horizontal',
+        actions: <Button>{right}</Button>,
+    },
+    {
+        message,
+        theme: 'normal',
+        view: 'outlined',
+    },
+    {
+        title,
+        message,
+        theme: 'normal',
+        view: 'filled',
+    },
+];
+
+const DefaultTemplate: StoryFn<AlertProps> = () => (
+    <Row space="3">
+        {stories.map((props, i) => (
+            <Col s="12" l="6" xxl="4" key={i}>
+                <Alert {...props} />
+            </Col>
+        ))}
+    </Row>
+);
+export const Examples = DefaultTemplate.bind({});
diff --git a/src/components/Alert/__stories__/examples/index.tsx b/src/components/Alert/__stories__/examples/index.tsx
new file mode 100644
index 00000000..7ed32a3f
--- /dev/null
+++ b/src/components/Alert/__stories__/examples/index.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+
+import {HandOk} from '@gravity-ui/icons';
+
+import {Button} from '../../../Button';
+import {Icon} from '../../../Icon';
+import {Col, Row} from '../../../layout';
+import {Alert} from '../../Alert';
+import type {AlertProps} from '../../types';
+
+const title = 'Good news, everyone!';
+const message = 'Today is a good day to do something!';
+
+export const DefaultUsage = () => <Alert theme="info" title={title} message={message} />;
+
+const variants: AlertProps[] = [
+    {
+        theme: 'info',
+        title,
+        message,
+    },
+    {theme: 'warning', title, message},
+    {theme: 'danger', title, message},
+    {theme: 'positive', title, message},
+    {theme: 'normal', title, message},
+    {theme: 'info', view: 'outlined', title, message},
+    {theme: 'warning', view: 'outlined', title, message},
+    {theme: 'danger', view: 'outlined', title, message},
+    {theme: 'positive', view: 'outlined', title, message},
+    {theme: 'normal', view: 'outlined', title, message},
+];
+
+export const Variants = () => (
+    <Row space="3">
+        {variants.map((props, i) => (
+            <Col key={i} s="12" l="6">
+                <Alert {...props} />
+            </Col>
+        ))}
+    </Row>
+);
+
+export const WithPlaneActions = () => (
+    <Alert
+        theme="danger"
+        message="Are you sure?"
+        actions={[
+            {
+                text: 'Do it!',
+                handler: () => {},
+            },
+            {
+                text: 'No way!',
+                handler: () => {},
+            },
+        ]}
+    />
+);
+
+export const WithActionsOverride = () => (
+    <Alert
+        theme="positive"
+        corners="square"
+        title="Are you sure?"
+        onClose={() => alert('Ok!')}
+        view="outlined"
+        actions={
+            <Alert.Actions>
+                <Button view="outlined">Do it!</Button>
+                <Button view="flat">No way!</Button>
+            </Alert.Actions>
+        }
+    />
+);
+
+export const WithCloseButton = () => (
+    <Alert
+        theme="info"
+        title="Hey, Hero!"
+        message="Wanna save the World?"
+        onClose={() => alert('Ok!')}
+        actions={<Button view="action">Do it!</Button>}
+    />
+);
+
+export const WithHtmlMessageAndTitle = () => (
+    <Alert
+        theme="normal"
+        title={<span dangerouslySetInnerHTML={{__html: '<b>Alert title</b>'}} />}
+        message={<div dangerouslySetInnerHTML={{__html: '<i>Some message text</i>'}} />}
+        onClose={() => alert('Ok!')}
+    />
+);
+
+export const HorizontalLayoutAndCustomIcon = () => (
+    <Alert
+        theme="positive"
+        align="center"
+        icon={<Icon data={HandOk} size={16} />}
+        title="Everything is ok, mister!"
+        onClose={() => alert('Ok!')}
+        layout="horizontal"
+        actions={<Button view="outlined">Thanks</Button>}
+    />
+);
diff --git a/src/components/Alert/constants.ts b/src/components/Alert/constants.ts
new file mode 100644
index 00000000..a5e5c48d
--- /dev/null
+++ b/src/components/Alert/constants.ts
@@ -0,0 +1,7 @@
+import {block} from '../utils/cn';
+
+import './Alert.scss';
+
+export const DEFAULT_ICON_SIZE = 18;
+
+export const bAlert = block('alert');
diff --git a/src/components/Alert/index.ts b/src/components/Alert/index.ts
new file mode 100644
index 00000000..f289b963
--- /dev/null
+++ b/src/components/Alert/index.ts
@@ -0,0 +1,2 @@
+export {Alert} from './Alert';
+export type {AlertProps} from './types';
diff --git a/src/components/Alert/types.ts b/src/components/Alert/types.ts
new file mode 100644
index 00000000..4921c2ea
--- /dev/null
+++ b/src/components/Alert/types.ts
@@ -0,0 +1,89 @@
+export type AlertTheme = 'normal' | 'info' | 'positive' | 'warning' | 'danger';
+export type AlertView = 'filled' | 'outlined';
+export type AlertCorners = 'rounded' | 'square';
+
+export interface AlertProps {
+    title?: React.ReactNode;
+    message?: React.ReactNode;
+    theme?: AlertTheme;
+    /**
+     * Override default icons
+     */
+    icon?: React.ReactNode;
+    /**
+     * @default - normal
+     */
+    corners?: AlertCorners;
+    /**
+     * Override actions position
+     *
+     * variants:
+     * - `vertical` - bottom (default);
+     * - `horizontal` - right;
+     */
+    layout?: 'vertical' | 'horizontal';
+    view?: AlertView;
+    /**
+     * Define function and close button will be appear
+     */
+    hasClose?: boolean;
+    onClose?: () => void;
+    /**
+     * Add you Actions to alert component:
+     * - by declaration:
+     * ```tsx
+     * actions: [{text: '...', handler: () => {}}]
+     * ```
+     * - custom component with predicted way:
+     * ```tsx
+     * actions: (
+     *  <Alert.Actions>
+     *      <Button onClick={...}>...</Button>
+     *  </Alert.Actions>
+     * )
+     * ```
+     * - full custom:
+     * ```tsx
+     * actions: (
+     *  <Flex>
+     *      <img src={...} />
+     *      <Text>...</Text>
+     *  </Flex>
+     * )
+     * ```
+     */
+    actions?: React.ReactNode | AlertAction[];
+    /**
+     * Center all content in vertical direction,
+     * useful if for some reason you actions takes more space then text
+     * or needed icon to be on the middle of the card
+     *
+     * @default - 'baseline'
+     */
+    align?: 'center' | 'baseline';
+    style?: React.CSSProperties;
+    className?: string;
+}
+
+export interface AlertAction {
+    text: string;
+    handler?: () => void;
+}
+
+export interface AlertIconProps {
+    theme: AlertTheme;
+    view?: AlertView;
+    size?: number;
+    className?: string;
+}
+
+export interface AlertActionsProps {
+    className?: string;
+    items?: AlertAction[];
+    children?: React.ReactNode | React.ReactNode[];
+    parentLayout?: 'vertical' | 'horizontal';
+}
+export interface AlertTitleProps {
+    className?: string;
+    text: string;
+}
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
index d11011ad..8ec6d82d 100644
--- a/src/components/Card/Card.tsx
+++ b/src/components/Card/Card.tsx
@@ -16,6 +16,7 @@ export type CardSize = 'm' | 'l';
 
 export interface CardProps {
     children: React.ReactNode;
+    style?: React.CSSProperties;
     className?: string;
     /** Card click handler. Available for type: 'selection', 'action' */
     onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
@@ -44,6 +45,7 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(function Card(pr
         onClick,
         disabled,
         selected,
+        style,
     } = props;
     const isTypeAction = type === 'action';
     const isTypeSelection = type === 'selection';
@@ -61,6 +63,7 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(function Card(pr
     return (
         // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
         <div
+            style={style}
             ref={ref}
             className={b(
                 {
diff --git a/src/components/index.ts b/src/components/index.ts
index fbaf00cb..0c4fdc03 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -4,6 +4,7 @@ export * from './theme';
 
 export * from './ActionTooltip';
 export * from './ArrowToggle';
+export * from './Alert';
 export * from './Breadcrumbs';
 export * from './Button';
 export * from './Checkbox';


Test patch:
diff --git a/src/components/Alert/Alert.test.tsx b/src/components/Alert/Alert.test.tsx
new file mode 100644
index 00000000..8be2665d
--- /dev/null
+++ b/src/components/Alert/Alert.test.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+
+import {render, screen} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import {Flex} from '../layout';
+
+import {Alert} from './Alert';
+import type {AlertTheme} from './types';
+
+const title = 'Where will you go, hero?';
+const message = 'Choose wisely: the end of the fairy tale depends on your decision';
+const right = 'To the right (lose the horse)';
+const center = 'Straight (find a wife)';
+const left = 'To the left (CC 235.2)';
+
+describe('Alert', () => {
+    test('render close button if callback passed', async () => {
+        const callback = jest.fn();
+
+        render(<Alert title={title} message={message} onClose={callback} />);
+
+        const buttons = await screen.findAllByRole('button');
+
+        expect(buttons.length).toEqual(1);
+
+        await userEvent.click(buttons[0]);
+
+        expect(callback).toBeCalledTimes(1);
+    });
+
+    test('render actions as buttons if decl passed', async () => {
+        render(
+            <Alert
+                title={title}
+                message={message}
+                actions={[
+                    {
+                        text: right,
+                    },
+                    {
+                        text: center,
+                    },
+                    {
+                        text: left,
+                    },
+                ]}
+            />,
+        );
+
+        expect((await screen.findAllByRole('button')).length).toEqual(3);
+    });
+
+    test('render actions as is if component passed', async () => {
+        render(<Alert title={title} message={message} actions={<Flex>{center}</Flex>} />);
+
+        expect(await screen.findByText(center)).toBeInTheDocument();
+    });
+
+    test('has predicted styles if inline layout rendered', async () => {
+        const {container} = render(
+            <Alert
+                theme="danger"
+                align="center"
+                title={title}
+                layout="horizontal"
+                onClose={jest.fn()}
+                actions={[{text: right}]}
+            />,
+        );
+
+        expect(container).toMatchSnapshot();
+    });
+
+    test.each<AlertTheme>(['danger', 'info', 'positive', 'warning'])(
+        'render correct icon if not normal theme',
+        async (theme) => {
+            const {container} = render(
+                <Alert theme={theme} title={title} message={message} onClose={jest.fn()} />,
+            );
+
+            // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+            const icon = await container.querySelector('.yc-alert__icon');
+
+            expect(icon).toBeInTheDocument();
+        },
+    );
+
+    test("by default don't render icon component", async () => {
+        const {container} = render(<Alert title={title} message={message} onClose={jest.fn()} />);
+
+        // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+        const icon = await container.querySelector('.yc-alert__icon');
+
+        expect(icon).not.toBeInTheDocument();
+    });
+
+    test('can render custom icon component if needed', async () => {
+        const customIconText = "i'ma icon";
+        render(<Alert title={title} message={message} icon={<div>{customIconText}</div>} />);
+
+        expect(await screen.findByText(customIconText)).toBeInTheDocument();
+    });
+});
diff --git a/src/components/Alert/__snapshots__/Alert.test.tsx.snap b/src/components/Alert/__snapshots__/Alert.test.tsx.snap
new file mode 100644
index 00000000..3070108c
--- /dev/null
+++ b/src/components/Alert/__snapshots__/Alert.test.tsx.snap
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Alert has predicted styles if inline layout rendered 1`] = `
+<div>
+  <div
+    class="yc-card yc-card_theme_danger yc-card_view_filled yc-card_type_container yc-card_size_m yc-alert yc-s__py_4 yc-s__px_5"
+  >
+    <div
+      class="yc-flex"
+      style="column-gap: 12px; row-gap: 12px; align-items: center;"
+    >
+      <div
+        class="yc-alert__icon yc-color-text yc-color-text_color_danger"
+      >
+        <svg
+          aria-hidden="true"
+          class="yc-icon"
+          fill="currentColor"
+          height="18"
+          stroke="none"
+          width="18"
+          xmlns="http://www.w3.org/2000/svg"
+          xmlns:xlink="http://www.w3.org/1999/xlink"
+        >
+          <svg
+            fill="none"
+            viewBox="0 0 16 16"
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <path
+              clip-rule="evenodd"
+              d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14ZM6.53 5.47a.75.75 0 0 0-1.06 1.06L6.94 8 5.47 9.47a.75.75 0 1 0 1.06 1.06L8 9.06l1.47 1.47a.75.75 0 1 0 1.06-1.06L9.06 8l1.47-1.47a.75.75 0 1 0-1.06-1.06L8 6.94 6.53 5.47Z"
+              fill="currentColor"
+              fill-rule="evenodd"
+            />
+          </svg>
+        </svg>
+      </div>
+      <div
+        class="yc-flex"
+        style="flex-direction: row; flex-grow: 1; column-gap: 20px; row-gap: 20px;"
+      >
+        <div
+          class="yc-flex yc-alert__text-content"
+          style="flex-grow: 1; column-gap: 8px; row-gap: 8px;"
+        >
+          <div
+            class="yc-flex"
+            style="flex-direction: column; flex-grow: 1; column-gap: 4px; row-gap: 4px; justify-content: center;"
+          >
+            <span
+              class="yc-text yc-text_variant_subheader-2 yc-alert__title"
+            >
+              Where will you go, hero?
+            </span>
+          </div>
+        </div>
+        <div
+          class="yc-flex yc-alert__actions yc-alert__actions_minContent"
+          style="flex-direction: row; flex-wrap: wrap; column-gap: 12px; row-gap: 12px; align-items: center;"
+        >
+          <button
+            class="yc-button yc-button_view_normal yc-button_size_m yc-button_pin_round-round"
+            type="button"
+          >
+            <span
+              class="yc-button__text"
+            >
+              To the right (lose the horse)
+            </span>
+          </button>
+        </div>
+      </div>
+      <button
+        class="yc-button yc-button_view_flat yc-button_size_m yc-button_pin_round-round"
+        type="button"
+      >
+        <span
+          class="yc-button__icon"
+        >
+          <span
+            class="yc-button__icon-inner"
+          >
+            <svg
+              aria-hidden="true"
+              class="yc-icon yc-color-text yc-color-text_color_secondary"
+              fill="currentColor"
+              height="18"
+              stroke="none"
+              width="18"
+              xmlns="http://www.w3.org/2000/svg"
+              xmlns:xlink="http://www.w3.org/1999/xlink"
+            >
+              <svg
+                fill="none"
+                viewBox="0 0 16 16"
+                xmlns="http://www.w3.org/2000/svg"
+              >
+                <path
+                  clip-rule="evenodd"
+                  d="M3.47 3.47a.75.75 0 0 1 1.06 0L8 6.94l3.47-3.47a.75.75 0 1 1 1.06 1.06L9.06 8l3.47 3.47a.75.75 0 1 1-1.06 1.06L8 9.06l-3.47 3.47a.75.75 0 0 1-1.06-1.06L6.94 8 3.47 4.53a.75.75 0 0 1 0-1.06Z"
+                  fill="currentColor"
+                  fill-rule="evenodd"
+                />
+              </svg>
+            </svg>
+          </span>
+        </span>
+      </button>
+    </div>
+  </div>
+</div>
+`;