Skip to content

Commit

Permalink
feat(Stories): add unstable versions of Stories and StoriesGroup comp…
Browse files Browse the repository at this point in the history
…onents
  • Loading branch information
DarkGenius committed Dec 9, 2024
1 parent 5f3a336 commit 0685024
Show file tree
Hide file tree
Showing 32 changed files with 1,439 additions and 0 deletions.
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,31 @@
"version": "3.12.5",
"description": "",
"license": "MIT",
"exports": {
".": {
"types": "./build/esm/index.d.ts",
"require": "./build/cjs/index.js",
"import": "./build/esm/index.js"
},
"./unstable": {
"types": "./build/esm/components/unstable/index.d.ts",
"require": "./build/cjs/components/unstable/index.js",
"import": "./build/esm/components/unstable/index.js"
}
},
"main": "./build/cjs/index.js",
"module": "./build/esm/index.js",
"types": "./build/esm/index.d.ts",
"typesVersions": {
"*": {
"index.d.ts": [
"./build/esm/index.d.ts"
],
"unstable": [
"./build/esm/components/unstable/index.d.ts"
]
}
},
"sideEffects": [
"*.css",
"*.scss"
Expand Down
52 changes: 52 additions & 0 deletions src/components/unstable/Stories/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Stories

Component for displaying stories. It looks like a carousel in a modal with given places to display text and media.

### PropTypes

| Property | Type | Required | Default | Description |
| :------------------ | :-------------- | :------- | :------ | :----------------------------------------------- |
| open | `Boolean` || | Visibility flag |
| items | `StoriesItem[]` || | List of stories to display |
| initialStoryIndex | `Number` | | 0 | Index of the first story to be displayed |
| onClose | `Function` | | | Action on close |
| onPreviousClick | `Function` | | | Action when switching to previous story |
| onNextClick | `Function` | | | Action when switching to next story |
| disableOutsideClick | `Boolean` | | true | If `true`, do not close stories on click outside |
| className | `string` | | | Stories modal class |
| action | `ButtonProps` | | | Custom action button props for the last step |

### StoriesItem object

| Field | Type | Required | Default | Description |
| ----------- | ------------------ | -------- | ------- | -------------------------------- |
| title | `String` | | | Title |
| description | `String` | | | Main text, deprecated |
| content | `React.ReactNode` | | | Main content |
| url | `String` | | | Link to display more information |
| media | `StoriesItemMedia` | | | Media content |

### StoriesItemMedia object

| Field | Type | Required | Default | Description |
| --------- | -------- | -------- | ------- | --------------------------------- |
| type | `String` | | image | Content type (`image` or `video`) |
| url | `String` || | File link |
| posterUrl | `String` | | | Poster URL (only used for video) |

#### Usage example

```jsx harmony
<Stories
open
items={[
{
title: 'Story title',
content: <b>Story text</b>,
media: {
url: 'https://storage.yandexcloud.net/uikit-storybook-assets/story-picture-2.png',
},
},
]}
/>
```
13 changes: 13 additions & 0 deletions src/components/unstable/Stories/Stories.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use '../../variables';

$block: '.#{variables.$ns}stories';
$borderRadius: 20px;

#{$block} {
--g-modal-border-radius: #{$borderRadius};
--g-modal-margin: 20px;

&__modal-content {
border-radius: $borderRadius;
}
}
135 changes: 135 additions & 0 deletions src/components/unstable/Stories/Stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from 'react';

import type {ModalCloseReason} from '@gravity-ui/uikit';
import {Modal} from '@gravity-ui/uikit';

import {block} from '../../utils/cn';

import {
IndexType,
StoriesLayout,
type StoriesLayoutProps,
} from './components/StoriesLayout/StoriesLayout';
import {useSyncWithLS} from './hooks';
import type {StoriesItem} from './types';

import './Stories.scss';

const b = block('stories');

export interface StoriesProps {
open: boolean;
items: StoriesItem[];
onClose?: (
event: MouseEvent | KeyboardEvent | React.MouseEvent<HTMLElement, MouseEvent>,
reason: ModalCloseReason | 'closeButtonClick',
) => void;
initialStoryIndex?: number;
onPreviousClick?: (storyIndex: number) => void;
onNextClick?: (storyIndex: number) => void;
disableOutsideClick?: boolean;
className?: string;
action?: StoriesLayoutProps['action'];
syncInTabsKey?: string;
}

