Skip to content

Commit

Permalink
feat: new component Sheet & PromoSheet
Browse files Browse the repository at this point in the history
  • Loading branch information
Avol-V committed Dec 13, 2022
1 parent 6757919 commit 8a42dd3
Show file tree
Hide file tree
Showing 20 changed files with 1,295 additions and 0 deletions.
68 changes: 68 additions & 0 deletions src/components/PromoSheet/PromoSheet.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@use '../variables';

$block: '.#{variables.$ns}promo-sheet';

#{$block} {
&__content[class] {
width: auto;
padding: var(--yc-promo-sheet-padding);
margin: 0 var(--yc-promo-sheet-margin) var(--yc-promo-sheet-margin);

color: var(--yc-promo-sheet-foreground);
background: var(--yc-promo-sheet-background);
border-radius: var(--yc-promo-sheet-border-radius);
}

&__header {
position: relative;

padding: 0 20px 0 0;
margin: 0 0 var(--yc-promo-sheet-header-margin);
}

&__title {
margin: 0;

font-size: var(--yc-text-header-1-font-size);
line-height: var(--yc-text-header-1-line-height);
}

&__close-button {
position: absolute;
top: calc(12px * -1);
right: calc(12px * -1);
}

&__message {
margin: 0 0 var(--yc-promo-sheet-message-margin);

font-size: var(--yc-text-body-3-font-size);
line-height: var(--yc-text-body-3-line-height);
}

&__image-container {
margin-bottom: var(--yc-promo-sheet-image-margin);
}

&__image {
display: block;

width: 100%;
height: auto;
}

&__action-button {
display: block;
}
}

.yc-root {
--yc-promo-sheet-margin: 8px;
--yc-promo-sheet-padding: 20px;
--yc-promo-sheet-border-radius: 12px;
--yc-promo-sheet-header-margin: 12px;
--yc-promo-sheet-message-margin: 16px;
--yc-promo-sheet-image-margin: 12px;
--yc-promo-sheet-foreground: var(--yc-color-text-light-primary);
--yc-promo-sheet-background: var(--yc-my-color-brand-normal);
}
144 changes: 144 additions & 0 deletions src/components/PromoSheet/PromoSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type {FC} from 'react';
import React, {useState, useCallback, useEffect, useMemo} from 'react';
import {block} from '../utils/cn';
import {CrossIcon} from '../icons/CrossIcon';
import type {ButtonProps, SheetProps} from '../';
import {Button, Icon, Sheet} from '../';

import './PromoSheet.scss';

const cn = block('promo-sheet');

export type PromoSheetProps = {
title: string;
message: string;
actionText: string;
closeText: string;
actionHref?: string;
imageSrc?: string;
className?: string;
contentClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
onActionClick?: ButtonProps['onClick'];
onClose?: SheetProps['onClose'];
};

type ImageSizes = {
width?: number;
height?: number;
};

export const PromoSheet: FC<PromoSheetProps> = ({
title,
message,
actionText,
closeText,
actionHref,
imageSrc,
className,
contentClassName,
imageContainerClassName,
imageClassName,
onActionClick,
onClose,
}) => {
const [visible, setVisible] = useState(true);
const [loaded, setLoaded] = useState(!imageSrc);
const [imageSizes, setImageSizes] = useState<ImageSizes | undefined>();

const handleActionClick = useCallback<NonNullable<PromoSheetProps['onActionClick']>>(
(event) => {
setVisible(false);
onActionClick?.(event);
},
[onActionClick],
);

const handleCloseClick = useCallback<NonNullable<PromoSheetProps['onClose']>>(() => {
setVisible(false);
}, []);

const closeButtonExtraProps = useMemo(
() => ({
'aria-label': closeText,
}),
[closeText],
);

useEffect(() => {
if (!imageSrc) {
setLoaded(true);

return;
}

const image = new Image();

image.onload = () => {
setImageSizes({
width: image.naturalWidth,
height: image.naturalHeight,
});
setLoaded(true);
image.onload = null;
image.onerror = null;
};
image.onerror = () => {
setImageSizes(undefined);
setLoaded(true);
image.onload = null;
image.onerror = null;
};

image.src = imageSrc;
}, [imageSrc]);

return (
<Sheet
className={cn(null, className)}
contentClassName={cn('content', contentClassName)}
visible={visible && loaded}
noTopBar
onClose={onClose}
>
<header className={cn('header')}>
<h2 className={cn('title')}>{title}</h2>
<Button
className={cn('close-button')}
size="xl"
view="flat-contrast"
onClick={handleCloseClick}
extraProps={closeButtonExtraProps}
>
<Icon data={CrossIcon} size={16} />
</Button>
</header>
<p className={cn('message')}>{message}</p>
{imageSrc && (
<div className={cn('image-container', imageContainerClassName)}>
<img
role="presentation"
className={cn('image', imageClassName)}
src={imageSrc}
alt=""
width={imageSizes?.width}
height={imageSizes?.height}
/>
</div>
)}
<div className={cn('actions')}>
<Button
className={cn('action-button')}
size="xl"
view="outlined-contrast"
width="max"
href={actionHref}
onClick={handleActionClick}
>
{actionText}
</Button>
</div>
</Sheet>
);
};
3 changes: 3 additions & 0 deletions src/components/PromoSheet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# PromoSheet

