Skip to content

Commit c9cf9ca

Browse files
committed
feat(Gallery): add Gallery and FilesGallery components
1 parent 2602ff7 commit c9cf9ca

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1539
-24
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
/src/components/StoreBadge @NikitaCG
1414
/src/components/Stories @darkgenius
1515
/src/components/ConfirmDialog @kseniya57
16+
/src/components/Gallery @kseniya57
17+
/src/components/FilesGallery @kseniya57

src/components/FilePreview/FilePreview.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,35 @@ const FILE_ICON: Record<FileType, IconData> = {
4646

4747
export interface FilePreviewProps extends QAProps {
4848
className?: string;
49+
nameClassName?: string;
50+
descriptionClassName?: string;
51+
cardClassName?: string;
52+
previewClassName?: string;
53+
actionsClassName?: string;
4954

5055
file: File;
5156
imageSrc?: string;
5257
description?: string;
5358

5459
onClick?: React.MouseEventHandler<HTMLDivElement>;
5560
actions?: FilePreviewActionProps[];
61+
hideName?: boolean;
5662
}
5763

5864
export function FilePreview({
5965
className,
66+
nameClassName,
67+
descriptionClassName,
68+
cardClassName,
69+
previewClassName,
70+
actionsClassName,
6071
qa,
6172
file,
6273
imageSrc,
6374
description,
6475
onClick,
6576
actions,
77+
hideName,
6678
}: FilePreviewProps) {
6779
const id = useUniqId();
6880

@@ -110,27 +122,38 @@ export function FilePreview({
110122
return (
111123
<div className={cn(null, className)} data-qa={qa}>
112124
<div
113-
className={cn('card', {clickable, hoverable: clickable || withActions})}
125+
className={cn(
126+
'card',
127+
{clickable, hoverable: clickable || withActions},
128+
cardClassName,
129+
)}
114130
role={clickable ? 'button' : undefined}
115131
onKeyDown={clickable ? onKeyDown : undefined}
116132
tabIndex={clickable ? 0 : undefined}
117133
onClick={handleClick}
118134
>
119135
{isPreviewString ? (
120-
<div className={cn('image')}>
136+
<div className={cn('image', previewClassName)}>
121137
<img className={cn('image-img')} src={previewSrc} alt={file.name} />
122138
</div>
123139
) : (
124-
<div className={cn('icon', {type})}>
140+
<div className={cn('icon', {type}, previewClassName)}>
125141
<Icon className={cn('icon-svg')} data={FILE_ICON[type]} size={20} />
126142
</div>
127143
)}
128-
<Text className={cn('name')} color="secondary" ellipsis title={file.name}>
129-
{file.name}
130-
</Text>
144+
{!hideName && (
145+
<Text
146+
className={cn('name', nameClassName)}
147+
color="secondary"
148+
ellipsis
149+
title={file.name}
150+
>
151+
{file.name}
152+
</Text>
153+
)}
131154
{Boolean(description) && (
132155
<Text
133-
className={cn('description')}
156+
className={cn('description', descriptionClassName)}
134157
color="secondary"
135158
ellipsis
136159
title={description}
@@ -140,7 +163,7 @@ export function FilePreview({
140163
)}
141164
</div>
142165
{actions?.length ? (
143-
<div className={cn('actions', {hide: hideActions})}>
166+
<div className={cn('actions', {hide: hideActions}, actionsClassName)}>
144167
{actions.map((action, index) => (
145168
<FilePreviewAction
146169
key={`${id}-${index}`}

src/components/FilePreview/README.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@ A component for displaying the file.
44

55
### PropTypes
66

7-
| Property | Type | Required | Default | Description |
8-
| :------------------ | :------------------------- | :------: | :------ | :--------------------------------------------------------------------------------------------------------------- |
9-
| file | `File` | yes | | The File interface provides information about files and allows JavaScript in a web page to access their content. |
10-
| imageSrc | `string` | | | source for image preview |
11-
| description | `string` | | | Description displayed under the file name |
12-
| className | `string` | | | Class name for the file container |
13-
| onClick | `function` | | | Click handler for the file container |
14-
| [actions](#actions) | `FilePreviewActionProps[]` | | `[]` | Click handler for the file container |
7+
| Property | Type | Required | Default | Description |
8+
| :------------------- | :------------------------- | :------: | :------ | :--------------------------------------------------------------------------------------------------------------- |
9+
| file | `File` | yes | | The File interface provides information about files and allows JavaScript in a web page to access their content. |
10+
| imageSrc | `string` | | | source for image preview |
11+
| description | `string` | | | Description displayed under the file name |
12+
| className | `string` | | | Class name for the file container |
13+
| nameClassName | `string` | | | Class name for the file name |
14+
| descriptionClassName | `string` | | | Class name for the file description |
15+
| cardClassName | `string` | | | Class name for the file card |
16+
| previewClassName | `string` | | | Class name for the file preview image or icon container |
17+
| actionsClassName | `string` | | | Class name for the file actions |
18+
| onClick | `function` | | | Click handler for the file container |
19+
| [actions](#actions) | `FilePreviewActionProps[]` | | `[]` | Click handler for the file container |
20+
| hideName | `Boolean` | | | Hide the file name |
1521

1622
#### Actions
1723

1824
For a file, you can prescribe actions that will be visible when you hover over it.
1925

20-
| Property | Type | Required | Default | Description |
21-
| ---------- | ------------------------------------------------------------------------------------ | -------- | ------- | ------------------------------ |
22-
| id | `String` | | | Action id |
23-
| icon | `String` | | | Action icon |
24-
| title | `String` | | | Action hint on hover |
25-
| onClick | `function` | | | Action click handler |
26-
| href | `String` | | | Action button href |
27-
| extraProps | `ButtonHTMLAttributes<HTMLButtonElement> \| AnchorHTMLAttributes<HTMLAnchorElement>` | | | Additional action button props |
26+
| Property | Type | Required | Default | Description |
27+
| ---------- | ----------------------------------------- | ---------------------------------------- | ------- | ------------------------------ |
28+
| id | `String` | | | Action id |
29+
| icon | `String` | | | Action icon |
30+
| title | `String` | | | Action hint on hover |
31+
| onClick | `function` | | | Action click handler |
32+
| href | `String` | | | Action button href |
33+
| extraProps | `ButtonHTMLAttributes<HTMLButtonElement>` | AnchorHTMLAttributes<HTMLAnchorElement>` | | Additional action button props |
2834

2935
```jsx
3036

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@use '../variables';
2+
3+
$block: '.#{variables.$ns}files-gallery';
4+
5+
#{$block} {
6+
&__active-item-info {
7+
align-self: center;
8+
}
9+
10+
&__file-preview {
11+
width: 100%;
12+
height: 100%;
13+
}
14+
15+
&__file-preview-card {
16+
width: 100%;
17+
min-width: 100%;
18+
height: 100%;
19+
padding: 0;
20+
}
21+
22+
&__file-preview-image {
23+
width: 100%;
24+
height: 100%;
25+
}
26+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
3+
import {ChevronsCollapseUpRight, ChevronsExpandUpRight} from '@gravity-ui/icons';
4+
import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit';
5+
6+
import {Gallery, GalleryProps} from '../Gallery';
7+
import {block} from '../utils/cn';
8+
9+
import {useFullScreen} from './hooks/useFullScreen';
10+
import {i18n} from './i18n';
11+
import {GalleryFileBase} from './types';
12+
import {renderActiveItemInfo} from './utils/renderActiveItemInfo';
13+
import {renderItemPreview} from './utils/renderItemPreview';
14+
15+
import './FilesGallery.scss';
16+
17+
export const cnFilesGallery = block('files-gallery');
18+
19+
export type FilesGalleryProps<GalleryFileType extends GalleryFileBase> = Omit<
20+
GalleryProps<GalleryFileType>,
21+
'fullScreen' | 'renderItemPreview' | 'renderActiveItemInfo'
22+
> & {};
23+
24+
export const FilesGallery = <GalleryFileType extends GalleryFileBase>({
25+
renderActions: providedRenderActions,
26+
activeItemInfoClassName,
27+
...galleryProps
28+
}: FilesGalleryProps<GalleryFileType>) => {
29+
const {fullScreen, handleSwitchFullScreenMode} = useFullScreen();
30+
31+
const renderActions = React.useCallback<
32+
NonNullable<GalleryProps<GalleryFileType>['renderActions']>
33+
>(
34+
(item) => {
35+
return (
36+
<React.Fragment>
37+
{providedRenderActions?.(item)}
38+
<ActionTooltip
39+
title={fullScreen ? i18n('exit-full-screen') : i18n('enter-full-screen')}
40+
hotkey="Shift+F"
41+
>
42+
<Button
43+
size="l"
44+
view="flat"
45+
extraProps={{
46+
'aria-label': fullScreen
47+
? i18n('exit-full-screen')
48+
: i18n('enter-full-screen'),
49+
}}
50+
onClick={handleSwitchFullScreenMode}
51+
>
52+
<Icon
53+
data={fullScreen ? ChevronsCollapseUpRight : ChevronsExpandUpRight}
54+
/>
55+
</Button>
56+
</ActionTooltip>
57+
</React.Fragment>
58+
);
59+
},
60+
[fullScreen, handleSwitchFullScreenMode, providedRenderActions],
61+
);
62+
63+
return (
64+
<Gallery<GalleryFileType>
65+
fullScreen={fullScreen}
66+
renderItemPreview={renderItemPreview}
67+
renderActions={renderActions}
68+
renderActiveItemInfo={renderActiveItemInfo}
69+
activeItemInfoClassName={cnFilesGallery('active-item-info', activeItemInfoClassName)}
70+
{...galleryProps}
71+
/>
72+
);
73+
};

src/components/FilesGallery/README.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
## FilesGallery
2+
3+
The component for rendering file galleries.
4+
The component is responsible for the gallery navigation (keyboard arrows, body side click and header arrow click).
5+
You should provide the renderers for the body and file actions, for example the copy link action and the download action.
6+
7+
### PropTypes
8+
9+
| Property | Type | Required | Values | Default | Description |
10+
| :---------------------------- | :------------------------------------------------- | :------- | :----- | :------ | :---------------------------------------------------- |
11+
| items | `(GalleryFileType extends GalleryFileBase)[]` | Yes | | | The gallery items list |
12+
| initialItemIndex | `Number` | | | 0 | The initial active item index |
13+
| open | `Boolean` | | | | The modal opened state |
14+
| onClose | `() => void` | Yes | | | The modal close handler |
15+
| renderActions | `(activeItem: GalleryFileType) => React.ReactNode` | | | | The gallery actions renderer, accepts the active item |
16+
| renderBody | `(activeItem: GalleryFileType) => React.ReactNode` | Yes | | | The gallery body renderer, accepts the active item |
17+
| modalClassName | `String` | | | | The modal class |
18+
| className | `String` | | | | The modal content class |
19+
| headerClassName | `String` | | | | The gallery header class |
20+
| activeItemInfoClassName | `String` | | | | The active item info class name |
21+
| headerNavigationClassName | `String` | | | | The gallery header navigation class |
22+
| headerActionsClassName | `String` | | | | The gallery actions class |
23+
| footerClassName | `String` | | | | The gallery footer class |
24+
| bodyClassName | `String` | | | | The gallery body class |
25+
| bodyNavigationClassName | `String` | | | | The gallery body navigation class |
26+
| bodyNavigationButtonClassName | `String` | | | | The gallery body navigation button class |
27+
| previewListItemClassName | `String` | | | | The gallery preview list class |
28+
| previewListClassName | `String` | | | | The gallery preview list item class |
29+
30+
### Examples
31+
32+
```tsx
33+
import {
34+
FilesGallery,
35+
FilesGalleryFallbackText,
36+
ImageFileView,
37+
VideoFileView,
38+
DocumentFileView,
39+
} from '@gravity-ui/components';
40+
41+
const FilesGalleryShowcase = () => {
42+
const [open, setOpen] = React.useState(false);
43+
44+
const container = usePortalContainer();
45+
46+
const handleClose = React.useCallback(() => {
47+
setOpen(false);
48+
}, []);
49+
50+
const handleOpen = React.useCallback(() => {
51+
setOpen(true);
52+
}, []);
53+
54+
const renderBody = React.useCallback((activeFile: GalleryFile) => {
55+
switch (activeFile.type) {
56+
case 'image': {
57+
return <ImageFileView src={activeFile.url} />;
58+
}
59+
case 'video': {
60+
return <VideoFileView src={activeFile.url} />;
61+
}
62+
case 'document': {
63+
return <DocumentFileView name={activeFile.data.name} src={activeFile.url} />;
64+
}
65+
case 'text': {
66+
return <Text variant="body-1">{activeFile.text}</Text>;
67+
}
68+
default: {
69+
return <FilesGalleryFallbackText />;
70+
}
71+
}
72+
}, []);
73+
74+
const renderActions = React.useCallback((activeFile: GalleryFile) => {
75+
return (
76+
<React.Fragment>
77+
<CopyToClipboard text={'url' in activeFile ? activeFile.url : activeFile.text}>
78+
{() => (
79+
<div>
80+
<ActionTooltip title="Copy link">
81+
<Button
82+
size="l"
83+
view="flat"
84+
extraProps={{
85+
'aria-label': 'Copy link',
86+
}}
87+
>
88+
<Icon data={Link} />
89+
</Button>
90+
</ActionTooltip>
91+
</div>
92+
)}
93+
</CopyToClipboard>
94+
{'url' in activeFile && (
95+
<ActionTooltip title="Download">
96+
<Button
97+
size="l"
98+
view="flat"
99+
extraProps={{
100+
'aria-label': 'download',
101+
}}
102+
onClick={(event) => event.stopPropagation()}
103+
href={`${activeFile.url}?inline=false`}
104+
target="_blank"
105+
rel="noreferrer"
106+
>
107+
<Icon data={ArrowDownToLine} />
108+
</Button>
109+
</ActionTooltip>
110+
)}
111+
</React.Fragment>
112+
);
113+
}, []);
114+
115+
return (
116+
<React.Fragment>
117+
<Button onClick={handleOpen} view="action" size="l">
118+
Open gallery
119+
</Button>
120+
<FilesGallery<GalleryFile>
121+
theme="dark"
122+
open={open}
123+
onClose={handleClose}
124+
container={container || undefined}
125+
items={files}
126+
renderBody={renderBody}
127+
renderActions={renderActions}
128+
/>
129+
</React.Fragment>
130+
);
131+
};
132+
```

0 commit comments

Comments
 (0)