-
Notifications
You must be signed in to change notification settings - Fork 0
Components / Image #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Components / Image #125
Changes from 27 commits
99b4e51
7129eb7
7b4163a
44cd601
9d6de33
34dd9f8
819e63c
e9aecbc
9a0f4b6
dbd14a2
72c3977
db5ca42
c7a0937
9684f90
7d9e152
ce4c89e
5859ffa
300d006
3b95421
aa04f83
0a70d56
16db302
78fc796
5889695
a6f3327
90adf7c
7f05f64
f5c6955
a928956
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| src |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # `@byndyusoft-ui/image` | ||
| --- | ||
|
|
||
| ### Installation | ||
| ```sh | ||
| npm i @byndyusoft-ui/image | ||
| # or | ||
| yarn add @byndyusoft-ui/image | ||
| ``` | ||
|
|
||
|
|
||
| ### Usage Image | ||
| #### Basic usage | ||
| ```jsx | ||
| import React from 'react'; | ||
| import Image from '@byndyusoft-ui/image'; | ||
|
|
||
| const App = () => { | ||
| return ( | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default App; | ||
| ``` | ||
|
|
||
| #### Fallback components | ||
| ```jsx | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| fallback={<div>Loading...</div>} | ||
| errorFallback={<div>Error loading image</div>} | ||
| /> | ||
| ``` | ||
|
|
||
| #### Fallback src images | ||
| ```jsx | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| fallbackSrc="https://example.com/fallback.jpg" | ||
| errorFallbackSrc="https://example.com/error.jpg" | ||
| /> | ||
| ``` | ||
|
|
||
| #### Custom class names | ||
| The rootFallbackClassName parameter applies a class to the container that will display the fallback or errorFallback elements. | ||
| ```jsx | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| className="custom-image-class" | ||
| rootFallbackClassName="custom-root-fallback-class" | ||
|
Oustinger marked this conversation as resolved.
|
||
| fallback={<div>Loading...</div>} | ||
| errorFallback={<div>Error loading image</div>} | ||
| /> | ||
| ``` | ||
|
|
||
| #### Lazy loading | ||
| By default, `lazy` is set to `true`, which means the image will only be loaded when it enters the viewport. | ||
| This is achieved using the Intersection Observer pattern. If `lazy` is set to `false`, the image will be loaded immediately. | ||
| ```jsx | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| lazy={false} | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "name": "@byndyusoft-ui/image", | ||
| "version": "0.0.2", | ||
| "description": "Byndyusoft UI Image React Component", | ||
| "keywords": [ | ||
| "byndyusoft", | ||
| "byndyusoft-ui", | ||
| "react", | ||
| "component", | ||
| "image" | ||
| ], | ||
| "author": "Gleb Fomin <gleb.fom28@gmail.com>", | ||
| "homepage": "https://github.com/Byndyusoft/ui/tree/master/components/image#readme", | ||
| "license": "Apache-2.0", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/Byndyusoft/ui.git" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc --project tsconfig.build.json", | ||
| "clean": "rimraf dist", | ||
| "lint": "eslint src --config ../../eslint.config.js", | ||
| "test": "jest --config ../../jest.config.js --roots components/image/src" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/Byndyusoft/ui/issues" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "peerDependencies": { | ||
| "react": ">=17", | ||
| "@byndyusoft-ui/use-intersection-observer": "^0.0.1" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import React, { forwardRef, ReactElement, useImperativeHandle, useRef } from 'react'; | ||
| import { useImage } from './useImage'; | ||
| import type { IImageProps } from './Image.types'; | ||
|
|
||
| const Image = forwardRef<HTMLImageElement, IImageProps>((props, forwardedRef) => { | ||
| const { | ||
| src, | ||
| alt = '', | ||
| lazy = true, | ||
| fallback, | ||
| fallbackSrc, | ||
| errorFallback, | ||
| errorFallbackSrc, | ||
| className, | ||
| rootFallbackClassName, | ||
| ...otherProps | ||
| } = props; | ||
|
|
||
| const internalRef = useRef<HTMLImageElement | null>(null); | ||
|
|
||
| const { isLoading, isError, setObserverTargetRef } = useImage({ src, lazy }); | ||
|
|
||
| const setRefs = (node: HTMLImageElement | null): void => { | ||
| internalRef.current = node; | ||
| setObserverTargetRef(node); | ||
| }; | ||
|
|
||
| const renderImage = (imageSrc: string): JSX.Element => ( | ||
| <img ref={setRefs} className={className} src={imageSrc} alt={alt} {...otherProps} /> | ||
| ); | ||
|
|
||
| const renderFallback = (content: ReactElement): JSX.Element => ( | ||
| <div ref={setRefs} className={rootFallbackClassName}> | ||
| {content} | ||
| </div> | ||
| ); | ||
|
|
||
| useImperativeHandle(forwardedRef, () => internalRef.current as HTMLImageElement); | ||
|
|
||
| if (fallback && isLoading) return renderFallback(fallback); | ||
|
|
||
| if (errorFallback && isError) return renderFallback(errorFallback); | ||
|
|
||
| if (fallbackSrc && isLoading) return renderImage(fallbackSrc); | ||
|
|
||
| if (errorFallbackSrc && isError) return renderImage(errorFallbackSrc); | ||
|
|
||
| return renderImage(src); | ||
| }); | ||
|
|
||
| Image.displayName = 'Image'; | ||
|
|
||
| export default Image; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { Dispatch, ImgHTMLAttributes, ReactElement, SetStateAction } from 'react'; | ||
| import { Callback } from '@byndyusoft-ui/types'; | ||
|
|
||
| export interface IImageProps extends ImgHTMLAttributes<HTMLImageElement> { | ||
| src: string; | ||
| className?: string; | ||
| rootFallbackClassName?: string; | ||
| fallback?: ReactElement; | ||
| fallbackSrc?: string; | ||
| errorFallback?: ReactElement; | ||
| errorFallbackSrc?: string; | ||
| lazy?: boolean; | ||
| } | ||
|
|
||
| type TSetState<T> = Dispatch<SetStateAction<T>>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Мб вынести этот тип в пакет с типами?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можно. Задам этот вопрос на созвоне стрима
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Не задал) Спроси в чате) |
||
|
|
||
| export interface IUseImageProps { | ||
| src: string; | ||
| lazy?: boolean; | ||
| } | ||
|
|
||
| export interface IUseImageReturn { | ||
| setObserverTargetRef: TSetState<Element | null>; | ||
| isLoading: boolean; | ||
| isError: boolean; | ||
| } | ||
|
|
||
| export type TLoadImageFunction = Callback< | ||
| [src: string, setIsLoading: TSetState<boolean>, setIsError: TSetState<boolean>] | ||
| >; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { Meta } from '@storybook/addon-docs'; | ||
| import { Markdown } from '@storybook/blocks'; | ||
| import Readme from '../../README.md'; | ||
|
|
||
| <Meta title="components/Image" /> | ||
|
|
||
| <Markdown>{Readme}</Markdown> | ||
|
Oustinger marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| .wrapper { | ||
| margin: auto; | ||
| width: 800px; | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: 20px; | ||
| } | ||
|
|
||
| .skeleton { | ||
| background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); | ||
| background-size: 200% 100%; | ||
| animation: shimmer 1.5s infinite; | ||
| border-radius: 4px; | ||
| } | ||
|
|
||
| .errorFallback { | ||
| box-sizing: border-box; | ||
| border: 4px solid #7a0000; | ||
| background: #ffa9a9; | ||
| color: #b30000; | ||
| font-size: 32px; | ||
| display: flex; | ||
| align-items: center; | ||
| flex-direction: column; | ||
| justify-content: center; | ||
| } | ||
|
|
||
| @keyframes shimmer { | ||
| 0% { | ||
| background-position: 200% 0; | ||
| } | ||
| 100% { | ||
| background-position: -200% 0; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| import React from 'react'; | ||
| import type { StoryObj } from '@storybook/react'; | ||
| import Image from '../Image'; | ||
| import { IImageProps } from '../Image.types'; | ||
| import imagePlaceholder from './image-placeholder.png'; | ||
| import cls from './Image.stories.module.css'; | ||
|
|
||
| interface ISkeletonProps { | ||
| width: number | string; | ||
| height: number | string; | ||
| } | ||
|
|
||
| interface ITemplateProps { | ||
| mockImageIds: Array<number>; | ||
| imageProps: Partial<IImageProps>; | ||
| } | ||
|
|
||
| const generateRandomArray = (maxNumber: number, length: number): number[] => { | ||
| const randomArray = []; | ||
| for (let i = 0; i < length; i++) { | ||
| const randomNumber = Math.floor(Math.random() * maxNumber) + 1; | ||
| randomArray.push(randomNumber); | ||
| } | ||
| return randomArray; | ||
| }; | ||
|
|
||
| const FallbackSkeleton = ({ width, height }: ISkeletonProps): JSX.Element => ( | ||
| <div className={cls.skeleton} style={{ width, height }} /> | ||
| ); | ||
|
|
||
| const ErrorFallbackComponent = ({ width, height }: ISkeletonProps): JSX.Element => ( | ||
| <div className={cls.errorFallback} style={{ width, height }}> | ||
| Error Fallback | ||
| </div> | ||
| ); | ||
|
|
||
| const Template = ({ mockImageIds, imageProps }: ITemplateProps): JSX.Element => ( | ||
| <div className={cls.wrapper}> | ||
| {mockImageIds?.map((id, index) => ( | ||
| <Image | ||
| {...imageProps} | ||
| key={`${id}_${index + 1}`} | ||
| src={`https://rickandmortyapi.com/api/character/avatar/${id}.jpeg`} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
|
|
||
| export const LazyFallbackSkeleton: StoryObj<typeof Template> = { | ||
| args: { | ||
| mockImageIds: generateRandomArray(826, 100), | ||
| imageProps: { | ||
| width: 300, | ||
| height: 300, | ||
| fallback: <FallbackSkeleton width={300} height={300} />, | ||
| errorFallback: <ErrorFallbackComponent width={300} height={300} /> | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| export const LazyFallbackSrc: StoryObj<typeof Template> = { | ||
| args: { | ||
| mockImageIds: generateRandomArray(826, 100), | ||
| imageProps: { | ||
| width: 300, | ||
| height: 300, | ||
| fallbackSrc: imagePlaceholder, | ||
| errorFallback: <ErrorFallbackComponent width={300} height={300} /> | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| export const PreloadFallbackSkeleton: StoryObj<typeof Template> = { | ||
| args: { | ||
| mockImageIds: generateRandomArray(826, 30), | ||
| imageProps: { | ||
| width: 300, | ||
| height: 300, | ||
| lazy: false, | ||
| fallback: <FallbackSkeleton width={300} height={300} />, | ||
| errorFallback: <ErrorFallbackComponent width={300} height={300} /> | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| export const PreloadFallbackSrc: StoryObj<typeof Template> = { | ||
| args: { | ||
| mockImageIds: generateRandomArray(826, 30), | ||
| imageProps: { | ||
| width: 300, | ||
| height: 300, | ||
| lazy: false, | ||
| fallbackSrc: imagePlaceholder, | ||
| errorFallback: <ErrorFallbackComponent width={300} height={300} /> | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| export const ErrorFallback: StoryObj<typeof Template> = { | ||
| args: { | ||
| mockImageIds: [-1, -2, -3, -4, -5, -6], | ||
| imageProps: { | ||
| width: 300, | ||
| height: 300, | ||
| fallback: <FallbackSkeleton width={300} height={300} />, | ||
| errorFallback: <ErrorFallbackComponent width={300} height={300} /> | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| export const ErrorFallbackSrc: StoryObj<typeof Template> = { | ||
| args: { | ||
| mockImageIds: [-1, -2, -3, -4, -5, -6], | ||
| imageProps: { | ||
| width: 300, | ||
| height: 300, | ||
| fallback: <FallbackSkeleton width={300} height={300} />, | ||
| errorFallbackSrc: imagePlaceholder | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| export default { | ||
| title: 'components/Image', | ||
| component: Template | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.