From f0917df57faf38eaa85464202f78fb12317f0c35 Mon Sep 17 00:00:00 2001 From: kirill Date: Sat, 28 Sep 2024 18:11:21 +0300 Subject: [PATCH 1/4] feat(FilePreview): add component --- src/components/FilePreview/FilePreview.scss | 148 +++++++++++++++ src/components/FilePreview/FilePreview.tsx | 163 +++++++++++++++++ .../FilePreview/FilePreviewAction.tsx | 49 +++++ .../MobileImagePreview.scss | 45 +++++ .../MobileImagePreview/MobileImagePreview.tsx | 66 +++++++ src/components/FilePreview/README.md | 47 +++++ .../FilePreview/__stories__/Docs.mdx | 7 + .../__stories__/FilePreview.stories.tsx | 160 ++++++++++++++++ .../__tests__/FilePreview.test.tsx | 171 ++++++++++++++++++ src/components/FilePreview/i18n/en.json | 3 + src/components/FilePreview/i18n/index.ts | 8 + src/components/FilePreview/i18n/ru.json | 3 + src/components/FilePreview/index.ts | 4 + src/components/FilePreview/types.ts | 22 +++ src/components/FilePreview/utils.ts | 24 +++ src/components/index.ts | 1 + 16 files changed, 921 insertions(+) create mode 100644 src/components/FilePreview/FilePreview.scss create mode 100644 src/components/FilePreview/FilePreview.tsx create mode 100644 src/components/FilePreview/FilePreviewAction.tsx create mode 100644 src/components/FilePreview/MobileImagePreview/MobileImagePreview.scss create mode 100644 src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx create mode 100644 src/components/FilePreview/README.md create mode 100644 src/components/FilePreview/__stories__/Docs.mdx create mode 100644 src/components/FilePreview/__stories__/FilePreview.stories.tsx create mode 100644 src/components/FilePreview/__tests__/FilePreview.test.tsx create mode 100644 src/components/FilePreview/i18n/en.json create mode 100644 src/components/FilePreview/i18n/index.ts create mode 100644 src/components/FilePreview/i18n/ru.json create mode 100644 src/components/FilePreview/index.ts create mode 100644 src/components/FilePreview/types.ts create mode 100644 src/components/FilePreview/utils.ts diff --git a/src/components/FilePreview/FilePreview.scss b/src/components/FilePreview/FilePreview.scss new file mode 100644 index 000000000..471bdac6e --- /dev/null +++ b/src/components/FilePreview/FilePreview.scss @@ -0,0 +1,148 @@ +@use 'sass:math'; +@use '../variables'; + +$block: '.#{variables.$ns}file-preview'; + +$smallRoundedButtonSize: 24px; + +#{$block} { + --_-box-shadow: none; + --_-border-radius: 4px; + --_-color-base-background: transparent; + + position: relative; + + width: 120px; + + &:hover, + &:focus-within { + #{$block}__actions:not(#{$block}__actions_hide) { + opacity: 1; + } + + --_-color-base-background: var(--g-color-base-simple-hover, rgba(0, 0, 0, 0.05)); + } + + &__actions { + position: absolute; + inset-block-start: -1 * math.div($smallRoundedButtonSize, 2); + inset-inline-end: -1 * math.div($smallRoundedButtonSize, 2); + z-index: 1; + + display: flex; + gap: 4px; + + opacity: 0; + } + + &:hover { + --_-color-base-background: var(--g-color-base-simple-hover); + } + + &__card { + display: flex; + flex-direction: column; + align-items: center; + + position: relative; + outline: none; + + box-shadow: var(--gc-card-box-shadow); + border-radius: var(--_-border-radius); + padding: 4px 10px; + + &_clickable { + cursor: pointer; + } + + &_hoverable { + background-color: var(--_-color-base-background); + } + + &::after { + position: absolute; + inset: 0; + border-radius: var(--_-border-radius); + pointer-events: none; + } + + &:hover { + --_-box-shadow: 0px 3px 10px var(--g-color-sfx-shadow); + } + + &:focus::after { + content: ''; + box-shadow: 0 0 0 2px var(--g-color-line-misc); + } + + &:focus:not(:focus-visible)::after { + box-shadow: none; + } + } + + &__icon { + display: flex; + justify-content: center; + align-items: center; + + border-radius: 4px; + background-color: var(--g-color-base-generic-medium); + + height: 40px; + width: 40px; + + &-svg { + color: var(--g-color-base-background); + } + + &_type { + &_image, + &_video, + &_code, + &_archive, + &_music { + background-color: var(--g-color-base-misc-heavy); + } + &_text { + background-color: var(--g-color-base-info-heavy); + } + &_pdf { + background-color: var(--g-color-base-danger-medium); + } + &_table { + background-color: var(--g-color-base-positive-medium); + } + } + } + + &__name { + margin-block-start: 4px; + } + + &__name, + &__description { + text-align: center; + width: 100%; + } + + &__image { + position: relative; + + border-radius: 4px; + overflow: hidden; + + height: 64px; + width: 96px; + + &-img { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + + object-fit: cover; + + height: 100%; + width: 100%; + } + } +} diff --git a/src/components/FilePreview/FilePreview.tsx b/src/components/FilePreview/FilePreview.tsx new file mode 100644 index 000000000..82d96dc59 --- /dev/null +++ b/src/components/FilePreview/FilePreview.tsx @@ -0,0 +1,163 @@ +import React from 'react'; + +import { + FileZipper as ArchiveIcon, + Code as CodeIcon, + FileQuestion as DefaultIcon, + Picture as ImageIcon, + MusicNote as MusicIcon, + LogoAcrobat as PdfIcon, + LayoutHeaderCellsLarge as TableIcon, + TextAlignLeft as TextIcon, + Filmstrip as VideoIcon, +} from '@gravity-ui/icons'; + +import {useActionHandlers, useUniqId} from '../../hooks'; +import {Icon} from '../Icon'; +import type {IconData} from '../Icon'; +import {Text} from '../Text'; +import {useMobile} from '../mobile'; +import type {QAProps} from '../types'; +import {block} from '../utils/cn'; + +import {FilePreviewAction} from './FilePreviewAction'; +import type {FilePreviewActionProps} from './FilePreviewAction'; +import {MobileImagePreview} from './MobileImagePreview/MobileImagePreview'; +import type {FileType} from './types'; +import {getFileType} from './utils'; + +import './FilePreview.scss'; + +const cn = block('file-preview'); + +const FILE_ICON: Record = { + default: DefaultIcon, + image: ImageIcon, + video: VideoIcon, + code: CodeIcon, + archive: ArchiveIcon, + music: MusicIcon, + text: TextIcon, + pdf: PdfIcon, + table: TableIcon, +}; + +export interface FilePreviewProps extends QAProps { + className?: string; + + file: File; + imageSrc?: string; + description?: string; + + onClick?: React.MouseEventHandler; + actions?: FilePreviewActionProps[]; +} + +export function FilePreview({ + className, + qa, + file, + imageSrc, + description, + onClick, + actions, +}: FilePreviewProps) { + const id = useUniqId(); + + const [previewSrc, setPreviewSrc] = React.useState(imageSrc); + const [showPreviewSheet, setShowPreviewSheet] = React.useState(false); + const mobile = useMobile(); + const type = getFileType(file); + + const {onKeyDown} = useActionHandlers(onClick); + + React.useEffect(() => { + if (imageSrc) return undefined; + + try { + const createdUrl = URL.createObjectURL(file); + + setPreviewSrc(createdUrl); + + return () => { + URL.revokeObjectURL(createdUrl); + }; + } catch (error: unknown) { + return undefined; + } + }, [file, imageSrc]); + + const clickable = Boolean(onClick); + const withActions = Boolean(actions?.length); + + const isPreviewString = typeof previewSrc === 'string'; + const hideActions = isPreviewString && mobile; + + const handleClick: React.MouseEventHandler = (e) => { + if (mobile && isPreviewString && !onClick) { + setShowPreviewSheet(true); + } else { + onClick?.(e); + } + }; + + const handleSheetClose = () => { + setShowPreviewSheet(false); + }; + + return ( +
+
+ {isPreviewString ? ( +
+ {file.name} +
+ ) : ( +
+ +
+ )} + + {file.name} + + {Boolean(description) && ( + + {description} + + )} +
+ {actions?.length ? ( +
+ {actions.map((action, index) => ( + + ))} +
+ ) : null} + + +
+ ); +} + +FilePreview.displayName = 'FilePreview'; diff --git a/src/components/FilePreview/FilePreviewAction.tsx b/src/components/FilePreview/FilePreviewAction.tsx new file mode 100644 index 000000000..f8d65dc54 --- /dev/null +++ b/src/components/FilePreview/FilePreviewAction.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import {ActionTooltip} from '../ActionTooltip'; +import type {ActionTooltipProps} from '../ActionTooltip'; +import {Button} from '../Button'; +import {Icon} from '../Icon'; +import type {IconData} from '../Icon'; + +export interface FilePreviewActionProps { + id?: string; + icon: IconData; + title: string; + href?: string; + disabled?: boolean; + onClick?: React.MouseEventHandler; + extraProps?: + | React.ButtonHTMLAttributes + | React.AnchorHTMLAttributes; + tooltipExtraProps?: Omit; +} + +export function FilePreviewAction({ + id, + icon, + title, + href, + disabled, + onClick, + extraProps, + tooltipExtraProps, +}: FilePreviewActionProps) { + return ( + + + + ); +} + +FilePreviewAction.displayName = 'FilePreviewAction'; diff --git a/src/components/FilePreview/MobileImagePreview/MobileImagePreview.scss b/src/components/FilePreview/MobileImagePreview/MobileImagePreview.scss new file mode 100644 index 000000000..5c0b27d21 --- /dev/null +++ b/src/components/FilePreview/MobileImagePreview/MobileImagePreview.scss @@ -0,0 +1,45 @@ +@use '../../variables'; +@use '@gravity-ui/uikit/styles/mixins'; + +$block: '.#{variables.$ns}mobile-image-preview'; + +#{$block} { + $previewButtonsTop: 15px; + + &__sheet-content { + padding: 0; + } + + &__container { + height: 85vh; + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + + &__image { + width: auto; + max-width: 100%; + max-height: 100%; + } + + &__back-button { + position: absolute; + inset-block-start: $previewButtonsTop; + inset-inline-start: 15px; + } + + &__action-buttons { + position: absolute; + inset-block-start: $previewButtonsTop; + inset-inline-end: 15px; + display: flex; + flex-direction: column-reverse; + gap: 12px; + } + + &__error-label { + @include mixins.text-body-2; + } +} diff --git a/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx b/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx new file mode 100644 index 000000000..56f7cec1c --- /dev/null +++ b/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import {ArrowLeft as ArrowLeftIcon} from '@gravity-ui/icons'; + +import {Button} from '../../Button'; +import {Icon} from '../../Icon'; +import {Sheet} from '../../Sheet'; +import {block} from '../../utils/cn'; +import type {FilePreviewActionProps} from '../FilePreviewAction'; +import i18n from '../i18n'; + +import './MobileImagePreview.scss'; + +const cn = block('mobile-image-preview'); + +export interface FilePreviewProps { + fileName?: string; + previewSrc?: string; + visible: boolean; + onClose: () => void; + actions?: FilePreviewActionProps[]; +} + +export function MobileImagePreview({ + previewSrc, + visible, + onClose, + actions, + fileName, +}: FilePreviewProps) { + const [showError, setShowError] = React.useState(false); + const showSheet = Boolean(previewSrc && visible); + + const handleImagesError = () => { + setShowError(true); + }; + + return ( + +
+ {showError ? ( +
{i18n('label_image-preview-error')}
+ ) : ( + {fileName} + )} + +
+ {actions?.map((action) => ( + + ))} +
+
+
+ ); +} + +MobileImagePreview.displayName = 'MobileImagePreview'; diff --git a/src/components/FilePreview/README.md b/src/components/FilePreview/README.md new file mode 100644 index 000000000..9c47848e7 --- /dev/null +++ b/src/components/FilePreview/README.md @@ -0,0 +1,47 @@ +## FilePreview + +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 | + +#### 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 \| AnchorHTMLAttributes` | | | Additional action button props | + +```jsx + + +``` diff --git a/src/components/FilePreview/__stories__/Docs.mdx b/src/components/FilePreview/__stories__/Docs.mdx new file mode 100644 index 000000000..0affc4883 --- /dev/null +++ b/src/components/FilePreview/__stories__/Docs.mdx @@ -0,0 +1,7 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; +import * as Stories from './FilePreview.stories'; +import Readme from '../README.md?raw'; + + + +{Readme} diff --git a/src/components/FilePreview/__stories__/FilePreview.stories.tsx b/src/components/FilePreview/__stories__/FilePreview.stories.tsx new file mode 100644 index 000000000..780decf1f --- /dev/null +++ b/src/components/FilePreview/__stories__/FilePreview.stories.tsx @@ -0,0 +1,160 @@ +import React from 'react'; + +import {CircleExclamation, Link, Xmark} from '@gravity-ui/icons'; +import type {Meta, StoryFn} from '@storybook/react'; + +import {Flex} from '../../layout'; +import type {FilePreviewProps} from '../FilePreview'; +import {FilePreview} from '../FilePreview'; +import {FILE_TYPES} from '../types'; + +export default { + title: 'Components/Data Display/FilePreview', + component: FilePreview, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'color-contrast', + enabled: false, + }, + ], + }, + }, + }, +} as Meta; + +const CollageTemplate: StoryFn = () => { + return ( +
+ {FILE_TYPES.map((fileType) => ( + alert(`You clicked on the file: ${fileType}`)} + actions={[ + { + icon: Link, + title: 'open on drive', + onClick: () => window.open('https://disk.yandex.com', '_blank'), + }, + { + icon: Xmark, + title: 'delete a file', + onClick: () => alert('Are you sure you want to delete the file?'), + }, + ]} + /> + ))} + alert('Some info'), + }, + { + icon: Xmark, + onClick: () => alert('Are you sure you want to delete the file?'), + title: 'Close', + }, + ]} + /> +
+ ); +}; + +export const Collage = CollageTemplate.bind({}); + +const DefaultTemplate: StoryFn = (args) => { + return ; +}; + +export const Default = DefaultTemplate.bind({}); +Default.args = { + file: {name: 'my-file.docs', type: 'text/docs'} as File, + onClick: () => { + window.open('https://disk.yandex.com', '_blank'); + }, + actions: [ + { + icon: Xmark, + onClick: () => alert('Are you sure you want to delete the file?'), + title: 'Close', + }, + ], +}; + +const NoClickableTemplate: StoryFn> = (args) => { + return ( + + + alert('Are you sure you want to delete the file?'), + title: 'Close', + }, + ]} + /> + { + window.open('https://disk.yandex.com', '_blank'); + }} + actions={[ + { + icon: Xmark, + onClick: () => alert('Are you sure you want to delete the file?'), + title: 'Close', + }, + ]} + /> + + ); +}; + +export const NoClickable = NoClickableTemplate.bind({}); + +const WithoutActionTooltipTemplate: StoryFn> = (args) => { + return ( + + { + window.open('https://disk.yandex.com', '_blank'); + }} + actions={[ + { + icon: Xmark, + onClick: () => alert('Are you sure you want to delete the file?'), + title: 'Close', + tooltipExtraProps: { + disabled: true, + }, + }, + ]} + /> + + ); +}; + +export const WithoutActionTooltip = WithoutActionTooltipTemplate.bind({}); diff --git a/src/components/FilePreview/__tests__/FilePreview.test.tsx b/src/components/FilePreview/__tests__/FilePreview.test.tsx new file mode 100644 index 000000000..1a9b867e0 --- /dev/null +++ b/src/components/FilePreview/__tests__/FilePreview.test.tsx @@ -0,0 +1,171 @@ +import React from 'react'; + +import {CircleExclamation} from '@gravity-ui/icons'; +import userEvent from '@testing-library/user-event'; + +import {render, screen} from '../../../../test-utils/utils'; +import {FilePreview} from '../FilePreview'; + +describe('FilePreview', () => { + test('Renders base content', () => { + const fileName = 'Some file name'; + const fileType = 'image/png'; + const imageSrc = + 'https://storage.yandexcloud.net/uikit-storybook-assets/changelog-dialog-picture-2.png'; + + render(); + + expect(screen.getByText(fileName)).toBeInTheDocument(); + }); + + test('Renders preview image', () => { + const fileName = 'Some file name'; + const fileType = 'image/png'; + const imageSrc = + 'https://storage.yandexcloud.net/uikit-storybook-assets/changelog-dialog-picture-2.png'; + + render(); + + const previewImage = screen.getByRole('img'); + + expect(previewImage).toHaveAttribute('src', imageSrc); + }); + + test('Call onClick handler', async () => { + const qyId = 'file-preview'; + const fileName = 'Some file name'; + const fileType = 'image/png'; + + const clickHandler = jest.fn(); + + render( + , + ); + + const filePreview = screen.getByText(fileName); + + const user = userEvent.setup(); + await user.click(filePreview); + + expect(clickHandler).toBeCalled(); + }); + + test('Renders actions', () => { + const fileName = 'Some file name'; + const fileType = 'image/png'; + const imageSrc = + 'https://storage.yandexcloud.net/uikit-storybook-assets/changelog-dialog-picture-2.png'; + + const firstActionText = 'some hint'; + const secondActionText = 'second hint'; + + render( + , + ); + + const firstAction = screen.getByRole('button', {name: firstActionText}); + expect(firstAction).toBeDefined(); + + const secondAction = screen.getByRole('button', {name: secondActionText}); + expect(secondAction).toBeDefined(); + }); + + test('Call action click handlers', async () => { + const fileName = 'Some file name'; + const fileType = 'image/png'; + + const firstActionsClickHandler = jest.fn(); + const secondActionsClickHandler = jest.fn(); + + render( + , + ); + + const actionButtons = screen.getAllByRole('button'); + + const user = userEvent.setup(); + for (const actionButton of actionButtons) { + await user.click(actionButton); + } + + expect(firstActionsClickHandler).toBeCalled(); + expect(secondActionsClickHandler).toBeCalled(); + }); + + test("Don't Call disabled action click handler", async () => { + const fileName = 'Some file name'; + const fileType = 'image/png'; + + const mockFn = jest.fn(); + + const TestCase = () => { + const [disabled, setDisabled] = React.useState(false); + const [clicksCount, setClicksCount] = React.useState(0); + + const actionsClickHandler = () => { + mockFn(); + setClicksCount((prev) => prev + 1); + + if (clicksCount === 4) { + setDisabled(true); + } + }; + + return ( + + ); + }; + + render(); + + const actionButtons = screen.getAllByRole('button'); + + const user = userEvent.setup(); + for (const actionButton of actionButtons) { + for (let i = 0; i < 10; i++) { + await user.click(actionButton); + } + } + + expect(mockFn).toBeCalledTimes(5); + }); +}); diff --git a/src/components/FilePreview/i18n/en.json b/src/components/FilePreview/i18n/en.json new file mode 100644 index 000000000..5a2b2e2ee --- /dev/null +++ b/src/components/FilePreview/i18n/en.json @@ -0,0 +1,3 @@ +{ + "label_image-preview-error": "Failed to load image" +} diff --git a/src/components/FilePreview/i18n/index.ts b/src/components/FilePreview/i18n/index.ts new file mode 100644 index 000000000..87647e661 --- /dev/null +++ b/src/components/FilePreview/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '../../../i18n'; + +import en from './en.json'; +import ru from './ru.json'; + +const COMPONENT = 'FilePreview'; + +export default addComponentKeysets({en, ru}, COMPONENT); diff --git a/src/components/FilePreview/i18n/ru.json b/src/components/FilePreview/i18n/ru.json new file mode 100644 index 000000000..792489935 --- /dev/null +++ b/src/components/FilePreview/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "label_image-preview-error": "Не удалось загрузить изображение" +} diff --git a/src/components/FilePreview/index.ts b/src/components/FilePreview/index.ts new file mode 100644 index 000000000..717e50604 --- /dev/null +++ b/src/components/FilePreview/index.ts @@ -0,0 +1,4 @@ +export {FilePreview} from './FilePreview'; +export type {FilePreviewProps} from './FilePreview'; +export {getFileType} from './utils'; +export type {FileType} from './types'; diff --git a/src/components/FilePreview/types.ts b/src/components/FilePreview/types.ts new file mode 100644 index 000000000..b9acd0f4f --- /dev/null +++ b/src/components/FilePreview/types.ts @@ -0,0 +1,22 @@ +export type FileType = + | 'default' + | 'image' + | 'video' + | 'code' + | 'archive' + | 'music' + | 'text' + | 'pdf' + | 'table'; + +export const FILE_TYPES: FileType[] = [ + 'default', + 'image', + 'video', + 'code', + 'archive', + 'music', + 'text', + 'pdf', + 'table', +]; diff --git a/src/components/FilePreview/utils.ts b/src/components/FilePreview/utils.ts new file mode 100644 index 000000000..67afcd645 --- /dev/null +++ b/src/components/FilePreview/utils.ts @@ -0,0 +1,24 @@ +import type {FileType} from './types'; +import {FILE_TYPES} from './types'; + +const isFilePreviewFileType = (str: string): str is FileType => + FILE_TYPES.includes(str.toLowerCase() as FileType); + +export function getFileType(fileType: File['type']): FileType; +export function getFileType(file: File): FileType; + +export function getFileType(arg: File | File['type']): FileType { + const fileType: File['type'] = typeof arg === 'string' ? arg : arg.type; + + if (isFilePreviewFileType(fileType)) { + return fileType; + } + + const splittedFileType = fileType.split('/')[0]; + + if (isFilePreviewFileType(splittedFileType)) { + return splittedFileType; + } + + return 'default'; +} diff --git a/src/components/index.ts b/src/components/index.ts index 400a4dfdf..c3ae62ae5 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -20,6 +20,7 @@ export * from './Dialog'; export * from './Disclosure'; export * from './Divider'; export * from './DropdownMenu'; +export * from './FilePreview'; export * from './Hotkey'; export * from './Icon'; export * from './AvatarStack'; From 37dbd659377969e6c70d269a8f36f4cbb7dd8e64 Mon Sep 17 00:00:00 2001 From: kirill Date: Wed, 9 Oct 2024 18:02:59 +0300 Subject: [PATCH 2/4] chore: update readme --- src/components/FilePreview/README.md | 107 +++++++++++++++++++++------ 1 file changed, 86 insertions(+), 21 deletions(-) diff --git a/src/components/FilePreview/README.md b/src/components/FilePreview/README.md index 9c47848e7..2be80de43 100644 --- a/src/components/FilePreview/README.md +++ b/src/components/FilePreview/README.md @@ -1,8 +1,93 @@ + + ## FilePreview + + +```tsx +import {FilePreview} from '@gravity-ui/uikit'; +``` + A component for displaying the file. -### PropTypes + + +`} + +> + + + + + + + + + +### Properties | Property | Type | Required | Default | Description | | :------------------ | :------------------------- | :------: | :------ | :--------------------------------------------------------------------------------------------------------------- | @@ -25,23 +110,3 @@ For a file, you can prescribe actions that will be visible when you hover over i | onClick | `function` | | | Action click handler | | href | `String` | | | Action button href | | extraProps | `ButtonHTMLAttributes \| AnchorHTMLAttributes` | | | Additional action button props | - -```jsx - - -``` From ef274b042cbe647b031cb63741197ad4518b25cf Mon Sep 17 00:00:00 2001 From: kirill Date: Wed, 9 Oct 2024 18:17:20 +0300 Subject: [PATCH 3/4] chore: update readme --- src/components/FilePreview/README.md | 96 ++++++++++++---------------- 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/src/components/FilePreview/README.md b/src/components/FilePreview/README.md index 2be80de43..e6779457a 100644 --- a/src/components/FilePreview/README.md +++ b/src/components/FilePreview/README.md @@ -12,81 +12,69 @@ A component for displaying the file. -`} - -> - - - - +```tsx + +``` - + + + ### Properties | Property | Type | Required | Default | Description | From 6168e5ef4e377790b592e1c798ae55008993108a Mon Sep 17 00:00:00 2001 From: kirill Date: Tue, 15 Oct 2024 17:40:13 +0300 Subject: [PATCH 4/4] fix: some issues --- src/components/FilePreview/FilePreview.scss | 18 ++-- src/components/FilePreview/FilePreview.tsx | 37 ++++--- .../FilePreview/FilePreviewAction.tsx | 7 +- .../MobileImagePreview/MobileImagePreview.tsx | 2 +- src/components/FilePreview/README.md | 100 +++++++++--------- .../FilePreview/__stories__/Docs.mdx | 24 ++++- .../__stories__/FilePreview.stories.tsx | 80 +++++++------- .../__tests__/FilePreview.test.tsx | 14 +-- src/components/FilePreview/index.ts | 1 - src/components/FilePreview/types.ts | 17 +-- 10 files changed, 158 insertions(+), 142 deletions(-) diff --git a/src/components/FilePreview/FilePreview.scss b/src/components/FilePreview/FilePreview.scss index 471bdac6e..10c9a9e7c 100644 --- a/src/components/FilePreview/FilePreview.scss +++ b/src/components/FilePreview/FilePreview.scss @@ -125,7 +125,7 @@ $smallRoundedButtonSize: 24px; width: 100%; } - &__image { + &__image-container { position: relative; border-radius: 4px; @@ -133,16 +133,16 @@ $smallRoundedButtonSize: 24px; height: 64px; width: 96px; + } - &-img { - position: absolute; - inset-block-start: 0; - inset-inline-start: 0; + &-image { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; - object-fit: cover; + object-fit: cover; - height: 100%; - width: 100%; - } + height: 100%; + width: 100%; } } diff --git a/src/components/FilePreview/FilePreview.tsx b/src/components/FilePreview/FilePreview.tsx index 82d96dc59..563387533 100644 --- a/src/components/FilePreview/FilePreview.tsx +++ b/src/components/FilePreview/FilePreview.tsx @@ -13,6 +13,7 @@ import { } from '@gravity-ui/icons'; import {useActionHandlers, useUniqId} from '../../hooks'; +import {useBoolean} from '../../hooks/private'; import {Icon} from '../Icon'; import type {IconData} from '../Icon'; import {Text} from '../Text'; @@ -65,7 +66,7 @@ export function FilePreview({ const id = useUniqId(); const [previewSrc, setPreviewSrc] = React.useState(imageSrc); - const [showPreviewSheet, setShowPreviewSheet] = React.useState(false); + const [isPreviewSheetVisible, showPreviewSheet, closePreviewSheet] = useBoolean(false); const mobile = useMobile(); const type = getFileType(file); @@ -93,17 +94,19 @@ export function FilePreview({ const isPreviewString = typeof previewSrc === 'string'; const hideActions = isPreviewString && mobile; - const handleClick: React.MouseEventHandler = (e) => { - if (mobile && isPreviewString && !onClick) { - setShowPreviewSheet(true); - } else { - onClick?.(e); - } - }; - - const handleSheetClose = () => { - setShowPreviewSheet(false); - }; + const handleClick: React.MouseEventHandler = React.useCallback( + (e) => { + if (onClick) { + onClick(e); + return; + } + + if (mobile && isPreviewString) { + showPreviewSheet(); + } + }, + [isPreviewString, mobile, onClick, showPreviewSheet], + ); return (
@@ -112,11 +115,11 @@ export function FilePreview({ role={clickable ? 'button' : undefined} onKeyDown={clickable ? onKeyDown : undefined} tabIndex={clickable ? 0 : undefined} - onClick={handleClick} + onClick={clickable ? handleClick : undefined} > {isPreviewString ? ( -
- {file.name} +
+ {file.name}
) : (
@@ -150,8 +153,8 @@ export function FilePreview({ ) : null} ); diff --git a/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx b/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx index 56f7cec1c..76c837bf3 100644 --- a/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx +++ b/src/components/FilePreview/MobileImagePreview/MobileImagePreview.tsx @@ -54,7 +54,7 @@ export function MobileImagePreview({
{actions?.map((action) => ( ))}
diff --git a/src/components/FilePreview/README.md b/src/components/FilePreview/README.md index e6779457a..bf7221a6f 100644 --- a/src/components/FilePreview/README.md +++ b/src/components/FilePreview/README.md @@ -14,23 +14,27 @@ A component for displaying the file. ```tsx action('onClick')} actions={[ { - icon: linkIcon, - title: 'open on drive', - onClick: onFileOpen, + icon: , + title: 'Link', + onClick: () => action('onLink'), }, { - icon: xmarkIcon, - title: 'delete a file', - onClick: onFileDelete, + icon: , + title: 'Close', + onClick: () => action('onClose'), }, ]} /> ``` + + + + @@ -77,24 +81,24 @@ LANDING_BLOCK--> ### Properties -| 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 | +| Name | Description | Type | Required | Default | +| :------------------ | :--------------------------------------------------------------------------------------------------------------- | :------------------------- | :------: | :------ | +| file | The File interface provides information about files and allows JavaScript in a web page to access their content. | `File` | yes | | +| imageSrc | source for image preview | `string` | | | +| description | Description displayed under the file name | `string` | | | +| className | Class name for the file container | `string` | | | +| onClick | Click handler for the file container | `function` | | | +| [actions](#actions) | Аn array of interactive actions | `FilePreviewActionProps[]` | | `[]` | #### 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 \| AnchorHTMLAttributes` | | | Additional action button props | +| Name | Description | Type | Required | Default | +| ---------- | ------------------------------ | ------------------------------------------------------------------------------------ | -------- | ------- | +| id | Action id | `String` | | | +| icon | Action icon | `String` | ✓ | | +| title | Action hint on hover | `String` | ✓ | | +| onClick | Action click handler | `function` | | | +| href | Action button href | `String` | | | +| extraProps | Additional action button props | `ButtonHTMLAttributes \| AnchorHTMLAttributes` | | | diff --git a/src/components/FilePreview/__stories__/Docs.mdx b/src/components/FilePreview/__stories__/Docs.mdx index 0affc4883..1f76cc01c 100644 --- a/src/components/FilePreview/__stories__/Docs.mdx +++ b/src/components/FilePreview/__stories__/Docs.mdx @@ -1,7 +1,27 @@ -import {Meta, Markdown} from '@storybook/addon-docs'; +import { + Meta, + Markdown, + Canvas, + AnchorMdx, + CodeOrSourceMdx, + HeadersMdx, +} from '@storybook/addon-docs'; import * as Stories from './FilePreview.stories'; import Readme from '../README.md?raw'; +export const FilePreviewExample = () => ; + -{Readme} + + {Readme} + diff --git a/src/components/FilePreview/__stories__/FilePreview.stories.tsx b/src/components/FilePreview/__stories__/FilePreview.stories.tsx index 780decf1f..bd2caf7c1 100644 --- a/src/components/FilePreview/__stories__/FilePreview.stories.tsx +++ b/src/components/FilePreview/__stories__/FilePreview.stories.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {CircleExclamation, Link, Xmark} from '@gravity-ui/icons'; +import {action} from '@storybook/addon-actions'; import type {Meta, StoryFn} from '@storybook/react'; import {Flex} from '../../layout'; @@ -26,6 +27,28 @@ export default { }, } as Meta; +const DefaultTemplate: StoryFn = (args) => { + return ; +}; + +export const Default = DefaultTemplate.bind({}); +Default.args = { + file: {name: 'my-file.docs', type: 'text/docs'} as File, + onClick: () => action('onClick'), + actions: [ + { + icon: , + onClick: () => action('onLink'), + title: 'Link', + }, + { + icon: , + onClick: () => action('onClose'), + title: 'Close', + }, + ], +}; + const CollageTemplate: StoryFn = () => { return (
= () => { alert(`You clicked on the file: ${fileType}`)} + onClick={() => action('onClick')} actions={[ { - icon: Link, + icon: , title: 'open on drive', - onClick: () => window.open('https://disk.yandex.com', '_blank'), + onClick: () => action('onLink'), }, { - icon: Xmark, + icon: , title: 'delete a file', - onClick: () => alert('Are you sure you want to delete the file?'), + onClick: () => action('onClose'), }, ]} /> @@ -62,13 +85,13 @@ const CollageTemplate: StoryFn = () => { imageSrc="https://storage.yandexcloud.net/uikit-storybook-assets/changelog-dialog-picture-2.png" actions={[ { - icon: CircleExclamation, + icon: , title: 'some hint', - onClick: () => alert('Some info'), + onClick: () => action('onHint'), }, { - icon: Xmark, - onClick: () => alert('Are you sure you want to delete the file?'), + icon: , + onClick: () => action('onClose'), title: 'Close', }, ]} @@ -79,25 +102,6 @@ const CollageTemplate: StoryFn = () => { export const Collage = CollageTemplate.bind({}); -const DefaultTemplate: StoryFn = (args) => { - return ; -}; - -export const Default = DefaultTemplate.bind({}); -Default.args = { - file: {name: 'my-file.docs', type: 'text/docs'} as File, - onClick: () => { - window.open('https://disk.yandex.com', '_blank'); - }, - actions: [ - { - icon: Xmark, - onClick: () => alert('Are you sure you want to delete the file?'), - title: 'Close', - }, - ], -}; - const NoClickableTemplate: StoryFn> = (args) => { return ( @@ -107,8 +111,8 @@ const NoClickableTemplate: StoryFn> = (args) = file={{name: 'No clickable with actions', type: 'text/docs'} as File} actions={[ { - icon: Xmark, - onClick: () => alert('Are you sure you want to delete the file?'), + icon: , + onClick: () => action('Are you sure you want to delete the file?'), title: 'Close', }, ]} @@ -116,13 +120,11 @@ const NoClickableTemplate: StoryFn> = (args) = { - window.open('https://disk.yandex.com', '_blank'); - }} + onClick={() => action('onClick')} actions={[ { - icon: Xmark, - onClick: () => alert('Are you sure you want to delete the file?'), + icon: , + onClick: () => action('Are you sure you want to delete the file?'), title: 'Close', }, ]} @@ -139,13 +141,11 @@ const WithoutActionTooltipTemplate: StoryFn> = { - window.open('https://disk.yandex.com', '_blank'); - }} + onClick={() => action('onClick')} actions={[ { - icon: Xmark, - onClick: () => alert('Are you sure you want to delete the file?'), + icon: , + onClick: () => action('onClose'), title: 'Close', tooltipExtraProps: { disabled: true, diff --git a/src/components/FilePreview/__tests__/FilePreview.test.tsx b/src/components/FilePreview/__tests__/FilePreview.test.tsx index 1a9b867e0..fba4915c5 100644 --- a/src/components/FilePreview/__tests__/FilePreview.test.tsx +++ b/src/components/FilePreview/__tests__/FilePreview.test.tsx @@ -44,8 +44,8 @@ describe('FilePreview', () => { file={{name: fileName, type: fileType} as File} onClick={clickHandler} actions={[ - {icon: CircleExclamation, title: 'some hint'}, - {icon: CircleExclamation, title: 'second hint'}, + {icon: , title: 'some hint'}, + {icon: , title: 'second hint'}, ]} />, ); @@ -72,8 +72,8 @@ describe('FilePreview', () => { file={{name: fileName, type: fileType} as File} imageSrc={imageSrc} actions={[ - {icon: CircleExclamation, title: firstActionText}, - {icon: CircleExclamation, title: secondActionText}, + {icon: , title: firstActionText}, + {icon: , title: secondActionText}, ]} />, ); @@ -97,12 +97,12 @@ describe('FilePreview', () => { file={{name: fileName, type: fileType} as File} actions={[ { - icon: CircleExclamation, + icon: , title: 'some hint', onClick: firstActionsClickHandler, }, { - icon: CircleExclamation, + icon: , title: 'second hint', onClick: secondActionsClickHandler, }, @@ -146,7 +146,7 @@ describe('FilePreview', () => { actions={[ { disabled, - icon: CircleExclamation, + icon: , title: 'some hint', onClick: actionsClickHandler, }, diff --git a/src/components/FilePreview/index.ts b/src/components/FilePreview/index.ts index 717e50604..23ce3a875 100644 --- a/src/components/FilePreview/index.ts +++ b/src/components/FilePreview/index.ts @@ -1,4 +1,3 @@ export {FilePreview} from './FilePreview'; export type {FilePreviewProps} from './FilePreview'; -export {getFileType} from './utils'; export type {FileType} from './types'; diff --git a/src/components/FilePreview/types.ts b/src/components/FilePreview/types.ts index b9acd0f4f..d92b862a4 100644 --- a/src/components/FilePreview/types.ts +++ b/src/components/FilePreview/types.ts @@ -1,15 +1,4 @@ -export type FileType = - | 'default' - | 'image' - | 'video' - | 'code' - | 'archive' - | 'music' - | 'text' - | 'pdf' - | 'table'; - -export const FILE_TYPES: FileType[] = [ +export const FILE_TYPES = [ 'default', 'image', 'video', @@ -19,4 +8,6 @@ export const FILE_TYPES: FileType[] = [ 'text', 'pdf', 'table', -]; +] as const; + +export type FileType = (typeof FILE_TYPES)[number];