diff --git a/CODEOWNERS b/CODEOWNERS index f95a046..76603d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,3 +13,5 @@ /src/components/StoreBadge @NikitaCG /src/components/Stories @darkgenius /src/components/ConfirmDialog @kseniya57 +/src/components/Gallery @kseniya57 +/src/components/FilesGallery @kseniya57 diff --git a/src/components/FilePreview/FilePreview.tsx b/src/components/FilePreview/FilePreview.tsx index ee31103..5664c10 100644 --- a/src/components/FilePreview/FilePreview.tsx +++ b/src/components/FilePreview/FilePreview.tsx @@ -46,6 +46,11 @@ const FILE_ICON: Record = { export interface FilePreviewProps extends QAProps { className?: string; + nameClassName?: string; + descriptionClassName?: string; + cardClassName?: string; + previewClassName?: string; + actionsClassName?: string; file: File; imageSrc?: string; @@ -53,16 +58,23 @@ export interface FilePreviewProps extends QAProps { onClick?: React.MouseEventHandler; actions?: FilePreviewActionProps[]; + hideName?: boolean; } export function FilePreview({ className, + nameClassName, + descriptionClassName, + cardClassName, + previewClassName, + actionsClassName, qa, file, imageSrc, description, onClick, actions, + hideName, }: FilePreviewProps) { const id = useUniqId(); @@ -110,27 +122,38 @@ export function FilePreview({ return (
{isPreviewString ? ( -
+
{file.name}
) : ( -
+
)} - - {file.name} - + {!hideName && ( + + {file.name} + + )} {Boolean(description) && ( {actions?.length ? ( -
+
{actions.map((action, index) => ( \| AnchorHTMLAttributes` | | | 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` | AnchorHTMLAttributes` | | Additional action button props | ```jsx diff --git a/src/components/FilesGallery/FilesGallery.scss b/src/components/FilesGallery/FilesGallery.scss new file mode 100644 index 0000000..7cd06a7 --- /dev/null +++ b/src/components/FilesGallery/FilesGallery.scss @@ -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%; + } +} diff --git a/src/components/FilesGallery/FilesGallery.tsx b/src/components/FilesGallery/FilesGallery.tsx new file mode 100644 index 0000000..441c3b8 --- /dev/null +++ b/src/components/FilesGallery/FilesGallery.tsx @@ -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 = Omit< + GalleryProps, + 'fullScreen' | 'renderItemPreview' | 'renderActiveItemInfo' +> & {}; + +export const FilesGallery = ({ + renderActions: providedRenderActions, + activeItemInfoClassName, + ...galleryProps +}: FilesGalleryProps) => { + const {fullScreen, handleSwitchFullScreenMode} = useFullScreen(); + + const renderActions = React.useCallback< + NonNullable['renderActions']> + >( + (item) => { + return ( + + {providedRenderActions?.(item)} + + + + + ); + }, + [fullScreen, handleSwitchFullScreenMode, providedRenderActions], + ); + + return ( + + fullScreen={fullScreen} + renderItemPreview={renderItemPreview} + renderActions={renderActions} + renderActiveItemInfo={renderActiveItemInfo} + activeItemInfoClassName={cnFilesGallery('active-item-info', activeItemInfoClassName)} + {...galleryProps} + /> + ); +}; diff --git a/src/components/FilesGallery/README.md b/src/components/FilesGallery/README.md new file mode 100644 index 0000000..8a2f8c0 --- /dev/null +++ b/src/components/FilesGallery/README.md @@ -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 ; + } + case 'video': { + return ; + } + case 'document': { + return ; + } + case 'text': { + return {activeFile.text}; + } + default: { + return ; + } + } + }, []); + + const renderActions = React.useCallback((activeFile: GalleryFile) => { + return ( + + + {() => ( +
+ + + +
+ )} +
+ {'url' in activeFile && ( + + + + )} +
+ ); + }, []); + + return ( + + + + theme="dark" + open={open} + onClose={handleClose} + container={container || undefined} + items={files} + renderBody={renderBody} + renderActions={renderActions} + /> + + ); +}; +``` diff --git a/src/components/FilesGallery/__stories__/FilesGallery.stories.tsx b/src/components/FilesGallery/__stories__/FilesGallery.stories.tsx new file mode 100644 index 0000000..247f9bb --- /dev/null +++ b/src/components/FilesGallery/__stories__/FilesGallery.stories.tsx @@ -0,0 +1,172 @@ +import React from 'react'; + +import {ArrowDownToLine, Link} from '@gravity-ui/icons'; +import { + ActionTooltip, + Button, + CopyToClipboard, + Icon, + Text, + usePortalContainer, +} from '@gravity-ui/uikit'; +import type {Meta, StoryFn} from '@storybook/react'; + +import {FilesGallery, FilesGalleryProps} from '../FilesGallery'; +import {DocumentFileView} from '../components/DocumentFileView/DocumentFileView'; +import {ImageFileView} from '../components/ImageFileView/ImageFileView'; +import {VideoFileView} from '../components/VideoFileView/VideoFileView'; +import {GalleryFileBase} from '../types'; + +export default { + title: 'Components/FilesGallery', + component: FilesGallery, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'color-contrast', + enabled: false, + }, + ], + }, + }, + }, +} as Meta; + +type GalleryFile = GalleryFileBase & + ( + | { + type: 'image' | 'video' | 'document'; + url: string; + } + | {type: 'text'; text: string} + ); + +const files: GalleryFile[] = [ + { + type: 'image', + url: 'https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.jpg', + imageSrc: 'https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.jpg', + data: { + name: 'Corgi image', + type: 'image/jpeg', + } as File, + }, + { + type: 'video', + url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + data: { + name: 'Bunny Film', + type: 'video/mp4', + } as File, + }, + { + type: 'text', + text: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum', + data: { + name: 'Some text', + type: 'text', + } as File, + }, + { + type: 'document', + url: 'https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.pdf', + data: { + name: 'Pdf file', + type: 'pdf', + } as File, + }, +]; + +const FilesGalleryShowcaseTemplate: StoryFn> = () => { + 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 ; + } + case 'video': { + return ; + } + case 'document': { + return ; + } + case 'text': { + return {activeFile.text}; + } + } + }, []); + + const renderActions = React.useCallback((activeFile: GalleryFile) => { + return ( + + + {() => ( +
+ + + +
+ )} +
+ {'url' in activeFile && ( + + + + )} +
+ ); + }, []); + + return ( + + + + theme="dark" + open={open} + onClose={handleClose} + container={container || undefined} + items={files} + renderBody={renderBody} + renderActions={renderActions} + /> + + ); +}; + +export const FilesGalleryShowcase = FilesGalleryShowcaseTemplate.bind({}); diff --git a/src/components/FilesGallery/components/DocumentFileView/DocumentFileView.scss b/src/components/FilesGallery/components/DocumentFileView/DocumentFileView.scss new file mode 100644 index 0000000..5e8a724 --- /dev/null +++ b/src/components/FilesGallery/components/DocumentFileView/DocumentFileView.scss @@ -0,0 +1,10 @@ +@use '../../../variables'; + +$block: '.#{variables.$ns}document-file-view'; + +#{$block} { + width: 100%; + height: 100%; + + border: none; +} diff --git a/src/components/FilesGallery/components/DocumentFileView/DocumentFileView.tsx b/src/components/FilesGallery/components/DocumentFileView/DocumentFileView.tsx new file mode 100644 index 0000000..8bf3aca --- /dev/null +++ b/src/components/FilesGallery/components/DocumentFileView/DocumentFileView.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import {block} from '../../../utils/cn'; + +import './DocumentFileView.scss'; + +const cnDocumentFileView = block('document-file-view'); + +export type DocumentFileViewProps = { + className?: string; + name: string; + src: string; + sandbox?: string; +} & React.HTMLAttributes; + +export const DocumentFileView = ({className, name, ...props}: DocumentFileViewProps) => { + return