From f80db521e20898578a6dfdd797282ec06fc5eae9 Mon Sep 17 00:00:00 2001 From: kseniyakuzina Date: Sun, 15 Dec 2024 21:38:41 +0300 Subject: [PATCH] feat(Gallery): add Gallery and FilesGallery components --- CODEOWNERS | 2 + src/components/FilePreview/FilePreview.tsx | 39 +++- src/components/FilePreview/README.md | 38 ++-- src/components/FilesGallery/FilesGallery.scss | 26 +++ src/components/FilesGallery/FilesGallery.tsx | 73 +++++++ src/components/FilesGallery/README.md | 132 ++++++++++++ .../__stories__/FilesGallery.stories.tsx | 172 +++++++++++++++ .../DocumentFileView/DocumentFileView.scss | 10 + .../DocumentFileView/DocumentFileView.tsx | 18 ++ .../components/FallbackText/FallbackText.scss | 19 ++ .../components/FallbackText/FallbackText.tsx | 17 ++ .../components/FallbackText/i18n/en.json | 3 + .../components/FallbackText/i18n/index.ts | 8 + .../components/FallbackText/i18n/ru.json | 3 + .../ImageFileView/ImageFileView.scss | 13 ++ .../ImageFileView/ImageFileView.tsx | 44 ++++ .../VideoFileView/VideoFileView.scss | 13 ++ .../VideoFileView/VideoFileView.tsx | 55 +++++ .../components/VideoFileView/i18n/en.json | 4 + .../components/VideoFileView/i18n/index.ts | 8 + .../components/VideoFileView/i18n/ru.json | 4 + .../FilesGallery/hooks/useFullScreen.ts | 30 +++ src/components/FilesGallery/i18n/en.json | 4 + src/components/FilesGallery/i18n/index.ts | 8 + src/components/FilesGallery/i18n/ru.json | 4 + src/components/FilesGallery/index.ts | 6 + src/components/FilesGallery/types.ts | 4 + .../utils/renderActiveItemInfo.tsx | 22 ++ .../FilesGallery/utils/renderItemPreview.tsx | 18 ++ src/components/Gallery/Gallery.scss | 198 ++++++++++++++++++ src/components/Gallery/Gallery.tsx | 177 ++++++++++++++++ src/components/Gallery/README.md | 127 +++++++++++ .../Gallery/__stories__/Gallery.stories.tsx | 158 ++++++++++++++ .../Gallery/__stories__/GalleryShowcase.scss | 22 ++ src/components/Gallery/assets/arrow-left.svg | 1 + src/components/Gallery/assets/arrow-right.svg | 1 + src/components/Gallery/hooks/useNavigation.ts | 58 +++++ src/components/Gallery/i18n/en.json | 3 + src/components/Gallery/i18n/index.ts | 8 + src/components/Gallery/i18n/ru.json | 3 + src/components/Gallery/index.ts | 1 + src/components/index.ts | 2 + 42 files changed, 1532 insertions(+), 24 deletions(-) create mode 100644 src/components/FilesGallery/FilesGallery.scss create mode 100644 src/components/FilesGallery/FilesGallery.tsx create mode 100644 src/components/FilesGallery/README.md create mode 100644 src/components/FilesGallery/__stories__/FilesGallery.stories.tsx create mode 100644 src/components/FilesGallery/components/DocumentFileView/DocumentFileView.scss create mode 100644 src/components/FilesGallery/components/DocumentFileView/DocumentFileView.tsx create mode 100644 src/components/FilesGallery/components/FallbackText/FallbackText.scss create mode 100644 src/components/FilesGallery/components/FallbackText/FallbackText.tsx create mode 100644 src/components/FilesGallery/components/FallbackText/i18n/en.json create mode 100644 src/components/FilesGallery/components/FallbackText/i18n/index.ts create mode 100644 src/components/FilesGallery/components/FallbackText/i18n/ru.json create mode 100644 src/components/FilesGallery/components/ImageFileView/ImageFileView.scss create mode 100644 src/components/FilesGallery/components/ImageFileView/ImageFileView.tsx create mode 100644 src/components/FilesGallery/components/VideoFileView/VideoFileView.scss create mode 100644 src/components/FilesGallery/components/VideoFileView/VideoFileView.tsx create mode 100644 src/components/FilesGallery/components/VideoFileView/i18n/en.json create mode 100644 src/components/FilesGallery/components/VideoFileView/i18n/index.ts create mode 100644 src/components/FilesGallery/components/VideoFileView/i18n/ru.json create mode 100644 src/components/FilesGallery/hooks/useFullScreen.ts create mode 100644 src/components/FilesGallery/i18n/en.json create mode 100644 src/components/FilesGallery/i18n/index.ts create mode 100644 src/components/FilesGallery/i18n/ru.json create mode 100644 src/components/FilesGallery/index.ts create mode 100644 src/components/FilesGallery/types.ts create mode 100644 src/components/FilesGallery/utils/renderActiveItemInfo.tsx create mode 100644 src/components/FilesGallery/utils/renderItemPreview.tsx create mode 100644 src/components/Gallery/Gallery.scss create mode 100644 src/components/Gallery/Gallery.tsx create mode 100644 src/components/Gallery/README.md create mode 100644 src/components/Gallery/__stories__/Gallery.stories.tsx create mode 100644 src/components/Gallery/__stories__/GalleryShowcase.scss create mode 100644 src/components/Gallery/assets/arrow-left.svg create mode 100644 src/components/Gallery/assets/arrow-right.svg create mode 100644 src/components/Gallery/hooks/useNavigation.ts create mode 100644 src/components/Gallery/i18n/en.json create mode 100644 src/components/Gallery/i18n/index.ts create mode 100644 src/components/Gallery/i18n/ru.json create mode 100644 src/components/Gallery/index.ts diff --git a/CODEOWNERS b/CODEOWNERS index f95a046c..76603d48 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 ee31103b..5664c10c 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 00000000..7cd06a78 --- /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 00000000..441c3b8d --- /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 00000000..8a2f8c0e --- /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 00000000..247f9bbb --- /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 00000000..5e8a724e --- /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 00000000..8bf3aca1 --- /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