From 1ab8fc106848a586b70fac70db31bcb825a7096f Mon Sep 17 00:00:00 2001 From: Misael Arreola Date: Fri, 2 May 2025 15:47:41 -0600 Subject: [PATCH 1/2] feat: add file list components and new layout of input file --- exports.json | 18 ++ package.json | 24 ++ .../Form/File/FileImageItem.test.tsx | 70 +++++ src/components/Form/File/FileImageItem.tsx | 187 +++++++++++++ src/components/Form/File/FileItem.test.tsx | 32 +++ src/components/Form/File/FileItem.tsx | 105 +++++++ .../Form/File/FileItemButton.test.tsx | 30 ++ src/components/Form/File/FileItemButton.tsx | 69 +++++ .../Form/File/FileItemDropdown.test.tsx | 28 ++ src/components/Form/File/FileItemDropdown.tsx | 45 +++ src/components/Form/File/FileList.test.tsx | 28 ++ src/components/Form/File/FileList.tsx | 55 ++++ src/components/Form/File/index.ts | 15 + src/components/Form/Input/InputFile.test.tsx | 28 +- src/components/Form/Input/InputFile.tsx | 257 ++++++++++++------ src/components/Form/index.ts | 3 + src/stories/FileItem.stories.tsx | 31 +++ src/stories/FileItemImage.stories.tsx | 33 +++ src/stories/FileList.stories.tsx | 96 +++++++ src/stories/InputFile.stories.tsx | 15 +- 20 files changed, 1061 insertions(+), 108 deletions(-) create mode 100644 src/components/Form/File/FileImageItem.test.tsx create mode 100644 src/components/Form/File/FileImageItem.tsx create mode 100644 src/components/Form/File/FileItem.test.tsx create mode 100644 src/components/Form/File/FileItem.tsx create mode 100644 src/components/Form/File/FileItemButton.test.tsx create mode 100644 src/components/Form/File/FileItemButton.tsx create mode 100644 src/components/Form/File/FileItemDropdown.test.tsx create mode 100644 src/components/Form/File/FileItemDropdown.tsx create mode 100644 src/components/Form/File/FileList.test.tsx create mode 100644 src/components/Form/File/FileList.tsx create mode 100644 src/components/Form/File/index.ts create mode 100644 src/stories/FileItem.stories.tsx create mode 100644 src/stories/FileItemImage.stories.tsx create mode 100644 src/stories/FileList.stories.tsx diff --git a/exports.json b/exports.json index 10c831f5f..dc7432a03 100644 --- a/exports.json +++ b/exports.json @@ -152,6 +152,21 @@ "module": "./dist/esm/components/FeedBackBox/index.js", "default": "./dist/esm/components/FeedBackBox/index.js" }, + "./FileItem": { + "require": "./dist/cjs/components/Form/File/FileItem.js", + "module": "./dist/esm/components/Form/File/FileItem.js", + "default": "./dist/esm/components/Form/File/FileList.js" + }, + "./FileImageItem": { + "require": "./dist/cjs/components/Form/File/FileImageItem.js", + "module": "./dist/esm/components/Form/File/FileImageItem.js", + "default": "./dist/esm/components/Form/File/FileImageItem.js" + }, + "./FileList": { + "require": "./dist/cjs/components/Form/File/FileList.js", + "module": "./dist/esm/components/Form/File/FileList.js", + "default": "./dist/esm/components/Form/File/FileList.js" + }, "./FileViewer": { "require": "./dist/cjs/components/FileViewer/index.js", "module": "./dist/esm/components/FileViewer/index.js", @@ -442,6 +457,9 @@ "Dropdown": ["./dist/esm/components/Dropdown/index.d.ts"], "DynamicHeroIcon": ["./dist/esm/common/DynamicHeroIcon/index.d.ts"], "FeedBackBox": ["./dist/esm/components/FeedBackBox/index.d.ts"], + "FileItem": ["./dist/esm/components/Form/File/FileItem.d.ts"], + "FileImageItem": ["./dist/esm/components/Form/File/FileImageItem.d.ts"], + "FileList": ["./dist/esm/components/Form/File/FileList.d.ts"], "FileViewer": ["./dist/esm/components/FileViewer/index.d.ts"], "Filters": ["./dist/esm/components/Filters/index.d.ts"], "FormControl": ["./dist/esm/components/FormControl/index.d.ts"], diff --git a/package.json b/package.json index 444fc774d..201f737c7 100644 --- a/package.json +++ b/package.json @@ -288,6 +288,21 @@ "module": "./dist/esm/components/FeedBackBox/index.js", "default": "./dist/esm/components/FeedBackBox/index.js" }, + "./FileItem": { + "require": "./dist/cjs/components/Form/File/FileItem.js", + "module": "./dist/esm/components/Form/File/FileItem.js", + "default": "./dist/esm/components/Form/File/FileList.js" + }, + "./FileImageItem": { + "require": "./dist/cjs/components/Form/File/FileImageItem.js", + "module": "./dist/esm/components/Form/File/FileImageItem.js", + "default": "./dist/esm/components/Form/File/FileImageItem.js" + }, + "./FileList": { + "require": "./dist/cjs/components/Form/File/FileList.js", + "module": "./dist/esm/components/Form/File/FileList.js", + "default": "./dist/esm/components/Form/File/FileList.js" + }, "./FileViewer": { "require": "./dist/cjs/components/FileViewer/index.js", "module": "./dist/esm/components/FileViewer/index.js", @@ -636,6 +651,15 @@ "FeedBackBox": [ "./dist/esm/components/FeedBackBox/index.d.ts" ], + "FileItem": [ + "./dist/esm/components/Form/File/FileItem.d.ts" + ], + "FileImageItem": [ + "./dist/esm/components/Form/File/FileImageItem.d.ts" + ], + "FileList": [ + "./dist/esm/components/Form/File/FileList.d.ts" + ], "FileViewer": [ "./dist/esm/components/FileViewer/index.d.ts" ], diff --git a/src/components/Form/File/FileImageItem.test.tsx b/src/components/Form/File/FileImageItem.test.tsx new file mode 100644 index 000000000..852136115 --- /dev/null +++ b/src/components/Form/File/FileImageItem.test.tsx @@ -0,0 +1,70 @@ +import { vi } from 'vitest' +import { render, fireEvent } from '@testing-library/react' +import FileImageItem from './FileImageItem' + +const defaultProps = { + name: 'image.jpg', + type: 'image/jpeg', + src: 'https://example.com/image.jpg', + title: 'Test Image', + description: 'This is a test image description', + onChangeTitle: vi.fn(), + onChangeDescription: vi.fn() +} + +describe('', () => { + it('renders component correctly', () => { + const { getByRole } = render() + + expect(getByRole('file-item-image')).toBeDefined() + }) + + it('displays the image with correct src', () => { + const { getByRole } = render() + const image = getByRole('file-image') + + expect(image).toHaveAttribute('src', 'https://example.com/image.jpg') + }) + + it('calls onChangeTitle when title input changes', () => { + const { getByDisplayValue } = render() + const titleInput = getByDisplayValue('Test Image') + + fireEvent.change(titleInput, { + target: { value: 'Updated Title', name: 'title' } + }) + + expect(defaultProps.onChangeTitle).toHaveBeenCalled() + expect(getByDisplayValue('Updated Title')).toBeDefined() + }) + + it('calls onChangeDescription when description input changes', () => { + const { getByDisplayValue } = render() + const descriptionInput = getByDisplayValue( + 'This is a test image description' + ) + + fireEvent.change(descriptionInput, { + target: { value: 'Updated description', name: 'description' } + }) + + expect(defaultProps.onChangeDescription).toHaveBeenCalled() + expect(getByDisplayValue('Updated description')).toBeDefined() + }) + + it('displays character count for description', () => { + const { getByRole, getByDisplayValue } = render( + + ) + const counter = getByRole('file-description-length') + const descriptionInput = getByDisplayValue( + 'This is a test image description' + ) + + fireEvent.change(descriptionInput, { + target: { value: 'Short', name: 'description' } + }) + + expect(counter).toHaveTextContent('5/100') + }) +}) diff --git a/src/components/Form/File/FileImageItem.tsx b/src/components/Form/File/FileImageItem.tsx new file mode 100644 index 000000000..2c4821c7a --- /dev/null +++ b/src/components/Form/File/FileImageItem.tsx @@ -0,0 +1,187 @@ +import { + ChangeEvent, + ChangeEventHandler, + CSSProperties, + ReactNode, + useState +} from 'react' +import { Flex } from 'components/Layout' +import Text from 'components/Typography' +import BaseInput from '../Input/BaseInput' +import TextArea from '../TextArea' +import FileItem from './FileItem' + +export interface FileImageItemProps { + /** + * Source URL for the image preview + */ + src: string + /** + * Title of the file + */ + title: string + /** + * Description of the file + */ + description: string + /** + * Placeholder text for the title input + */ + titlePlaceholder?: string + /** + * Placeholder text for the description input + */ + descriptionPlaceholder?: string + /** + * Label for the title input + */ + titleLabel?: string + /** + * Label for the description input + */ + descriptionLabel?: string + /** + * Maximum length for the description text + */ + descriptionMaxLength?: number + /** + * Name of the file to display + */ + name: string + /** + * Type/format of the file + */ + type: string + /** + * Size of the file in KB (optional) + */ + fileSize?: number + /** + * Additional CSS class names + */ + className?: string + /** + * Custom CSS styles + */ + style?: CSSProperties + /** + * Children components (typically action buttons) + */ + children?: ReactNode + /** + * Handler for title changes + */ + onChangeTitle: ChangeEventHandler + /** + * Handler for description changes + */ + onChangeDescription: ChangeEventHandler +} + +interface stateType { + title: string + description: string +} + +export const FileImageItem = ({ + name, + type, + fileSize, + src, + title, + description, + titlePlaceholder, + descriptionPlaceholder, + titleLabel, + descriptionLabel, + descriptionMaxLength = 100, + className, + style, + children, + onChangeTitle, + onChangeDescription +}: FileImageItemProps) => { + const [state, setState] = useState({ + title: title || '', + description: description || '' + }) + + const handleChange = ( + e: ChangeEvent | ChangeEvent + ) => { + setState((prev) => ({ + ...prev, + [e.target.name]: e.target.value + })) + e.target.name === 'title' && + onChangeTitle(e as ChangeEvent) + e.target.name === 'description' && + onChangeDescription(e as ChangeEvent) + } + + return ( + + + {children} + +
+
+ File +
+ + + +