@snack-uikit/attachment 0.0.1-preview-1913b436.0 → 0.0.1-preview-bfae2d03.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +8 -7
  2. package/dist/components/Attachment/Attachment.d.ts +1 -1
  3. package/dist/components/Attachment/Attachment.js +3 -3
  4. package/dist/components/Attachment/components/Emblem/Emblem.d.ts +2 -2
  5. package/dist/components/Attachment/components/Emblem/Emblem.js +6 -9
  6. package/dist/components/AttachmentSquare/AttachmentSquare.d.ts +1 -1
  7. package/dist/components/AttachmentSquare/AttachmentSquare.js +13 -3
  8. package/dist/components/AttachmentSquare/components/HoverContent/HoverContent.js +3 -3
  9. package/dist/components/AttachmentSquare/components/LoadingContent/LoadingContent.js +4 -1
  10. package/dist/components/AttachmentSquare/components/MainContent/MainContent.d.ts +2 -2
  11. package/dist/components/AttachmentSquare/components/MainContent/MainContent.js +6 -8
  12. package/dist/components/AttachmentSquare/components/styles.module.css +6 -0
  13. package/dist/components/AttachmentSquare/styles.module.css +15 -7
  14. package/dist/contexts.d.ts +7 -1
  15. package/dist/contexts.js +7 -0
  16. package/dist/helperComponents/Actions/Actions.d.ts +2 -1
  17. package/dist/helperComponents/Actions/Actions.js +27 -11
  18. package/dist/helperComponents/Actions/styles.module.css +8 -0
  19. package/dist/helperComponents/Text/Text.d.ts +3 -1
  20. package/dist/helperComponents/Text/Text.js +15 -2
  21. package/dist/helperComponents/TextBlock/TextBlock.js +3 -2
  22. package/dist/testIds.d.ts +11 -0
  23. package/dist/testIds.js +12 -0
  24. package/dist/types.d.ts +24 -6
  25. package/package.json +3 -3
  26. package/src/components/Attachment/Attachment.tsx +11 -8
  27. package/src/components/Attachment/components/Emblem/Emblem.tsx +15 -10
  28. package/src/components/AttachmentSquare/AttachmentSquare.tsx +34 -15
  29. package/src/components/AttachmentSquare/components/HoverContent/HoverContent.tsx +4 -3
  30. package/src/components/AttachmentSquare/components/LoadingContent/LoadingContent.tsx +14 -3
  31. package/src/components/AttachmentSquare/components/MainContent/MainContent.tsx +15 -11
  32. package/src/components/AttachmentSquare/components/styles.module.scss +8 -0
  33. package/src/components/AttachmentSquare/styles.module.scss +15 -5
  34. package/src/contexts.ts +15 -1
  35. package/src/helperComponents/Actions/Actions.tsx +54 -12
  36. package/src/helperComponents/Actions/styles.module.scss +14 -0
  37. package/src/helperComponents/Text/Text.tsx +5 -2
  38. package/src/helperComponents/TextBlock/TextBlock.tsx +10 -4
  39. package/src/testIds.ts +13 -0
  40. package/src/types.ts +27 -8
  41. package/dist/helperComponents/Text/styles.module.css +0 -16
  42. package/src/helperComponents/Text/styles.module.scss +0 -24
package/README.md CHANGED
@@ -1,12 +1,11 @@
1
1
  # Attachment
2
2
 
3
3
  ## Installation
4
+
4
5
  `npm i @snack-uikit/attachment`
5
6
 
6
7
  [Changelog](./CHANGELOG.md)
7
8
 
