-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new component Sheet & PromoSheet
- Loading branch information
Showing
21 changed files
with
1,298 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
hideTopBar | ||
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
27
src/components/PromoSheet/__stories__/PromoSheet.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './PromoSheet'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | | ||
| hideTopBar | `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`) | |
Oops, something went wrong.