Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/meteor/client/components/GenericModal/GenericModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { RequiredModalProps } from './withDoNotAskAgain';
import { withDoNotAskAgain } from './withDoNotAskAgain';
import { modalStore } from '../../providers/ModalProvider/ModalStore';

type VariantType = 'danger' | 'warning' | 'info' | 'success';
type VariantType = 'danger' | 'warning' | 'info' | 'success' | 'upsell';

type GenericModalProps = RequiredModalProps & {
variant?: VariantType;
Expand Down Expand Up @@ -38,6 +38,7 @@ const getButtonProps = (variant: VariantType): ComponentProps<typeof Button> =>
case 'danger':
return { danger: true };
case 'warning':
case 'upsell':
return { primary: true };
default:
return {};
Expand Down Expand Up @@ -83,6 +84,8 @@ const GenericModal = ({

const dismissedRef = useRef(true);

const taglineColor = variant === 'upsell' ? 'annotation' : undefined;

const handleConfirm = useEffectEvent(() => {
dismissedRef.current = false;
onConfirm?.();
Expand Down Expand Up @@ -118,7 +121,7 @@ const GenericModal = ({
<Modal.Header>
{renderIcon(icon, variant)}
<Modal.HeaderText>
{tagline && <Modal.Tagline>{tagline}</Modal.Tagline>}
{tagline && <Modal.Tagline color={taglineColor}>{tagline}</Modal.Tagline>}
<Modal.Title id={`${genericModalId}-title`}>{title ?? t('Are_you_sure')}</Modal.Title>
</Modal.HeaderText>
{onClose && <Modal.Close aria-label={t('Close')} onClick={handleCloseButtonClick} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import { axe } from 'jest-axe';

import GenericUpsellModal from './GenericUpsellModal';
import * as stories from './GenericUpsellModal.stories';

const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Premium_capability: 'Premium capability',
Cancel: 'Cancel',
Upgrade: 'Upgrade',
})
.build();

describe('GenericUpsellModal', () => {
const defaultProps = {
title: 'Test Title',
img: 'test-image.png',
onClose: jest.fn(),
};

afterEach(() => {
jest.clearAllMocks();
});

const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);

test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});

test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);

const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should render basic properties', () => {
const props = {
...defaultProps,
subtitle: 'Test Subtitle',
description: 'Test Description',
onCancel: jest.fn(),
onConfirm: jest.fn(),
};
render(<GenericUpsellModal {...props} />, { wrapper: appRoot });

expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Subtitle')).toBeInTheDocument();
expect(screen.getByText('Test Description')).toBeInTheDocument();
});

it('should render with default confirm and cancel buttons', () => {
render(<GenericUpsellModal {...defaultProps} onCancel={jest.fn()} onConfirm={jest.fn()} />, { wrapper: appRoot });

expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Upgrade' })).toBeInTheDocument();
});

it('should render with default tagline', () => {
render(<GenericUpsellModal {...defaultProps} />, { wrapper: appRoot });

expect(screen.getByText('Premium capability')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';

import GenericUpsellModal from '.';

const meta = {
title: 'Components/GenericUpsellModal',
component: GenericUpsellModal,
args: {
onClose: action('onClose'),
img: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z',
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof GenericUpsellModal>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
title: 'This is the title',
subtitle: 'This is a subtitle',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
onCancel: action('onCancel'),
onConfirm: action('onConfirm'),
},
};

export const WithCustomTagLine: Story = {
args: {
...Default.args,
tagline: 'Exclusive',
},
};

export const WithCustomButtons: Story = {
args: {
...Default.args,
cancelText: 'Learn More',
confirmText: 'Contact Sales',
},
};

export const WithAnnotation: Story = {
args: {
...Default.args,
annotation: 'This is an annotation.',
},
};
Original file line number Diff line number Diff line change
@@ -1,82 +1,40 @@
import { Box, Button, Modal } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ReactNode, ReactElement, ComponentProps } from 'react';
import { Box, Modal } from '@rocket.chat/fuselage';
import type { ReactElement, ComponentProps } from 'react';
import { useTranslation } from 'react-i18next';

type GenericUpsellModalProps = {
children?: ReactNode;
tagline?: ReactNode;
cancelText?: ReactNode;
confirmText?: ReactNode;
title: string | ReactElement;
import GenericModal from '../GenericModal';

type GenericUpsellModalProps = Omit<ComponentProps<typeof GenericModal>, 'variant' | 'children' | 'onClose' | 'onDismiss'> & {
subtitle?: string | ReactElement;
description?: string | ReactElement;
icon?: IconName;
img: ComponentProps<typeof Modal.HeroImage>['src'];
onCancel?: () => void;
onClose: () => void;
onConfirm?: () => void;
annotation?: ReactNode;
} & ComponentProps<typeof Modal>;
};

const GenericUpsellModal = ({
tagline,
title,
subtitle,
img,
cancelText,
confirmText,
icon,
description,
onClose,
onCancel,
onConfirm,
annotation,
...props
}: GenericUpsellModalProps) => {
const GenericUpsellModal = ({ tagline, subtitle, img, description, confirmText, icon = null, ...props }: GenericUpsellModalProps) => {
const { t } = useTranslation();

return (
<Modal {...props}>
<Modal.Header>
{icon && <Modal.Icon name={icon} />}
<Modal.HeaderText>
<Modal.Tagline color='font-annotation'>{tagline ?? t('Premium_capability')}</Modal.Tagline>
<Modal.Title>{title}</Modal.Title>
</Modal.HeaderText>
<Modal.Close aria-label={t('Close')} onClick={onClose} />
</Modal.Header>
<Modal.Content>
<Modal.HeroImage src={img} alt='' />
{subtitle && (
<Box is='h3' fontScale='h3'>
{subtitle}
</Box>
)}
{description && (
<Box style={{ whiteSpace: 'break-spaces' }} fontScale='p2' mbs={16}>
{description}
</Box>
)}
</Modal.Content>
<Modal.Footer justifyContent={annotation ? 'space-between' : 'flex-end'}>
{annotation && <Modal.FooterAnnotation>{annotation}</Modal.FooterAnnotation>}
{(onCancel || onConfirm) && (
<Modal.FooterControllers>
{onCancel && (
<Button secondary onClick={onCancel}>
{cancelText ?? t('Cancel')}
</Button>
)}
{onConfirm && (
<Button primary onClick={onConfirm}>
{confirmText ?? t('Upgrade')}
</Button>
)}
</Modal.FooterControllers>
)}
</Modal.Footer>
</Modal>
<GenericModal
{...props}
icon={icon}
tagline={tagline ?? t('Premium_capability')}
variant='upsell'
confirmText={confirmText ?? t('Upgrade')}
>
<Modal.HeroImage src={img} alt='' />
{subtitle && (
<Box is='h3' fontScale='h3'>
{subtitle}
</Box>
)}
{description && (
<Box style={{ whiteSpace: 'break-spaces' }} fontScale='p2' mbs={16}>
{description}
</Box>
)}
</GenericModal>
);
};

Expand Down
Loading
Loading