8
-
9
-
10
9
  [//]: DOCUMENTATION_SECTION_START
11
10
  [//]: THIS_SECTION_IS_AUTOGENERATED_PLEASE_DONT_EDIT_IT
12
11
  ## AttachmentSquare
@@ -16,15 +15,16 @@
16
15
  | file | `File` | - | Файл |
17
16
  | onDownload | `(file?: File) => void` | - | Колбек на клик по кнопке скачивания |
18
17
  | onDelete | `(file?: File) => void` | - | Колбек на клик по кнопке удаления |
18
+ | onRetry | `() => void` | - | Колбек на клик по кнопке повторения |
19
19
  | title | `string` | fileName | Заголовок |
20
20
  | description | `string` | fileExtension | Описание |
21
21
  | error | `string` | - | Сообщение об ошибке |
22
22
  | truncateVariant | "end" \| "middle" | - | Вариант обрезания строки: <br> - `End` - с конца; <br> - `Middle` - по середине |
23
23
  | loading | `boolean` | - | Управление состоянием загрузки |
24
- | extension2Icon | `Record<string, JSXElementConstructor<{ size?: number; className?: string; }>>` | - | Объект сопоставления расширения файла и отображаемой иконки @defaultIcon FileSVG |
24
+ | icon | `JSXElementConstructor<{ size?: number; className?: string; }>` | - | Иконка для файла @defaultIcon FileSVG |
25
+ | size | enum Size: `"s"`, `"m"` | s | Размер |
25
26
  | checked | `boolean` | - | Управление состоянием выбран/не выбран |
26
27
  | disabled | `boolean` | - | Управление состоянием интерактивности |
27
- | multipleSelection | `boolean` | - | Отображение галочки для режима массового выделения карточек |
28
28
  | onClick | `(e: MouseEvent<HTMLDivElement \| HTMLAnchorElement, MouseEvent>) => void` | - | Колбек на клик по карточке |
29
29
  | className | `string` | - | CSS-класс для элемента с контентом |
30
30
  ## Attachment
@@ -34,18 +34,19 @@
34
34
  | file | `File` | - | Файл |
35
35
  | onDownload | `(file?: File) => void` | - | Колбек на клик по кнопке скачивания |
36
36
  | onDelete | `(file?: File) => void` | - | Колбек на клик по кнопке удаления |
37
+ | onRetry | `() => void` | - | Колбек на клик по кнопке повторения |
37
38
  | title | `string` | fileName | Заголовок |
38
39
  | description | `string` | fileExtension | Описание |
39
40
  | error | `string` | - | Сообщение об ошибке |
40
41
  | truncateVariant | "end" \| "middle" | - | Вариант обрезания строки: <br> - `End` - с конца; <br> - `Middle` - по середине |
41
42
  | loading | `boolean` | - | Управление состоянием загрузки |
42
- | extension2Icon | `Record<string, JSXElementConstructor<{ size?: number; className?: string; }>>` | - | Объект сопоставления расширения файла и отображаемой иконки @defaultIcon FileSVG |
43
+ | icon | `JSXElementConstructor<{ size?: number; className?: string; }>` | - | Иконка для файла @defaultIcon FileSVG |
44
+ | size | enum Size: `"s"`, `"m"` | s | Размер |
43
45
  | checked | `boolean` | - | Управление состоянием выбран/не выбран |
44
46
  | disabled | `boolean` | - | Управление состоянием интерактивности |
45
- | multipleSelection | `boolean` | - | Отображение галочки для режима массового выделения карточек |
46
47
  | onClick | `(e: MouseEvent<HTMLDivElement \| HTMLAnchorElement, MouseEvent>) => void` | - | Колбек на клик по карточке |
47
48
  | className | `string` | - | CSS-класс для элемента с контентом |
48
- | size | enum Size: `"s"`, `"m"` | s | Размер |
49
+ | truncate | `{ title?: number; description?: number; error?: number; }` | - | |
49
50
 
50
51
 
51
52
  [//]: DOCUMENTATION_SECTION_END
@@ -1,2 +1,2 @@
1
1
  import { AttachmentProps } from '../../types';
2
- export declare function Attachment({ file, loading, extension2Icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, multipleSelection, onDownload, onDelete, onClick, size, ...rest }: AttachmentProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function Attachment({ file, loading, icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, onDownload, onDelete, onClick, onRetry, size, truncate, ...rest }: AttachmentProps): import("react/jsx-runtime").JSX.Element;
@@ -16,13 +16,13 @@ import { AttachmentContext } from '../../contexts';
16
16
  import { Actions, TextBlock } from '../../helperComponents';
17
17
  import { getBaseFileName, getFileExtension } from '../../helpers';
18
18
  import { useImage } from '../../hooks';
19
- import { Emblem } from './components/Emblem';
19
+ import { Emblem } from './components';
20
20
  import styles from './styles.module.css';
21
21
  export function Attachment(_a) {
22
- var { file, loading, extension2Icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, multipleSelection, onDownload, onDelete, onClick, size = 's' } = _a, rest = __rest(_a, ["file", "loading", "extension2Icon", "title", "description", "error", "disabled", "truncateVariant", "checked", "multipleSelection", "onDownload", "onDelete", "onClick", "size"]);
22
+ var { file, loading, icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, onDownload, onDelete, onClick, onRetry, size = 's', truncate } = _a, rest = __rest(_a, ["file", "loading", "icon", "title", "description", "error", "disabled", "truncateVariant", "checked", "onDownload", "onDelete", "onClick", "onRetry", "size", "truncate"]);
23
23
  const { loading: loadingImage, imageData } = useImage(file);
24
24
  const isLoading = loading || loadingImage;
25
25
  const title = titleProp || getBaseFileName(file === null || file === void 0 ? void 0 : file.name);
26
26
  const description = descriptionProp || getFileExtension(file === null || file === void 0 ? void 0 : file.name);
27
- return (_jsx(Card, Object.assign({}, extractSupportProps(rest), { outline: true, disabled: disabled, checked: checked, multipleSelection: multipleSelection, size: size, onClick: !isLoading && !disabled ? onClick : undefined, header: _jsx(AttachmentContext.Provider, { value: { file, truncateVariant, onDelete, onDownload, disabled, size }, children: _jsxs("div", { className: styles.composition, "data-size": size, children: [_jsx(Emblem, { title: title, loading: isLoading, extension2Icon: extension2Icon, imageData: imageData }), _jsx(TextBlock, { title: title, description: description, error: error }), _jsx(Actions, { hideDownload: isLoading })] }) }) })));
27
+ return (_jsx(Card, Object.assign({}, extractSupportProps(rest), { outline: true, disabled: disabled, checked: !loading && checked, multipleSelection: true, size: size, onClick: !isLoading && !disabled ? onClick : undefined, header: _jsx(AttachmentContext.Provider, { value: { file, truncateVariant, onDelete, onDownload, onRetry, disabled, size, truncate }, children: _jsxs("div", { className: styles.composition, "data-size": size, children: [_jsx(Emblem, { title: title, loading: isLoading, icon: icon, imageData: imageData }), _jsx(TextBlock, { title: title, description: description, error: error }), _jsx(Actions, { hideDownload: isLoading || Boolean(error), hideRetry: isLoading || !Boolean(error) })] }) }) })));
28
28
  }
@@ -1,5 +1,5 @@
1
1
  import { AttachmentProps } from '../../../../types';
2
2
  export type EmblemProps = {
3
3
  imageData?: string;
4
- } & Pick<AttachmentProps, 'extension2Icon' | 'loading' | 'title'>;
5
- export declare function Emblem({ loading, extension2Icon, imageData, title }: EmblemProps): import("react/jsx-runtime").JSX.Element;
4
+ } & Pick<AttachmentProps, 'icon' | 'loading' | 'title'>;
5
+ export declare function Emblem({ loading, icon, imageData, title }: EmblemProps): import("react/jsx-runtime").JSX.Element;
@@ -1,20 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
2
  import { IconPredefined } from '@snack-uikit/icon-predefined';
4
3
  import { FileSVG } from '@snack-uikit/icons';
5
4
  import { Spinner } from '@snack-uikit/loaders';
6
5
  import { useAttachmentContext } from '../../../../contexts';
7
- import { getFileExtension } from '../../../../helpers';
6
+ import { TEST_IDS } from '../../../../testIds';
8
7
  import styles from './styled.module.css';
9
- export function Emblem({ loading, extension2Icon, imageData, title }) {
10
- var _a;
11
- const { file, size } = useAttachmentContext();
12
- const extension = useMemo(() => { var _a; return (_a = getFileExtension(file === null || file === void 0 ? void 0 : file.name)) !== null && _a !== void 0 ? _a : ''; }, [file === null || file === void 0 ? void 0 : file.name]);
8
+ export function Emblem({ loading, icon, imageData, title }) {
9
+ const { size } = useAttachmentContext();
13
10
  if (loading) {
14
- return (_jsx(IconPredefined, { size: size, icon: () => _jsx(Spinner, { size: 's' }), decor: false, appearance: 'neutral', shape: 'square' }));
11
+ return (_jsx(IconPredefined, { size: size, icon: () => _jsx(Spinner, { size: 's' }), decor: false, appearance: 'neutral', shape: 'square', "data-test-id": TEST_IDS.loading }));
15
12
  }
16
13
  if (imageData) {
17
- return (_jsx(IconPredefined, { size: size, icon: () => _jsx("img", { src: imageData, alt: title, "data-size": size || undefined, className: styles.img }), decor: false, shape: 'square' }));
14
+ return (_jsx(IconPredefined, { size: size, icon: () => _jsx("img", { src: imageData, alt: title, "data-size": size || undefined, className: styles.img }), decor: false, shape: 'square', "data-test-id": TEST_IDS.image }));
18
15
  }
19
- return (_jsx(IconPredefined, { size: size, icon: (_a = extension2Icon === null || extension2Icon === void 0 ? void 0 : extension2Icon[extension]) !== null && _a !== void 0 ? _a : FileSVG, decor: true, appearance: 'neutral', shape: 'square' }));
16
+ return (_jsx(IconPredefined, { size: size, icon: icon !== null && icon !== void 0 ? icon : FileSVG, decor: true, appearance: 'neutral', shape: 'square', "data-test-id": TEST_IDS.icon }));
20
17
  }
@@ -1,2 +1,2 @@
1
1
  import { AttachmentSquareProps } from '../../types';
2
- export declare function AttachmentSquare({ file, loading, extension2Icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, multipleSelection, onDownload, onDelete, onClick, ...rest }: AttachmentSquareProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function AttachmentSquare({ file, loading, icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, onDownload, onDelete, onClick, onRetry, size, ...rest }: AttachmentSquareProps): import("react/jsx-runtime").JSX.Element;
@@ -10,18 +10,28 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import cn from 'classnames';
14
+ import { useState } from 'react';
13
15
  import { Card } from '@snack-uikit/card';
14
16
  import { extractSupportProps } from '@snack-uikit/utils';
15
- import { AttachmentContext } from '../../contexts';
17
+ import { AttachmentContext, AttachmentFocusActionsContext } from '../../contexts';
16
18
  import { getBaseFileName, getFileExtension } from '../../helpers';
17
19
  import { useImage } from '../../hooks';
18
20
  import { HoverContent, LoadingContent, MainContent } from './components';
19
21
  import styles from './styles.module.css';
20
22
  export function AttachmentSquare(_a) {
21
- var { file, loading, extension2Icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, multipleSelection, onDownload, onDelete, onClick } = _a, rest = __rest(_a, ["file", "loading", "extension2Icon", "title", "description", "error", "disabled", "truncateVariant", "checked", "multipleSelection", "onDownload", "onDelete", "onClick"]);
23
+ var { file, loading, icon, title: titleProp, description: descriptionProp, error, disabled, truncateVariant, checked, onDownload, onDelete, onClick, onRetry, size = 's' } = _a, rest = __rest(_a, ["file", "loading", "icon", "title", "description", "error", "disabled", "truncateVariant", "checked", "onDownload", "onDelete", "onClick", "onRetry", "size"]);
22
24
  const { loading: loadingImage, imageData } = useImage(file);
23
25
  const isLoading = loading || loadingImage;
24
26
  const title = titleProp || getBaseFileName(file === null || file === void 0 ? void 0 : file.name);
25
27
  const description = descriptionProp || getFileExtension(file === null || file === void 0 ? void 0 : file.name);
26
- return (_jsx(Card, Object.assign({}, extractSupportProps(rest), { outline: true, disabled: disabled, size: 's', className: styles.wrapper, checked: checked, multipleSelection: multipleSelection, onClick: !isLoading && !disabled ? onClick : undefined, header: _jsxs(AttachmentContext.Provider, { value: { file, truncateVariant, onDelete, onDownload, disabled, size: 's' }, children: [!isLoading && !disabled && (_jsx(MainContent, { title: title, extension2Icon: extension2Icon, imageData: imageData, className: styles.main })), !isLoading && (_jsx(HoverContent, { title: title, description: description, error: error, className: styles.hover })), isLoading && _jsx(LoadingContent, {})] }) })));
28
+ const [hasFocusedActions, setHasFocusedActions] = useState(false);
29
+ return (_jsx(Card, Object.assign({}, extractSupportProps(rest), { outline: true, disabled: disabled, size: size, className: cn(styles.wrapper, {
30
+ [styles.sizeS]: size === 's',
31
+ [styles.sizeM]: size === 'm',
32
+ [styles.hasFocusedActions]: hasFocusedActions,
33
+ }), checked: !loading && checked, multipleSelection: true, onClick: !isLoading && !disabled ? onClick : undefined, header: _jsx(AttachmentContext.Provider, { value: { file, truncateVariant, onDelete, onDownload, onRetry, disabled, size }, children: _jsxs(AttachmentFocusActionsContext.Provider, { value: {
34
+ focused: hasFocusedActions,
35
+ setFocused: setHasFocusedActions,
36
+ }, children: [!isLoading && !disabled && !error && (_jsx(MainContent, { title: title, icon: icon, imageData: imageData, className: styles.main })), !isLoading && (_jsx(HoverContent, { title: title, description: description, error: error, className: styles.hover })), isLoading && _jsx(LoadingContent, {})] }) }) })));
27
37
  }
@@ -4,8 +4,8 @@ import { useAttachmentContext } from '../../../../contexts';
4
4
  import { Actions, TextBlock } from '../../../../helperComponents';
5
5
  import styles from '../styles.module.css';
6
6
  export function HoverContent({ title, description, error, className = '' }) {
7
- const { disabled } = useAttachmentContext();
7
+ const { disabled, size } = useAttachmentContext();
8
8
  return (_jsxs("div", { className: cn(styles.composition, {
9
- [className]: !disabled,
10
- }), children: [_jsx(TextBlock, { title: title, error: error, description: !error ? description : undefined, align: 'center' }), _jsx(Actions, {})] }));
9
+ [className]: !disabled && !error,
10
+ }), "data-size": size, children: [_jsx(TextBlock, { title: title, error: error, description: !error ? description : undefined, align: 'center' }), _jsx(Actions, { hideDownload: Boolean(error), hideRetry: !Boolean(error) })] }));
11
11
  }
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { IconPredefined } from '@snack-uikit/icon-predefined';
3
3
  import { Spinner } from '@snack-uikit/loaders';
