diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e1a8d..0991b96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index ba32f8a..12de3a5 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/DragFileUploadInput/DragFileUploadInput.stories.tsx b/src/components/DragFileUploadInput/DragFileUploadInput.stories.tsx index ef744e9..aadab91 100644 --- a/src/components/DragFileUploadInput/DragFileUploadInput.stories.tsx +++ b/src/components/DragFileUploadInput/DragFileUploadInput.stories.tsx @@ -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' +}; diff --git a/src/components/DragFileUploadInput/index.tsx b/src/components/DragFileUploadInput/index.tsx index e459b67..5ae5dd4 100644 --- a/src/components/DragFileUploadInput/index.tsx +++ b/src/components/DragFileUploadInput/index.tsx @@ -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 }; @@ -55,6 +56,26 @@ export type DragFileUploadInputProps = Omit( @@ -72,6 +93,11 @@ export const DragFileUploadInput = forwardRef +
) : filesDescription ? ( <> - -

{filesDescription}

+ {previewSrc ? ( + + ) : ( + + )} + {!hideFilesDescription &&

{filesDescription}

} ) : hasDragSupport ? ( isDragging ? ( diff --git a/src/components/ImageLoader/ImageLoader.stories.tsx b/src/components/ImageLoader/ImageLoader.stories.tsx new file mode 100644 index 0000000..b21c333 --- /dev/null +++ b/src/components/ImageLoader/ImageLoader.stories.tsx @@ -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 = (props) => ; + +export const Default = Template.bind({}); diff --git a/src/components/ImageLoader/index.tsx b/src/components/ImageLoader/index.tsx new file mode 100644 index 0000000..b8ad908 --- /dev/null +++ b/src/components/ImageLoader/index.tsx @@ -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, 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 = ({ + src, + observerOptions, + loaderClassName, + containerClassName, + ...props +}) => { + const imgRef = useRef(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 ( + + {!imgLoaded &&
} + + + ); +}; diff --git a/src/components/index.tsx b/src/components/index.tsx index fef5114..8a42bf3 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -29,3 +29,4 @@ export * from './FormikSubmitOnChange'; export * from './ButtonStack'; export * from './DatePickerInput'; export * from './DateRangePickerInput'; +export * from './ImageLoader';