Skip to content

Commit

Permalink
Merge pull request #101 from perimetre/feature/add-thumbnail-to-file-…
Browse files Browse the repository at this point in the history
…input

feat: added imageLoader and preview for file upload
  • Loading branch information
AssisrMatheus authored Apr 11, 2023
2 parents 025bf61 + 0652689 commit e6e4178
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 6 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

## [9.4.0] 2023-04-11

### Added

- Added `ImageLoader` component

### Changes

- Added preview options for `DragFileUploadInput`

## [9.3.2] 2023-04-06

### Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@perimetre/ui",
"description": "A component library made by @perimetre",
"version": "9.3.2",
"version": "9.4.0",
"repository": {
"type": "git",
"url": "git+https://github.com/perimetre/ui.git"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,11 @@ export const InitialDescription = Template.bind({});
InitialDescription.args = {
initialFilesDescription: 'image.png'
};

export const PreviewContent = Template.bind({});
PreviewContent.args = {
initialFilesDescription: 'cat.png',
previewSrc: 'https://cataas.com/cat',
previewClassName: 'max-h-64',
previewLoaderClassName: 'h-64 w-72'
};
48 changes: 43 additions & 5 deletions src/components/DragFileUploadInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo
import { addMultipleEventListeners, removeMultipleEventListeners } from '../../utils/dom';
import { AttentionIcon, CrossIcon } from '../Icons';
import { defaultDragFileUploadTranslations, DragFileUploadTranslations } from './translations';
import { ImageLoader } from '../ImageLoader';

export type DragFileUploadInputRef = { reset: () => void };

Expand Down Expand Up @@ -55,6 +56,26 @@ export type DragFileUploadInputProps = Omit<React.InputHTMLAttributes<HTMLInputE
* The initial description to show in the input when it is loaded
*/
initialFilesDescription?: string;
/**
* The class used for the container that wraps the input
*/
containerClassName?: string;
/**
* The SRC content used to preview the file
*/
previewSrc?: string;
/**
* Classname for the preview image
*/
previewClassName?: string;
/**
* Classname for the preview image loader
*/
previewLoaderClassName?: string;
/**
* Whether or not to hide the file description text
*/
hideFilesDescription?: boolean;
};

export const DragFileUploadInput = forwardRef<DragFileUploadInputRef, DragFileUploadInputProps>(
Expand All @@ -72,6 +93,11 @@ export const DragFileUploadInput = forwardRef<DragFileUploadInputRef, DragFileUp
onReset: onResetProps,
translations: translationsProps,
initialFilesDescription,
containerClassName,
previewSrc,
previewClassName,
previewLoaderClassName,
hideFilesDescription,
...props
}: DragFileUploadInputProps,
ref
Expand Down Expand Up @@ -322,10 +348,13 @@ export const DragFileUploadInput = forwardRef<DragFileUploadInputRef, DragFileUp
[onReset]
);

if (previewSrc && !previewLoaderClassName)
throw new Error('DragFileUploadInput: You must provide a previewLoaderClassName when using previewSrc');

return (
<div className="pui-drag-file-container">
<div className={classnames('pui-drag-file-container', containerClassName)}>
<div
className={classnames('relative', {
className={classnames('relative h-full', {
'pui-drag-file-error': !!error || errorProps,
'pui-drag-file-success': success,
'pui-drag-file-loading': loading,
Expand All @@ -335,7 +364,7 @@ export const DragFileUploadInput = forwardRef<DragFileUploadInputRef, DragFileUp
<label
htmlFor={id}
ref={labelRef}
className={classnames('pui-drag-file-label', {
className={classnames('pui-drag-file-label h-full', {
disabled: props.disabled
// dragging: isDragging,
// error: !!error
Expand All @@ -349,8 +378,17 @@ export const DragFileUploadInput = forwardRef<DragFileUploadInputRef, DragFileUp
</>
) : filesDescription ? (
<>
<FontAwesomeIcon icon={faFileUpload} className="pui-animate-fadeIn" size="2x" />
<p className="pui-drag-file-text pui-animate-fadeIn">{filesDescription}</p>
{previewSrc ? (
<ImageLoader
className={previewClassName}
loaderClassName={previewLoaderClassName || 'h-64 w-64'}
src={previewSrc}
alt={filesDescription}
/>
) : (
<FontAwesomeIcon icon={faFileUpload} className="pui-animate-fadeIn" size="2x" />
)}
{!hideFilesDescription && <p className="pui-drag-file-text pui-animate-fadeIn">{filesDescription}</p>}
</>
) : hasDragSupport ? (
isDragging ? (
Expand Down
24 changes: 24 additions & 0 deletions src/components/ImageLoader/ImageLoader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
import { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { ImageLoader, ImageLoaderProps } from '.';

export default {
title: 'Components/ImageLoader',
component: ImageLoader,
argTypes: {
src: { defaultValue: 'https://cataas.com/cat' },
alt: { defaultValue: 'An image of a cat' },
className: { defaultValue: 'w-64 h-64 object-cover' },
loaderClassName: { defaultValue: 'w-64 h-64' }
}
} as Meta;

/**
* A story that displays a ImageLoader example
*
* @param props the story props
*/
const Template: Story<ImageLoaderProps> = (props) => <ImageLoader {...props} />;

export const Default = Template.bind({});
82 changes: 82 additions & 0 deletions src/components/ImageLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable jsx-a11y/alt-text */
import classnames from 'classnames';
import React, { useLayoutEffect, useRef, useState } from 'react';

export type ImageLoaderProps = React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> & {
/**
* The image alt tag
*/
alt: string; // Never make "alt" optional.
/**
* Class name for the loader
*/
loaderClassName: string; // Required because the user must provide desired height/width
/**
* Class name for the container
*/
containerClassName?: string;
/**
* Options for the intersection observer
*/
observerOptions?: IntersectionObserverInit;
};

/**
* Wraps the image tag with a loader placeholder that shows up until the image is loaded
*
* @param props The image props
* @param props.src The image src tag
* @param props.observerOptions The options for the intersection observer
* @param props.loaderClassName The class name for the loader
* @param props.containerClassName The class name for the container
*/
export const ImageLoader: React.FC<ImageLoaderProps> = ({
src,
observerOptions,
loaderClassName,
containerClassName,
...props
}) => {
const imgRef = useRef<HTMLImageElement | null>(null);
const [imgLoaded, setImgLoaded] = useState(false);

useLayoutEffect(() => {
const img = imgRef.current;

if (!!img) {
// Creates a new intersection observer to check if the image is visible or not
// Image only gets loaded if it's in screen
const observer = new IntersectionObserver(
() => {
/**
* Callback for when the image is loaded
*/
img.onload = () => setImgLoaded(true);
img.src = src || '';
},
{
threshold: 0.2, // Only trigger with this threshold
...observerOptions
}
);

observer.observe(img);

return () => {
observer.unobserve(img);
observer.disconnect();
};
}

return;
// Don't need to listen for observerOptions changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [imgRef, src]);

return (
<span className={containerClassName}>
{!imgLoaded && <div className={classnames('pui-skeleton', loaderClassName)} />}
<img style={{ display: !imgLoaded ? 'none' : undefined }} ref={imgRef} {...props} />
</span>
);
};
1 change: 1 addition & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export * from './FormikSubmitOnChange';
export * from './ButtonStack';
export * from './DatePickerInput';
export * from './DateRangePickerInput';
export * from './ImageLoader';

0 comments on commit e6e4178

Please sign in to comment.