4
+ import { useAttachmentContext } from '../../../../contexts';
4
5
  import { Actions } from '../../../../helperComponents';
6
+ import { TEST_IDS } from '../../../../testIds';
5
7
  import styles from '../styles.module.css';
6
8
  export function LoadingContent() {
7
- return (_jsxs("div", { className: styles.composition, children: [_jsx(IconPredefined, { size: 's', icon: () => _jsx(Spinner, { size: 's' }), decor: false, appearance: 'neutral', shape: 'square' }), _jsx(Actions, { hideDownload: true })] }));
9
+ const { size } = useAttachmentContext();
10
+ return (_jsxs("div", { className: styles.composition, "data-size": size, children: [_jsx(IconPredefined, { size: size, icon: () => _jsx(Spinner, { size: 's' }), decor: false, appearance: 'neutral', shape: 'square', "data-test-id": TEST_IDS.loading }), _jsx(Actions, { hideDownload: true, hideRetry: true })] }));
8
11
  }
@@ -1,6 +1,6 @@
1
1
  import { AttachmentProps } from '../../../../types';
2
2
  type MainContentProps = {
3
3
  imageData?: string;
4
- } & Pick<AttachmentProps, 'title' | 'extension2Icon' | 'className'>;
5
- export declare function MainContent({ title, extension2Icon, imageData, className }: MainContentProps): import("react/jsx-runtime").JSX.Element;
4
+ } & Pick<AttachmentProps, 'title' | 'icon' | 'className'>;
5
+ export declare function MainContent({ title, icon, imageData, className }: MainContentProps): import("react/jsx-runtime").JSX.Element;
6
6
  export {};
@@ -1,19 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import cn from 'classnames';
3
- import { useMemo } from 'react';
4
3
  import { Card } from '@snack-uikit/card';
5
4
  import { IconPredefined } from '@snack-uikit/icon-predefined';
6
5
  import { FileSVG } from '@snack-uikit/icons';
7
6
  import { useAttachmentContext } from '../../../../contexts';
8
7
  import { TextBlock } from '../../../../helperComponents';
9
- import { getFileExtension } from '../../../../helpers';
8
+ import { TEST_IDS } from '../../../../testIds';
10
9
  import styles from '../styles.module.css';
11
- export function MainContent({ title, extension2Icon, imageData, className }) {
12
- var _a, _b;
13
- const { file } = useAttachmentContext();
14
- const extension = useMemo(() => { var _a; return (_a = getFileExtension(file === null || file === void 0 ? void 0 : file.name)) !== null && _a !== void 0 ? _a : ''; }, [file === null || file === void 0 ? void 0 : file.name]);
10
+ export function MainContent({ title, icon, imageData, className }) {
11
+ var _a;
12
+ const { file, size } = useAttachmentContext();
15
13
  if (imageData) {
16
- return (_jsx("div", { className: cn(styles.composition, className), children: _jsx(Card.Image, { src: imageData, alt: (_a = file === null || file === void 0 ? void 0 : file.name) !== null && _a !== void 0 ? _a : 'file', mode: 'background', hideFading: true }) }));
14
+ return (_jsx("div", { className: cn(styles.composition, className), children: _jsx(Card.Image, { src: imageData, alt: (_a = file === null || file === void 0 ? void 0 : file.name) !== null && _a !== void 0 ? _a : 'file', mode: 'background', hideFading: true, "data-test-id": TEST_IDS.image }) }));
17
15
  }
18
- return (_jsxs("div", { className: cn(styles.composition, className), children: [_jsx(TextBlock, { title: title, align: 'center' }), _jsx(IconPredefined, { size: 's', icon: (_b = extension2Icon === null || extension2Icon === void 0 ? void 0 : extension2Icon[extension]) !== null && _b !== void 0 ? _b : FileSVG, decor: true, appearance: 'neutral', shape: 'square' })] }));
16
+ return (_jsxs("div", { className: cn(styles.composition, className), "data-size": size, children: [_jsx(TextBlock, { title: title, align: 'center' }), _jsx(IconPredefined, { size: size, icon: icon !== null && icon !== void 0 ? icon : FileSVG, decor: true, appearance: 'neutral', shape: 'square', "data-test-id": TEST_IDS.icon })] }));
19
17
  }
@@ -9,4 +9,10 @@
9
9
  margin:calc(0px - var(--dimension-050m, 4px));
10
10
  padding:var(--dimension-050m, 4px);
11
11
  text-align:center;
12
+ }
13
+ .composition[data-size=s]{
14
+ gap:var(--space-attachment-composition-s-gap, 8px);
15
+ }
16
+ .composition[data-size=m]{
17
+ gap:var(--space-attachment-composition-m-gap, 12px);
12
18
  }