export function Stories({
open,
onClose,
items,
onPreviousClick,
onNextClick,
initialStoryIndex = 0,
disableOutsideClick = true,
className,
action,
syncInTabsKey,
}: StoriesProps) {
const [storyIndex, setStoryIndex] = React.useState(initialStoryIndex);

const handleClose = React.useCallback<NonNullable<StoriesProps['onClose']>>(
(event, reason) => {
onClose?.(event, reason);
},
[onClose],
);

const {callback: closeWithLS} = useSyncWithLS<NonNullable<StoriesProps['onClose']>>({
callback: (event, reason) => {
onClose?.(event, reason);
},
uniqueKey: `close-story-${syncInTabsKey}`,
});

const handleButtonClose = React.useCallback<
(event: MouseEvent | KeyboardEvent | React.MouseEvent<HTMLElement, MouseEvent>) => void
>(
(event) => {
handleClose(event, 'closeButtonClick');
if (syncInTabsKey) closeWithLS(event, 'closeButtonClick');
},
[handleClose, syncInTabsKey, closeWithLS],
);

const handleGotoPrevious = React.useCallback(() => {
setStoryIndex((currentStoryIndex) => {
if (currentStoryIndex <= 0) {
return 0;
}

const newIndex = currentStoryIndex - 1;
onPreviousClick?.(newIndex);
return newIndex;
});
}, [onPreviousClick]);

const handleGotoNext = React.useCallback(() => {
setStoryIndex((currentStoryIndex) => {
if (currentStoryIndex >= items.length - 1) {
return items.length - 1;
}

const newIndex = currentStoryIndex + 1;
onNextClick?.(newIndex);
return newIndex;
});
}, [items, onNextClick]);

if (items.length === 0) {
return null;
}

// case when items has changed and index has ceased to be valid
if (items[storyIndex] === undefined) {
const correctIndex = items[initialStoryIndex] === undefined ? 0 : initialStoryIndex;
setStoryIndex(correctIndex);

return null;
}

const indexType =
(items.length === 1 && IndexType.Single) ||
(storyIndex === 0 && IndexType.Start) ||
(storyIndex === items.length - 1 && IndexType.End) ||
IndexType.InProccess;

return (
<Modal
open={open}
onClose={handleClose}
disableOutsideClick={disableOutsideClick}
className={b()}
contentClassName={b('modal-content', className)}
>
<StoriesLayout
items={items}
storyIndex={storyIndex}
indexType={indexType}
handleButtonClose={handleButtonClose}
handleGotoNext={handleGotoNext}
handleGotoPrevious={handleGotoPrevious}
action={action}
/>
</Modal>
);
}
103 changes: 103 additions & 0 deletions src/components/unstable/Stories/__stories__/Stories.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';

import {Button} from '@gravity-ui/uikit';
import type {Meta, StoryFn} from '@storybook/react';

import type {StoriesProps} from '../Stories';
import {Stories} from '../Stories';
import type {StoriesItem} from '../types';

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

const items: StoriesItem[] = [
{
title: 'New navigation',
content:
'At the top of the panel is the service navigation for each service. ' +
'Below are common navigation elements: a component for switching between accounts ' +
'and organizations, settings, help center, search, notifications, favorites.',
url: 'https://yandex.eu',
media: {
url: 'https://storage.yandexcloud.net/uikit-storybook-assets/story-picture-2.png',
},
},
{
title: 'New navigation (2)',
content: 'A little more about the new navigation',
media: {
url: 'https://storage.yandexcloud.net/uikit-storybook-assets/sample_960x400_ocean_with_audio.mp4',
type: 'video',
},
},
{
title: 'New navigation (3)',
content: <b>Switch to the new navigation right now</b>,
media: {
url: 'https://storage.yandexcloud.net/uikit-storybook-assets/story-picture-4.png',
},
},
];

const DefaultTemplate: StoryFn<StoriesProps> = (props: StoriesProps) => {
const [visible, setVisible] = React.useState(props.open);

React.useEffect(() => {
setVisible(props.open);
}, [props.open]);

return (
<React.Fragment>
<div>
<Button
onClick={() => {
setVisible(true);
}}
>
Open
</Button>
</div>
<Stories
{...props}
open={visible}
onClose={() => {
setVisible(false);
}}
/>
</React.Fragment>
);
};
export const Default = DefaultTemplate.bind({});
Default.args = {
open: false,
items,
};
Default.argTypes = {
onPreviousClick: {action: 'onPreviousClick'},
onNextClick: {action: 'onNextClick'},
};

export const Single = DefaultTemplate.bind({});
Single.args = {
open: false,
items: [items[0]],
};

export const WithCustomAction = DefaultTemplate.bind({});
WithCustomAction.args = {
open: false,
items: [items[0]],
action: {
view: 'action',
children: 'View examples',
},
};

export const WithSyncInTabs = DefaultTemplate.bind({});
WithSyncInTabs.args = {
open: true,
syncInTabsKey: 'test-story',
items: [items[0]],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@use '../../../../variables';

$block: '.#{variables.$ns}stories-image-view';

#{$block} {
width: auto;
max-width: 100%;
max-height: 100%;
margin: auto;
}
18 changes: 18 additions & 0 deletions src/components/unstable/Stories/components/ImageView/ImageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

import {block} from '../../../../utils/cn';
import type {StoriesItemMedia} from '../../types';

import './ImageView.scss';

const b = block('stories-image-view');

export interface ImageViewProps {
media: StoriesItemMedia;
}

export function ImageView({media}: ImageViewProps) {
const type = media.type || 'image';

return type === 'image' ? <img className={b()} src={media.url} alt="" /> : null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

import {ImageView, VideoView} from '../../components';
import type {StoriesItemMedia} from '../../types';

export interface MediaRendererProps {
media: StoriesItemMedia;
}

export function MediaRenderer({media}: MediaRendererProps) {
return (media.type || 'image') === 'image' ? (
<ImageView media={media} />
) : (
<VideoView media={media} />
);
}
Loading

0 comments on commit 0685024

Please sign in to comment.