A component for displaying a promo dialog informing the user about a new feature in the service's mobile application.
27 changes: 27 additions & 0 deletions src/components/PromoSheet/__stories__/PromoSheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import type {Meta, Story} from '@storybook/react/types-6-0';
import {actions} from '@storybook/addon-actions';
import type {PromoSheetProps} from '../PromoSheet';
import {PromoSheet} from '../PromoSheet';

export default {
title: 'Components/PromoSheet',
component: PromoSheet,
} as Meta;

const actionsHandlers = actions('onActionClick', 'onClose');

const DefaultTemplate: Story<PromoSheetProps> = (args) => {
return <PromoSheet {...args} {...actionsHandlers} />;
};

export const Default = DefaultTemplate.bind({});

Default.args = {
title: 'Some announcement title',
message:
'Some announcement message with a lot of text. We want to see how it looks like when there is more than one line of text. Check if everything looks OK with margins.',
actionText: 'Action',
closeText: 'Close',
};
Default.parameters = {};
71 changes: 71 additions & 0 deletions src/components/PromoSheet/__tests__/PromoSheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import {act, render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {PromoSheet} from '../PromoSheet';

test('Renders base content', () => {
const title = 'Title text';
const message = 'Message text';
const actionText = 'Action text';
const closeText = 'Close text';

render(
<PromoSheet
title={title}
message={message}
actionText={actionText}
closeText={closeText}
/>,
);

expect(screen.getByRole('heading')).toHaveTextContent(title);
expect(screen.getByText(message)).toBeInTheDocument();
expect(screen.getByRole('button', {name: closeText})).toBeInTheDocument();
expect(screen.getByRole('button', {name: actionText})).toBeInTheDocument();

expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
});

test('Has image when imageSrc property is set', () => {
const originalWindowImage = window.Image;
let onLoad = () => {};

window.Image = class FakeImage {
naturalWidth = 0;
naturalHeight = 0;

set onload(fn: () => void) {
onLoad = fn;
}
} as unknown as typeof Image;

render(<PromoSheet title="" message="" actionText="" closeText="" imageSrc="image.png" />);

window.Image = originalWindowImage;
onLoad();

expect(screen.getByRole('presentation')).toBeInTheDocument();
});

test('Call onActionClick and onClose by action button', async () => {
const handleActionClick = jest.fn();
const handleClose = jest.fn();

render(
<PromoSheet
title=""
message=""
actionText="Action"
closeText=""
onActionClick={handleActionClick}
onClose={handleClose}
/>,
);

const actionButton = screen.getByRole('button', {name: 'Action'});
const user = userEvent.setup();

await act(() => user.click(actionButton));

expect(handleActionClick).toBeCalled();
});
1 change: 1 addition & 0 deletions src/components/PromoSheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PromoSheet';
17 changes: 17 additions & 0 deletions src/components/Sheet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## MobileModal

Sheet component for mobile devices

### PropTypes

| Property | Type | Required | Default | Description |
| :----------------------- | :--------- | :------: | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| visible | `boolean` || | Show/hide sheet |
| allowHideOnContentScroll | `boolean` | | `true` | Enable the behavior in which you can close the sheet window with a swipe down if the content is scrolled to its top (`contentNode.scrollTop === 0`) or has no scroll at all |
| noTopBar | `boolean` | | | Hide top bar with resize handle |
| id | `string` | | `modal` | ID of the sheet, used as hash in URL. It's important to specify different `id` values if there can be more than one sheet on the page |
| title | `string` | | `undefined` | Title of the sheet window |
| className | `string` | | `undefined` | Class name for the sheet window |
| contentClassName | `string` | | `undefined` | Class name for the sheet content |
| swipeAreaClassName | `string` | | `undefined` | Class name for the swipe area |
| onClose | `function` | | `undefined` | Function called when the sheet is closed (when `visible` sets to `false`) |
Loading

0 comments on commit 8a42dd3

Please sign in to comment.