@@ -1,16 +1,24 @@
1
- .wrapper{
2
- width:var(--size-attachment-container-square, 80px);
3
- height:var(--size-attachment-container-square, 80px);
1
+ .wrapper.sizeS{
2
+ width:var(--size-attachment-container-square-s, 80px);
3
+ height:var(--size-attachment-container-square-s, 80px);
4
+ }
5
+ .wrapper.sizeM{
6
+ width:var(--size-attachment-container-square-m, 108px);
7
+ height:var(--size-attachment-container-square-m, 108px);
4
8
  }
5
9
  .wrapper .main{
6
10
  display:flex;
7
11
  }
8
12
  .wrapper .hover{
9
- display:none;
13
+ pointer-events:none;
14
+ height:0;
15
+ opacity:0;
10
16
  }
11
- .wrapper:hover .hover, .wrapper:focus-within .hover, .wrapper:focus-visible .hover{
12
- display:flex;
17
+ .wrapper:hover .hover, .wrapper:focus-visible .hover, .wrapper.hasFocusedActions .hover{
18
+ pointer-events:unset;
19
+ height:unset;
20
+ opacity:1;
13
21
  }
14
- .wrapper:hover .main, .wrapper:focus-within .main, .wrapper:focus-visible .main{
22
+ .wrapper:hover .main, .wrapper:focus-visible .main, .wrapper.hasFocusedActions .main{
15
23
  display:none;
16
24
  }
@@ -1,5 +1,11 @@
1
1
  /// <reference types="react" />
2
2
  import { AttachmentProps } from './types';
3
- export type AttachmentContextType = Pick<AttachmentProps, 'size' | 'file' | 'truncateVariant' | 'onDownload' | 'onDelete' | 'disabled'>;
3
+ export type AttachmentContextType = Pick<AttachmentProps, 'size' | 'file' | 'truncateVariant' | 'onDownload' | 'onDelete' | 'onRetry' | 'disabled' | 'truncate'>;
4
4
  export declare const AttachmentContext: import("react").Context<AttachmentContextType>;
5
5
  export declare function useAttachmentContext(): AttachmentContextType;
6
+ export type AttachmentFocusActionsContextType = {
7
+ focused: boolean;
8
+ setFocused(focused: boolean): void;
9
+ };
10
+ export declare const AttachmentFocusActionsContext: import("react").Context<AttachmentFocusActionsContextType>;
11
+ export declare function useAttachmentFocusActionsContext(): AttachmentFocusActionsContextType;
package/dist/contexts.js CHANGED
@@ -3,3 +3,10 @@ export const AttachmentContext = createContext({});
3
3
  export function useAttachmentContext() {
4
4
  return useContext(AttachmentContext);
5
5
  }
6
+ export const AttachmentFocusActionsContext = createContext({
7
+ focused: false,
8
+ setFocused: () => { },
9
+ });
10
+ export function useAttachmentFocusActionsContext() {
11
+ return useContext(AttachmentFocusActionsContext);
12
+ }
@@ -1,6 +1,7 @@
1
1
  type ActionsProps = {
2
2
  hideDownload?: boolean;
3
3
  hideDelete?: boolean;
4
+ hideRetry?: boolean;
4
5
  };
5
- export declare function Actions({ hideDelete, hideDownload }: ActionsProps): import("react/jsx-runtime").JSX.Element | null;
6
+ export declare function Actions({ hideDelete, hideDownload, hideRetry }: ActionsProps): import("react/jsx-runtime").JSX.Element | null;
6
7
  export {};
@@ -1,20 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ButtonFunction } from '@snack-uikit/button';
3
- import { DownloadSVG, TrashSVG } from '@snack-uikit/icons';
4
- import { useAttachmentContext } from '../../contexts';
3
+ import { DownloadSVG, TrashSVG, UpdateSVG } from '@snack-uikit/icons';
4
+ import { useAttachmentContext, useAttachmentFocusActionsContext } from '../../contexts';
5
+ import { TEST_IDS } from '../../testIds';
5
6
  import styles from './styles.module.css';
6
- export function Actions({ hideDelete, hideDownload }) {
7
- const { onDelete, onDownload, file, disabled } = useAttachmentContext();
7
+ const noop = () => { };
8
+ export function Actions({ hideDelete, hideDownload, hideRetry }) {
9
+ const { onDelete, onDownload, file, disabled, onRetry } = useAttachmentContext();
10
+ const { setFocused = noop } = useAttachmentFocusActionsContext();
8
11
  const showDownload = onDownload && !hideDownload;
9
12
  const showDelete = onDelete && !hideDelete;
13
+ const showRetry = onRetry && !hideRetry;
14
+ const handleFocus = () => {
15
+ setFocused(true);
16
+ };
17
+ const handleBlur = () => {
18
+ setFocused(false);
19
+ };
20
+ const handleRetry = (e) => {
21
+ onRetry === null || onRetry === void 0 ? void 0 : onRetry();
22
+ e.stopPropagation();
23
+ };
24
+ const handleDownload = (e) => {
25
+ onDownload === null || onDownload === void 0 ? void 0 : onDownload(file);
26
+ e.stopPropagation();
27
+ };
28
+ const handleDelete = (e) => {
29
+ onDelete === null || onDelete === void 0 ? void 0 : onDelete(file);
30
+ e.stopPropagation();
31
+ };
10
32
  if (!showDownload && !showDelete) {
11
33
  return null;
12
34
  }
13
- return (_jsxs("div", { className: styles.actions, children: [showDownload && (_jsx(ButtonFunction, { disabled: disabled, size: 'xs', icon: _jsx(DownloadSVG, {}), onClick: e => {
14
- onDownload(file);
15
- e.stopPropagation();
16
- } })), showDelete && (_jsx(ButtonFunction, { disabled: disabled, size: 'xs', icon: _jsx(TrashSVG, {}), onClick: e => {
17
- onDelete(file);
18
- e.stopPropagation();
19
- } }))] }));
35
+ return (_jsxs("div", { className: styles.actions, children: [showDownload && (_jsx(ButtonFunction, { disabled: disabled, size: 'xs', icon: _jsx(DownloadSVG, {}), onClick: handleDownload, onFocus: handleFocus, onBlur: handleBlur, "data-test-id": TEST_IDS.downloadAction })), showRetry && (_jsx(ButtonFunction, { disabled: disabled, size: 'xs', icon: _jsx(UpdateSVG, {}), onClick: handleRetry, onFocus: handleFocus, onBlur: handleBlur, className: styles.retryAction, "data-test-id": TEST_IDS.retryAction })), showDelete && (_jsx(ButtonFunction, { disabled: disabled, size: 'xs', icon: _jsx(TrashSVG, {}), onClick: handleDelete, onFocus: handleFocus, onBlur: handleBlur, "data-test-id": TEST_IDS.deleteAction }))] }));
20
36
  }
@@ -2,4 +2,12 @@
2
2
  gap:var(--space-attachment-composition-end-item-gap, 4px);
3
3
  display:flex;
4
4
  flex-shrink:0;
5
+ }
6
+ .actions :not([data-disabled]).retryAction svg{
7
+ transform:rotate(360deg);
8
+ transition:transform 0.5s;
9
+ }
10
+ .actions :not([data-disabled]).retryAction:active svg{
11
+ transform:rotate(0deg);
12
+ transition-duration:0s;
5
13
  }
@@ -1,5 +1,7 @@
1
1
  export type TextProps = {
2
2
  text?: string;
3
3
  className: string;
4
+ maxLines?: number;
5
+ 'data-test-id'?: string;
4
6
  };
5
- export declare function Text({ text, className }: TextProps): import("react/jsx-runtime").JSX.Element | null;
7
+ export declare function Text({ text, className, maxLines, ...rest }: TextProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,10 +1,23 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { jsx as _jsx } from "react/jsx-runtime";
2
13
  import { TruncateString } from '@snack-uikit/truncate-string';
14
+ import { extractSupportProps } from '@snack-uikit/utils';
3
15
  import { useAttachmentContext } from '../../contexts';
4
- export function Text({ text, className }) {
16
+ export function Text(_a) {
17
+ var { text, className, maxLines = 1 } = _a, rest = __rest(_a, ["text", "className", "maxLines"]);
5
18
  const { truncateVariant } = useAttachmentContext();
6
19
  if (!text) {
7
20
  return null;
8
21
  }
9
- return (_jsx("div", { className: className, children: _jsx(TruncateString, { text: text, maxLines: 1, variant: truncateVariant }) }));
22
+ return (_jsx("div", { className: className, children: _jsx(TruncateString, Object.assign({ text: text, maxLines: maxLines, variant: truncateVariant }, extractSupportProps(rest))) }));
10
23
  }
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import cn from 'classnames';
3
3
  import { useAttachmentContext } from '../../contexts';
4
+ import { TEST_IDS } from '../../testIds';
4
5
  import { Text } from '../Text';
5
6
  import styles from './styles.module.css';
6
7
  export function TextBlock({ title, description, error, className, align = 'left' }) {
7
- const { size } = useAttachmentContext();
8
- return (_jsxs("div", { className: cn(styles.content, className), "data-size": size, "data-align": align, children: [_jsx(Text, { className: styles.title, text: title }), _jsx(Text, { className: styles.description, text: description }), _jsx(Text, { className: styles.error, text: error })] }));
8
+ const { size, truncate = {} } = useAttachmentContext();
9
+ return (_jsxs("div", { className: cn(styles.content, className), "data-size": size, "data-align": align, children: [_jsx(Text, { className: styles.title, text: title, "data-test-id": TEST_IDS.title, maxLines: truncate === null || truncate === void 0 ? void 0 : truncate.title }), _jsx(Text, { className: styles.description, text: description, "data-test-id": TEST_IDS.description, maxLines: truncate === null || truncate === void 0 ? void 0 : truncate.description }), _jsx(Text, { className: styles.error, text: error, "data-test-id": TEST_IDS.error, maxLines: truncate === null || truncate === void 0 ? void 0 : truncate.error })] }));
9
10
  }
@@ -0,0 +1,11 @@
1
+ export declare const TEST_IDS: {
2
+ icon: string;
3
+ image: string;
4
+ loading: string;
5
+ retryAction: string;
6
+ deleteAction: string;
7
+ downloadAction: string;
8
+ title: string;
9
+ description: string;
10
+ error: string;
11
+ };
@@ -0,0 +1,12 @@
1
+ const PREFIX = 'attachment';
2
+ export const TEST_IDS = {
3
+ icon: `${PREFIX}__icon`,
4
+ image: `${PREFIX}__image`,
5
+ loading: `${PREFIX}__loading`,
6
+ retryAction: `${PREFIX}__retry-action`,
7
+ deleteAction: `${PREFIX}__delete-action`,
8
+ downloadAction: `${PREFIX}__download-action`,
9
+ title: `${PREFIX}__title`,
10
+ description: `${PREFIX}__description`,
11
+ error: `${PREFIX}__error`,
12
+ };
package/dist/types.d.ts CHANGED
@@ -3,13 +3,15 @@ import { IconPredefinedProps } from '@snack-uikit/icon-predefined';
3
3
  import { TruncateStringProps } from '@snack-uikit/truncate-string';
4
4
  import { WithSupportProps } from '@snack-uikit/utils';
5
5
  export type Size = 's' | 'm';
6
- export type AttachmentCommonProps = WithSupportProps<{
6
+ export type AttachmentSquareProps = WithSupportProps<{
7
7
  /** Файл */
8
8
  file?: File;
9
9
  /** Колбек на клик по кнопке скачивания */
10
10
  onDownload?(file?: File | undefined): void;
11
11
  /** Колбек на клик по кнопке удаления */
12
12
  onDelete?(file?: File | undefined): void;
13
+ /** Колбек на клик по кнопке повторения */
14
+ onRetry?(): void;
13
15
  /**
14
16
  * Заголовок
15
17
  * @default fileName
@@ -31,13 +33,29 @@ export type AttachmentCommonProps = WithSupportProps<{
31
33
  /** Управление состоянием загрузки */
32
34
  loading?: boolean;
33
35
  /**
34
- * Объект сопоставления расширения файла и отображаемой иконки
36
+ * Иконка для файла
35
37
  * @defaultIcon FileSVG
36
38
  */
37
- extension2Icon?: Record<string, IconPredefinedProps['icon']>;
38
- }> & Pick<CardProps, 'checked' | 'disabled' | 'multipleSelection' | 'onClick' | 'className'>;
39
- export type AttachmentSquareProps = AttachmentCommonProps;
40
- export type AttachmentProps = AttachmentCommonProps & {
39
+ icon?: IconPredefinedProps['icon'];
41
40
  /** Размер */
42
41
  size?: Size;
42
+ }> & Pick<CardProps, 'checked' | 'disabled' | 'onClick' | 'className'>;
43
+ export type AttachmentProps = AttachmentSquareProps & {
44
+ truncate?: {
45
+ /**
46
+ * Максимальное кол-во строк заголовка
47
+ * @default 1
48
+ */
49
+ title?: number;
50
+ /**
51
+ * Максимальное кол-во строк описания
52
+ * @default 1
53
+ */
54
+ description?: number;
55
+ /**
56
+ * Максимальное кол-во строк ошибки
57
+ * @default 1
58
+ */
59
+ error?: number;
60
+ };
43
61
  };
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Attachment",
7
- "version": "0.0.1-preview-1913b436.0",
7
+ "version": "0.0.1-preview-bfae2d03.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -33,7 +33,7 @@
33
33
  "scripts": {},
34
34
  "dependencies": {
35
35
  "@snack-uikit/button": "0.17.1",
36
- "@snack-uikit/card": "0.12.13",
36
+ "@snack-uikit/card": "0.12.14-preview-bfae2d03.0",
37
37
  "@snack-uikit/icon-predefined": "0.5.1",
38
38
  "@snack-uikit/icons": "0.21.0",
39
39
  "@snack-uikit/loaders": "0.5.2",
@@ -41,5 +41,5 @@
41
41
  "@snack-uikit/utils": "3.3.0",
42
42
  "classnames": "2.3.2"
43
43
  },
44
- "gitHead": "4052d521d291e1987c85162369886d8e49b3ef49"
44
+ "gitHead": "7b2a733a770a8bdd14adb9951c8fc552ad192578"
45
45
  }
@@ -6,24 +6,25 @@ import { Actions, TextBlock } from '../../helperComponents';
6
6
  import { getBaseFileName, getFileExtension } from '../../helpers';
7
7
  import { useImage } from '../../hooks';
8
8
  import { AttachmentProps } from '../../types';
9
- import { Emblem } from './components/Emblem';
9
+ import { Emblem } from './components';
10
10
  import styles from './styles.module.scss';
11
11
 
12
12
  export function Attachment({
13
13
  file,
14
14
  loading,
15
- extension2Icon,
15
+ icon,
16
16
  title: titleProp,
17
17
  description: descriptionProp,
18
18
  error,
19
19
  disabled,
20
20
  truncateVariant,
21
21
  checked,
22
- multipleSelection,
23
22
  onDownload,
24
23
  onDelete,
25
24
  onClick,
25
+ onRetry,
26
26
  size = 's',
27
+ truncate,
27
28
  ...rest
28
29
  }: AttachmentProps) {
29
30
  const { loading: loadingImage, imageData } = useImage(file);
@@ -37,16 +38,18 @@ export function Attachment({
37
38
  {...extractSupportProps(rest)}
38
39
  outline
39
40
  disabled={disabled}
40
- checked={checked}
41
- multipleSelection={multipleSelection}
41
+ checked={!loading && checked}
42
+ multipleSelection
42
43
  size={size}
43
44
  onClick={!isLoading && !disabled ? onClick : undefined}
44
45
  header={
45
- <AttachmentContext.Provider value={{ file, truncateVariant, onDelete, onDownload, disabled, size }}>
46
+ <AttachmentContext.Provider
47
+ value={{ file, truncateVariant, onDelete, onDownload, onRetry, disabled, size, truncate }}
48
+ >
46
49
  <div className={styles.composition} data-size={size}>
47
- <Emblem title={title} loading={isLoading} extension2Icon={extension2Icon} imageData={imageData} />
50
+ <Emblem title={title} loading={isLoading} icon={icon} imageData={imageData} />
48
51
  <TextBlock title={title} description={description} error={error} />
49
- <Actions hideDownload={isLoading} />
52
+ <Actions hideDownload={isLoading || Boolean(error)} hideRetry={isLoading || !Boolean(error)} />
50
53
  </div>
51
54
  </AttachmentContext.Provider>
52
55
  }
@@ -1,26 +1,29 @@
1
- import { useMemo } from 'react';
2
-
3
1
  import { IconPredefined } from '@snack-uikit/icon-predefined';
4
2
  import { FileSVG } from '@snack-uikit/icons';
5
3
  import { Spinner } from '@snack-uikit/loaders';
6
4
 
7
5
  import { useAttachmentContext } from '../../../../contexts';
8
- import { getFileExtension } from '../../../../helpers';
6
+ import { TEST_IDS } from '../../../../testIds';
9
7
  import { AttachmentProps } from '../../../../types';
10
8
  import styles from './styled.module.scss';
11
9
 
12
10
  export type EmblemProps = {
13
11
  imageData?: string;
14
- } & Pick<AttachmentProps, 'extension2Icon' | 'loading' | 'title'>;
15
-
16
- export function Emblem({ loading, extension2Icon, imageData, title }: EmblemProps) {
17
- const { file, size } = useAttachmentContext();
12
+ } & Pick<AttachmentProps, 'icon' | 'loading' | 'title'>;
18
13
 
19
- const extension = useMemo(() => getFileExtension(file?.name) ?? '', [file?.name]);
14
+ export function Emblem({ loading, icon, imageData, title }: EmblemProps) {
15
+ const { size } = useAttachmentContext();
20
16
 
21
17
  if (loading) {
22
18
  return (
23
- <IconPredefined size={size} icon={() => <Spinner size='s' />} decor={false} appearance='neutral' shape='square' />
19
+ <IconPredefined
20
+ size={size}
21
+ icon={() => <Spinner size='s' />}
22
+ decor={false}
23
+ appearance='neutral'
24
+ shape='square'
25
+ data-test-id={TEST_IDS.loading}
26
+ />
24
27
  );
25
28
  }
26
29
 
@@ -31,6 +34,7 @@ export function Emblem({ loading, extension2Icon, imageData, title }: EmblemProp
31
34
  icon={() => <img src={imageData} alt={title} data-size={size || undefined} className={styles.img} />}
32
35
  decor={false}
33
36
  shape='square'
37
+ data-test-id={TEST_IDS.image}
34
38
  />
35
39
  );
36
40
  }
@@ -38,10 +42,11 @@ export function Emblem({ loading, extension2Icon, imageData, title }: EmblemProp
38
42
  return (
39
43
  <IconPredefined
40
44
  size={size}
41
- icon={extension2Icon?.[extension] ?? FileSVG}
45
+ icon={icon ?? FileSVG}
42
46
  decor
43
47
  appearance='neutral'
44
48
  shape='square'
49
+ data-test-id={TEST_IDS.icon}
45
50
  />
46
51
  );
47
52
  }
@@ -1,7 +1,10 @@
1
+ import cn from 'classnames';
2
+ import { useState } from 'react';
3
+
1
4
  import { Card } from '@snack-uikit/card';
2
5
  import { extractSupportProps } from '@snack-uikit/utils';
3
6
 
4
- import { AttachmentContext } from '../../contexts';
7
+ import { AttachmentContext, AttachmentFocusActionsContext } from '../../contexts';
5
8
  import { getBaseFileName, getFileExtension } from '../../helpers';
6
9
  import { useImage } from '../../hooks';
7
10
  import { AttachmentSquareProps } from '../../types';
@@ -11,17 +14,18 @@ import styles from './styles.module.scss';
11
14
  export function AttachmentSquare({
12
15
  file,
13
16
  loading,
14
- extension2Icon,
17
+ icon,
15
18
  title: titleProp,
16
19
  description: descriptionProp,
17
20
  error,
18
21
  disabled,
19
22
  truncateVariant,
20
23
  checked,
21
- multipleSelection,
22
24
  onDownload,
23
25
  onDelete,
24
26
  onClick,
27
+ onRetry,
28
+ size = 's',
25
29
  ...rest
26
30
  }: AttachmentSquareProps) {
27
31
  const { loading: loadingImage, imageData } = useImage(file);
@@ -30,25 +34,40 @@ export function AttachmentSquare({
30
34
  const title = titleProp || getBaseFileName(file?.name);
31
35
  const description = descriptionProp || getFileExtension(file?.name);
32
36
 
37
+ const [hasFocusedActions, setHasFocusedActions] = useState<boolean>(false);
38
+
33
39
  return (
34
40
  <Card
35
41
  {...extractSupportProps(rest)}
36
42
  outline
37
43
  disabled={disabled}
38
- size='s'
39
- className={styles.wrapper}
40
- checked={checked}
41
- multipleSelection={multipleSelection}
44
+ size={size}
45
+ className={cn(styles.wrapper, {
46
+ [styles.sizeS]: size === 's',
47
+ [styles.sizeM]: size === 'm',
48
+ [styles.hasFocusedActions]: hasFocusedActions,
49
+ })}
50
+ checked={!loading && checked}
51
+ multipleSelection
42
52
  onClick={!isLoading && !disabled ? onClick : undefined}
43
53
  header={
44
- <AttachmentContext.Provider value={{ file, truncateVariant, onDelete, onDownload, disabled, size: 's' }}>
45
- {!isLoading && !disabled && (
46
- <MainContent title={title} extension2Icon={extension2Icon} imageData={imageData} className={styles.main} />
47
- )}
48
- {!isLoading && (
49
- <HoverContent title={title} description={description} error={error} className={styles.hover} />
50
- )}
51
- {isLoading && <LoadingContent />}
54
+ <AttachmentContext.Provider value={{ file, truncateVariant, onDelete, onDownload, onRetry, disabled, size }}>
55
+ <AttachmentFocusActionsContext.Provider
56
+ value={{
57
+ focused: hasFocusedActions,
58
+ setFocused: setHasFocusedActions,
59
+ }}
60
+ >
61
+ {!isLoading && !disabled && !error && (
62
+ <MainContent title={title} icon={icon} imageData={imageData} className={styles.main} />
63
+ )}
64
+
65
+ {!isLoading && (
66
+ <HoverContent title={title} description={description} error={error} className={styles.hover} />
67
+ )}
68
+
69
+ {isLoading && <LoadingContent />}
70
+ </AttachmentFocusActionsContext.Provider>
52
71
  </AttachmentContext.Provider>
53
72
  }
54
73
  />
@@ -8,17 +8,18 @@ import styles from '../styles.module.scss';
8
8
  type HoverContentProps = Pick<AttachmentProps, 'title' | 'description' | 'error' | 'className'>;
9
9
 
10
10
  export function HoverContent({ title, description, error, className = '' }: HoverContentProps) {
11
- const { disabled } = useAttachmentContext();
11
+ const { disabled, size } = useAttachmentContext();
12
12
 
13
13
  return (
14
14
  <div
15
15
  className={cn(styles.composition, {
16
- [className]: !disabled,
16
+ [className]: !disabled && !error,
17
17
  })}
18
+ data-size={size}
18
19
  >
19
20
  <TextBlock title={title} error={error} description={!error ? description : undefined} align='center' />
20
21
 
21
- <Actions />
22
+ <Actions hideDownload={Boolean(error)} hideRetry={!Boolean(error)} />
22
23
  </div>
23
24
  );
24
25
  }
@@ -1,14 +1,25 @@
1
1
  import { IconPredefined } from '@snack-uikit/icon-predefined';
2
2
  import { Spinner } from '@snack-uikit/loaders';
3
3
 
4
+ import { useAttachmentContext } from '../../../../contexts';
4
5
  import { Actions } from '../../../../helperComponents';
6
+ import { TEST_IDS } from '../../../../testIds';
5
7
  import styles from '../styles.module.scss';
6
8
 
7
9
  export function LoadingContent() {
10
+ const { size } = useAttachmentContext();
11
+
8
12
  return (
9
- <div className={styles.composition}>
10
- <IconPredefined size='s' icon={() => <Spinner size='s' />} decor={false} appearance='neutral' shape='square' />
11
- <Actions hideDownload />
13
+ <div className={styles.composition} data-size={size}>
14
+ <IconPredefined
15
+ size={size}
16
+ icon={() => <Spinner size='s' />}
17
+ decor={false}
18
+ appearance='neutral'
19
+ shape='square'
20
+ data-test-id={TEST_IDS.loading}
21
+ />
22
+ <Actions hideDownload hideRetry />
12
23
  </div>
13
24
  );
14
25
  }
@@ -1,5 +1,4 @@
1
1
  import cn from 'classnames';
2
- import { useMemo } from 'react';
3
2
 
4
3
  import { Card } from '@snack-uikit/card';
5
4
  import { IconPredefined } from '@snack-uikit/icon-predefined';
@@ -7,37 +6,42 @@ import { FileSVG } from '@snack-uikit/icons';
7
6
 
8
7
  import { useAttachmentContext } from '../../../../contexts';
9
8
  import { TextBlock } from '../../../../helperComponents';
10
- import { getFileExtension } from '../../../../helpers';
9
+ import { TEST_IDS } from '../../../../testIds';
11
10
  import { AttachmentProps } from '../../../../types';
12
11
  import styles from '../styles.module.scss';
13
12
 
14
13
  type MainContentProps = {
15
14
  imageData?: string;
16
- } & Pick<AttachmentProps, 'title' | 'extension2Icon' | 'className'>;
15
+ } & Pick<AttachmentProps, 'title' | 'icon' | 'className'>;
17
16
 
18
- export function MainContent({ title, extension2Icon, imageData, className }: MainContentProps) {
19
- const { file } = useAttachmentContext();
20
-
21
- const extension = useMemo(() => getFileExtension(file?.name) ?? '', [file?.name]);
17
+ export function MainContent({ title, icon, imageData, className }: MainContentProps) {
18
+ const { file, size } = useAttachmentContext();
22
19
 
23
20
  if (imageData) {
24
21
  return (
25
22
  <div className={cn(styles.composition, className)}>
26
- <Card.Image src={imageData} alt={file?.name ?? 'file'} mode='background' hideFading />
23
+ <Card.Image
24
+ src={imageData}
25
+ alt={file?.name ?? 'file'}
26
+ mode='background'
27
+ hideFading
28
+ data-test-id={TEST_IDS.image}
29
+ />
27
30
  </div>
28
31
  );
29
32
  }
30
33
 
31
34
  return (
32
- <div className={cn(styles.composition, className)}>
35
+ <div className={cn(styles.composition, className)} data-size={size}>
33
36
  <TextBlock title={title} align='center' />
34
37
 
35
38
  <IconPredefined
36
- size='s'
37
- icon={extension2Icon?.[extension] ?? FileSVG}
39
+ size={size}
40
+ icon={icon ?? FileSVG}
38
41
  decor
39
42
  appearance='neutral'
40
43
  shape='square'
44
+ data-test-id={TEST_IDS.icon}
41
45
  />
42
46
  </div>
43
47
  );
@@ -1,5 +1,7 @@
1
1
  @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-attachment';
2
2
 
3
+ $sizes: 's', 'm';
4
+
3
5
  .composition {
4
6
  @include composite-var($attachment-composition-s);
5
7
 
@@ -14,4 +16,10 @@
14
16
  padding: $dimension-050m;
15
17
 
16
18
  text-align: center;
19
+
20
+ @each $size in $sizes {
21
+ &[data-size='#{$size}'] {
22
+ @include composite-var($attachment, 'composition', $size);
23
+ }
24
+ }
17
25
  }
@@ -1,21 +1,31 @@
1
1
  @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-attachment';
2
2
 
3
3
  .wrapper {
4
- @include composite-var($attachment-container-square);
4
+ &.sizeS {
5
+ @include composite-var($attachment-container-square-s);
6
+ }
7
+
8
+ &.sizeM {
9
+ @include composite-var($attachment-container-square-m);
10
+ }
5
11
 
6
12
  .main {
7
13
  display: flex;
8
14
  }
9
15
 
10
16
  .hover {
11
- display: none;
17
+ pointer-events: none;
18
+ height: 0;
19
+ opacity: 0;
12
20
  }
13
21
 
14
22
  &:hover,
15
- &:focus-within,
16
- &:focus-visible {
23
+ &:focus-visible,
24
+ &.hasFocusedActions {
17
25
  .hover {
18
- display: flex;
26
+ pointer-events: unset;
27
+ height: unset;
28
+ opacity: 1;
19
29
  }
20
30
 
21
31
  .main {
package/src/contexts.ts CHANGED
@@ -4,7 +4,7 @@ import { AttachmentProps } from './types';
4
4
 
5
5
  export type AttachmentContextType = Pick<
6
6
  AttachmentProps,
7
- 'size' | 'file' | 'truncateVariant' | 'onDownload' | 'onDelete' | 'disabled'
7
+ 'size' | 'file' | 'truncateVariant' | 'onDownload' | 'onDelete' | 'onRetry' | 'disabled' | 'truncate'
8
8
  >;
9
9
 
10
10
  export const AttachmentContext = createContext<AttachmentContextType>({});
@@ -12,3 +12,17 @@ export const AttachmentContext = createContext<AttachmentContextType>({});
12
12
  export function useAttachmentContext() {
13
13
  return useContext(AttachmentContext);
14
14
  }
15
+
16
+ export type AttachmentFocusActionsContextType = {
17
+ focused: boolean;
18
+ setFocused(focused: boolean): void;
19
+ };
20
+
21
+ export const AttachmentFocusActionsContext = createContext<AttachmentFocusActionsContextType>({
22
+ focused: false,
23
+ setFocused: () => {},
24
+ });
25
+
26
+ export function useAttachmentFocusActionsContext() {
27
+ return useContext(AttachmentFocusActionsContext);
28
+ }
@@ -1,19 +1,49 @@
1
+ import { MouseEvent } from 'react';
2
+
1
3
  import { ButtonFunction } from '@snack-uikit/button';
2
- import { DownloadSVG, TrashSVG } from '@snack-uikit/icons';
4
+ import { DownloadSVG, TrashSVG, UpdateSVG } from '@snack-uikit/icons';
3
5
 
4
- import { useAttachmentContext } from '../../contexts';
6
+ import { useAttachmentContext, useAttachmentFocusActionsContext } from '../../contexts';
7
+ import { TEST_IDS } from '../../testIds';
5
8
  import styles from './styles.module.scss';
6
9
 
7
10
  type ActionsProps = {
8
11
  hideDownload?: boolean;
9
12
  hideDelete?: boolean;
13
+ hideRetry?: boolean;
10
14
  };
11
15
 
12
- export function Actions({ hideDelete, hideDownload }: ActionsProps) {
13
- const { onDelete, onDownload, file, disabled } = useAttachmentContext();
16
+ const noop = () => {};
17
+
18
+ export function Actions({ hideDelete, hideDownload, hideRetry }: ActionsProps) {
19
+ const { onDelete, onDownload, file, disabled, onRetry } = useAttachmentContext();
20
+ const { setFocused = noop } = useAttachmentFocusActionsContext();
14
21
 
15
22
  const showDownload = onDownload && !hideDownload;
16
23
  const showDelete = onDelete && !hideDelete;
24
+ const showRetry = onRetry && !hideRetry;
25
+
26
+ const handleFocus = () => {
27
+ setFocused(true);
28
+ };
29
+ const handleBlur = () => {
30
+ setFocused(false);
31
+ };
32
+
33
+ const handleRetry = (e: MouseEvent<HTMLButtonElement>) => {
34
+ onRetry?.();
35
+ e.stopPropagation();
36
+ };
37
+
38
+ const handleDownload = (e: MouseEvent<HTMLButtonElement>) => {
39
+ onDownload?.(file);
40
+ e.stopPropagation();
41
+ };
42
+
43
+ const handleDelete = (e: MouseEvent<HTMLButtonElement>) => {
44
+ onDelete?.(file);
45
+ e.stopPropagation();
46
+ };
17
47
 
18
48
  if (!showDownload && !showDelete) {
19
49
  return null;
@@ -26,10 +56,22 @@ export function Actions({ hideDelete, hideDownload }: ActionsProps) {
26
56
  disabled={disabled}
27
57
  size='xs'
28
58
  icon={<DownloadSVG />}
29
- onClick={e => {
30
- onDownload(file);
31
- e.stopPropagation();
32
- }}
59
+ onClick={handleDownload}
60
+ onFocus={handleFocus}
61
+ onBlur={handleBlur}
62
+ data-test-id={TEST_IDS.downloadAction}
63
+ />
64
+ )}
65
+ {showRetry && (
66
+ <ButtonFunction
67
+ disabled={disabled}
68
+ size='xs'
69
+ icon={<UpdateSVG />}
70
+ onClick={handleRetry}
71
+ onFocus={handleFocus}
72
+ onBlur={handleBlur}
73
+ className={styles.retryAction}
74
+ data-test-id={TEST_IDS.retryAction}
33
75
  />
34
76
  )}
35
77
  {showDelete && (
@@ -37,10 +79,10 @@ export function Actions({ hideDelete, hideDownload }: ActionsProps) {
37
79
  disabled={disabled}
38
80
  size='xs'
39
81
  icon={<TrashSVG />}
40
- onClick={e => {
41
- onDelete(file);
42
- e.stopPropagation();
43
- }}
82
+ onClick={handleDelete}
83
+ onFocus={handleFocus}
84
+ onBlur={handleBlur}
85
+ data-test-id={TEST_IDS.deleteAction}
44
86
  />
45
87
  )}
46
88
  </div>
@@ -5,4 +5,18 @@
5
5
 
6
6
  display: flex;
7
7
  flex-shrink: 0;
8
+
9
+ :not([data-disabled]).retryAction {
10
+ svg {
11
+ transform: rotate(360deg);
12
+ transition: transform 0.5s;
13
+ }
14
+
15
+ &:active {
16
+ svg {
17
+ transform: rotate(0deg);
18
+ transition-duration: 0s;
19
+ }
20
+ }
21
+ }
8
22
  }
@@ -1,13 +1,16 @@
1
1
  import { TruncateString } from '@snack-uikit/truncate-string';
2
+ import { extractSupportProps } from '@snack-uikit/utils';
2
3
 
3
4
  import { useAttachmentContext } from '../../contexts';
4
5
 
5
6
  export type TextProps = {
6
7
  text?: string;
7
8
  className: string;
9
+ maxLines?: number;
10
+ 'data-test-id'?: string;
8
11
  };
9
12
 
10
- export function Text({ text, className }: TextProps) {
13
+ export function Text({ text, className, maxLines = 1, ...rest }: TextProps) {
11
14
  const { truncateVariant } = useAttachmentContext();
12
15
 
13
16
  if (!text) {
@@ -16,7 +19,7 @@ export function Text({ text, className }: TextProps) {
16
19
 
17
20
  return (
18
21
  <div className={className}>
19
- <TruncateString text={text} maxLines={1} variant={truncateVariant} />
22
+ <TruncateString text={text} maxLines={maxLines} variant={truncateVariant} {...extractSupportProps(rest)} />
20
23
  </div>
21
24
  );
22
25
  }
@@ -1,6 +1,7 @@
1
1
  import cn from 'classnames';
2
2
 
3
3
  import { useAttachmentContext } from '../../contexts';
4
+ import { TEST_IDS } from '../../testIds';
4
5
  import { AttachmentProps } from '../../types';
5
6
  import { Text } from '../Text';
6
7
  import styles from './styles.module.scss';
@@ -10,13 +11,18 @@ export type TextBlockProps = Pick<AttachmentProps, 'title' | 'description' | 'er
10
11
  };
11
12
 
12
13
  export function TextBlock({ title, description, error, className, align = 'left' }: TextBlockProps) {
13
- const { size } = useAttachmentContext();
14
+ const { size, truncate = {} } = useAttachmentContext();
14
15
 
15
16
  return (
16
17
  <div className={cn(styles.content, className)} data-size={size} data-align={align}>
17
- <Text className={styles.title} text={title} />
18
- <Text className={styles.description} text={description} />
19
- <Text className={styles.error} text={error} />
18
+ <Text className={styles.title} text={title} data-test-id={TEST_IDS.title} maxLines={truncate?.title} />
19
+ <Text
20
+ className={styles.description}
21
+ text={description}
22
+ data-test-id={TEST_IDS.description}
23
+ maxLines={truncate?.description}
24
+ />
25
+ <Text className={styles.error} text={error} data-test-id={TEST_IDS.error} maxLines={truncate?.error} />
20
26
  </div>
21
27
  );
22
28
  }
package/src/testIds.ts ADDED
@@ -0,0 +1,13 @@
1
+ const PREFIX = 'attachment';
2
+
3
+ export const TEST_IDS = {
4
+ icon: `${PREFIX}__icon`,
5
+ image: `${PREFIX}__image`,
6
+ loading: `${PREFIX}__loading`,
7
+ retryAction: `${PREFIX}__retry-action`,
8
+ deleteAction: `${PREFIX}__delete-action`,
9
+ downloadAction: `${PREFIX}__download-action`,
10
+ title: `${PREFIX}__title`,
11
+ description: `${PREFIX}__description`,
12
+ error: `${PREFIX}__error`,
13
+ };
package/src/types.ts CHANGED
@@ -5,13 +5,15 @@ import { WithSupportProps } from '@snack-uikit/utils';
5
5
 
6
6
  export type Size = 's' | 'm';
7
7
 
8
- export type AttachmentCommonProps = WithSupportProps<{
8
+ export type AttachmentSquareProps = WithSupportProps<{
9
9
  /** Файл */
10
10
  file?: File;
11
11
  /** Колбек на клик по кнопке скачивания */
12
12
  onDownload?(file?: File | undefined): void;
13
13
  /** Колбек на клик по кнопке удаления */
14
14
  onDelete?(file?: File | undefined): void;
15
+ /** Колбек на клик по кнопке повторения */
16
+ onRetry?(): void;
15
17
  /**
16
18
  * Заголовок
17
19
  * @default fileName
@@ -30,18 +32,35 @@ export type AttachmentCommonProps = WithSupportProps<{
30
32
  * <br> - `Middle` - по середине
31
33
  * */
32
34
  truncateVariant?: TruncateStringProps['variant'];
35
+
33
36
  /** Управление состоянием загрузки */
34
37
  loading?: boolean;
35
38
  /**
36
- * Объект сопоставления расширения файла и отображаемой иконки
39
+ * Иконка для файла
37
40
  * @defaultIcon FileSVG
38
41
  */
39
- extension2Icon?: Record<string, IconPredefinedProps['icon']>;
40
- }> &
41
- Pick<CardProps, 'checked' | 'disabled' | 'multipleSelection' | 'onClick' | 'className'>;
42
-
43
- export type AttachmentSquareProps = AttachmentCommonProps;
44
- export type AttachmentProps = AttachmentCommonProps & {
42
+ icon?: IconPredefinedProps['icon'];
45
43
  /** Размер */
46
44
  size?: Size;
45
+ }> &
46
+ Pick<CardProps, 'checked' | 'disabled' | 'onClick' | 'className'>;
47
+
48
+ export type AttachmentProps = AttachmentSquareProps & {
49
+ truncate?: {
50
+ /**
51
+ * Максимальное кол-во строк заголовка
52
+ * @default 1
53
+ */
54
+ title?: number;
55
+ /**
56
+ * Максимальное кол-во строк описания
57
+ * @default 1
58
+ */
59
+ description?: number;
60
+ /**
61
+ * Максимальное кол-во строк ошибки
62
+ * @default 1
63
+ */
64
+ error?: number;
65
+ };
47
66
  };
@@ -1,16 +0,0 @@
1
- .wrapper{
2
- width:var(--size-attachment-container-square, 80px);
3
- height:var(--size-attachment-container-square, 80px);
4
- }
5
- .wrapper .main{
6
- display:flex;
7
- }
8
- .wrapper .hover{
9
- display:none;
10
- }
11
- .wrapper:hover .hover, .wrapper:focus-visible .hover{
12
- display:flex;
13
- }
14
- .wrapper:hover .main, .wrapper:focus-visible .main{
15
- display:none;
16
- }
@@ -1,24 +0,0 @@
1
- @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-attachment';
2
-
3
- .wrapper {
4
- @include composite-var($attachment-container-square);
5
-
6
- .main {
7
- display: flex;
8
- }
9
-
10
- .hover {
11
- display: none;
12
- }
13
-
14
- &:hover,
15
- &:focus-visible {
16
- .hover {
17
- display: flex;
18
- }
19
-
20
- .main {
21
- display: none;
22
- }
23
- }
24
- }