Skip to content

Commit

Permalink
feat(Gallery): add Gallery and FilesGallery components
Browse files Browse the repository at this point in the history
  • Loading branch information
kseniyakuzina committed Dec 15, 2024
1 parent 2602ff7 commit f80db52
Show file tree
Hide file tree
Showing 42 changed files with 1,532 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
/src/components/StoreBadge @NikitaCG
/src/components/Stories @darkgenius
/src/components/ConfirmDialog @kseniya57
/src/components/Gallery @kseniya57
/src/components/FilesGallery @kseniya57
39 changes: 31 additions & 8 deletions src/components/FilePreview/FilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,35 @@ const FILE_ICON: Record<FileType, IconData> = {

export interface FilePreviewProps extends QAProps {
className?: string;
nameClassName?: string;
descriptionClassName?: string;
cardClassName?: string;
previewClassName?: string;
actionsClassName?: string;

file: File;
imageSrc?: string;
description?: string;

onClick?: React.MouseEventHandler<HTMLDivElement>;
actions?: FilePreviewActionProps[];
hideName?: boolean;
}

export function FilePreview({
className,
nameClassName,
descriptionClassName,
cardClassName,
previewClassName,
actionsClassName,
qa,
file,
imageSrc,
description,
onClick,
actions,
hideName,
}: FilePreviewProps) {
const id = useUniqId();

Expand Down Expand Up @@ -110,27 +122,38 @@ export function FilePreview({
return (
<div className={cn(null, className)} data-qa={qa}>
<div
className={cn('card', {clickable, hoverable: clickable || withActions})}
className={cn(
'card',
{clickable, hoverable: clickable || withActions},
cardClassName,
)}
role={clickable ? 'button' : undefined}
onKeyDown={clickable ? onKeyDown : undefined}
tabIndex={clickable ? 0 : undefined}
onClick={handleClick}
>
{isPreviewString ? (
<div className={cn('image')}>
<div className={cn('image', previewClassName)}>
<img className={cn('image-img')} src={previewSrc} alt={file.name} />
</div>
) : (
<div className={cn('icon', {type})}>
<div className={cn('icon', {type}, previewClassName)}>
<Icon className={cn('icon-svg')} data={FILE_ICON[type]} size={20} />
</div>
)}
<Text className={cn('name')} color="secondary" ellipsis title={file.name}>
{file.name}
</Text>
{!hideName && (
<Text
className={cn('name', nameClassName)}
color="secondary"
ellipsis
title={file.name}
>
{file.name}
</Text>
)}
{Boolean(description) && (
<Text
className={cn('description')}
className={cn('description', descriptionClassName)}
color="secondary"
ellipsis
title={description}
Expand All @@ -140,7 +163,7 @@ export function FilePreview({
)}
</div>
{actions?.length ? (
<div className={cn('actions', {hide: hideActions})}>
<div className={cn('actions', {hide: hideActions}, actionsClassName)}>
{actions.map((action, index) => (
<FilePreviewAction
key={`${id}-${index}`}
Expand Down
38 changes: 22 additions & 16 deletions src/components/FilePreview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ A component for displaying the file.

### PropTypes

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

#### Actions

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

| Property | Type | Required | Default | Description |
| ---------- | ------------------------------------------------------------------------------------ | -------- | ------- | ------------------------------ |
| id | `String` | | | Action id |
| icon | `String` | | | Action icon |
| title | `String` | | | Action hint on hover |
| onClick | `function` | | | Action click handler |
| href | `String` | | | Action button href |
| extraProps | `ButtonHTMLAttributes<HTMLButtonElement> \| AnchorHTMLAttributes<HTMLAnchorElement>` | | | Additional action button props |
| Property | Type | Required | Default | Description |
| ---------- | ----------------------------------------- | ---------------------------------------- | ------- | ------------------------------ |
| id | `String` | | | Action id |
| icon | `String` | | | Action icon |
| title | `String` | | | Action hint on hover |
| onClick | `function` | | | Action click handler |
| href | `String` | | | Action button href |
| extraProps | `ButtonHTMLAttributes<HTMLButtonElement>` | AnchorHTMLAttributes<HTMLAnchorElement>` | | Additional action button props |

```jsx

Expand Down
26 changes: 26 additions & 0 deletions src/components/FilesGallery/FilesGallery.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@use '../variables';

$block: '.#{variables.$ns}files-gallery';

#{$block} {
&__active-item-info {
align-self: center;
}

&__file-preview {
width: 100%;
height: 100%;
}

&__file-preview-card {
width: 100%;
min-width: 100%;
height: 100%;
padding: 0;
}

&__file-preview-image {
width: 100%;
height: 100%;
}
}
73 changes: 73 additions & 0 deletions src/components/FilesGallery/FilesGallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';

import {ChevronsCollapseUpRight, ChevronsExpandUpRight} from '@gravity-ui/icons';
import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit';

import {Gallery, GalleryProps} from '../Gallery';
import {block} from '../utils/cn';

import {useFullScreen} from './hooks/useFullScreen';
import {i18n} from './i18n';
import {GalleryFileBase} from './types';
import {renderActiveItemInfo} from './utils/renderActiveItemInfo';
import {renderItemPreview} from './utils/renderItemPreview';

import './FilesGallery.scss';

export const cnFilesGallery = block('files-gallery');

export type FilesGalleryProps<GalleryFileType extends GalleryFileBase> = Omit<
GalleryProps<GalleryFileType>,
'fullScreen' | 'renderItemPreview' | 'renderActiveItemInfo'
> & {};

export const FilesGallery = <GalleryFileType extends GalleryFileBase>({
renderActions: providedRenderActions,
activeItemInfoClassName,
...galleryProps
}: FilesGalleryProps<GalleryFileType>) => {
const {fullScreen, handleSwitchFullScreenMode} = useFullScreen();

const renderActions = React.useCallback<
NonNullable<GalleryProps<GalleryFileType>['renderActions']>
>(
(item) => {
return (
<React.Fragment>
{providedRenderActions?.(item)}
<ActionTooltip
title={fullScreen ? i18n('exit-full-screen') : i18n('enter-full-screen')}
hotkey="Shift+F"
>
<Button
size="l"
view="flat"
extraProps={{
'aria-label': fullScreen
? i18n('exit-full-screen')
: i18n('enter-full-screen'),
}}
onClick={handleSwitchFullScreenMode}
>
<Icon
data={fullScreen ? ChevronsCollapseUpRight : ChevronsExpandUpRight}
/>
</Button>
</ActionTooltip>
</React.Fragment>
);
},
[fullScreen, handleSwitchFullScreenMode, providedRenderActions],
);

return (
<Gallery<GalleryFileType>
fullScreen={fullScreen}
renderItemPreview={renderItemPreview}
renderActions={renderActions}
renderActiveItemInfo={renderActiveItemInfo}
activeItemInfoClassName={cnFilesGallery('active-item-info', activeItemInfoClassName)}
{...galleryProps}
/>
);
};
132 changes: 132 additions & 0 deletions src/components/FilesGallery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
## FilesGallery

The component for rendering file galleries.
The component is responsible for the gallery navigation (keyboard arrows, body side click and header arrow click).
You should provide the renderers for the body and file actions, for example the copy link action and the download action.

### PropTypes

| Property | Type | Required | Values | Default | Description |
| :---------------------------- | :------------------------------------------------- | :------- | :----- | :------ | :---------------------------------------------------- |
| items | `(GalleryFileType extends GalleryFileBase)[]` | Yes | | | The gallery items list |
| initialItemIndex | `Number` | | | 0 | The initial active item index |
| open | `Boolean` | | | | The modal opened state |
| onClose | `() => void` | Yes | | | The modal close handler |
| renderActions | `(activeItem: GalleryFileType) => React.ReactNode` | | | | The gallery actions renderer, accepts the active item |
| renderBody | `(activeItem: GalleryFileType) => React.ReactNode` | Yes | | | The gallery body renderer, accepts the active item |
| modalClassName | `String` | | | | The modal class |
| className | `String` | | | | The modal content class |
| headerClassName | `String` | | | | The gallery header class |
| activeItemInfoClassName | `String` | | | | The active item info class name |
| headerNavigationClassName | `String` | | | | The gallery header navigation class |
| headerActionsClassName | `String` | | | | The gallery actions class |
| footerClassName | `String` | | | | The gallery footer class |
| bodyClassName | `String` | | | | The gallery body class |
| bodyNavigationClassName | `String` | | | | The gallery body navigation class |
| bodyNavigationButtonClassName | `String` | | | | The gallery body navigation button class |
| previewListItemClassName | `String` | | | | The gallery preview list class |
| previewListClassName | `String` | | | | The gallery preview list item class |

### Examples

```tsx
import {
FilesGallery,
FilesGalleryFallbackText,
ImageFileView,
VideoFileView,
DocumentFileView,
} from '@gravity-ui/components';

const FilesGalleryShowcase = () => {
const [open, setOpen] = React.useState(false);

const container = usePortalContainer();

const handleClose = React.useCallback(() => {
setOpen(false);
}, []);

const handleOpen = React.useCallback(() => {
setOpen(true);
}, []);

const renderBody = React.useCallback((activeFile: GalleryFile) => {
switch (activeFile.type) {
case 'image': {
return <ImageFileView src={activeFile.url} />;
}
case 'video': {
return <VideoFileView src={activeFile.url} />;
}
case 'document': {
return <DocumentFileView name={activeFile.data.name} src={activeFile.url} />;
}
case 'text': {
return <Text variant="body-1">{activeFile.text}</Text>;
}
default: {
return <FilesGalleryFallbackText />;
}
}
}, []);

const renderActions = React.useCallback((activeFile: GalleryFile) => {
return (
<React.Fragment>
<CopyToClipboard text={'url' in activeFile ? activeFile.url : activeFile.text}>
{() => (
<div>
<ActionTooltip title="Copy link">
<Button
size="l"
view="flat"
extraProps={{
'aria-label': 'Copy link',
}}
>
<Icon data={Link} />
</Button>
</ActionTooltip>
</div>
)}
</CopyToClipboard>
{'url' in activeFile && (
<ActionTooltip title="Download">
<Button
size="l"
view="flat"
extraProps={{
'aria-label': 'download',
}}
onClick={(event) => event.stopPropagation()}
href={`${activeFile.url}?inline=false`}
target="_blank"
rel="noreferrer"
>
<Icon data={ArrowDownToLine} />
</Button>
</ActionTooltip>
)}
</React.Fragment>
);
}, []);

return (
<React.Fragment>
<Button onClick={handleOpen} view="action" size="l">
Open gallery
</Button>
<FilesGallery<GalleryFile>
theme="dark"
open={open}
onClose={handleClose}
container={container || undefined}
items={files}
renderBody={renderBody}
renderActions={renderActions}
/>
</React.Fragment>
);
};
```
Loading

0 comments on commit f80db52

Please sign in to comment.