-
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 all 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,109 @@ | ||
| # `@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 adds a class to the root element that displays the `fallback` content, | ||
| while the `rootErrorFallbackClassName` parameter adds a class to the root element that displays the `errorFallback` content. | ||
|
|
||
| ```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.
Show resolved
Hide resolved
|
||
| rootErrorFallbackClassName="custom-root-error-fallback-class" | ||
| fallback={<div>Loading...</div>} | ||
| errorFallback={<div>Error loading image</div>} | ||
| /> | ||
| ``` | ||
|
|
||
| #### Lazy loading | ||
|
|
||
| By default, `lazy` is set to `false`, which means the image will be loaded immediately. If `lazy` is set to `true`, | ||
| the image will only be loaded when it enters the viewport. This is achieved using the Intersection Observer pattern. | ||
| For correct lazy loading, it is also necessary to pass the `fallback` attribute, which will be placeholder as a placeholder until the image is loaded. | ||
|
|
||
| By default, `lazy` is set to `false`, which means the image will be loaded immediately. If `lazy` is set to `true`, | ||
| the image will only be loaded when it enters the viewport. This is achieved using the Intersection Observer pattern. | ||
| For correct lazy loading, it is also necessary to pass the `fallback` attribute, which will serve as a placeholder until the image is loaded. | ||
|
|
||
| ```jsx | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| fallback={<div>Loading...</div>} | ||
| lazy | ||
| /> | ||
| ``` | ||
|
|
||
| #### Settings Intersection Observer | ||
|
|
||
| You can customize the options for the Intersection Observer using the `intersectionObserverSettings` attribute. | ||
|
|
||
| ```jsx | ||
| <Image | ||
| src="https://example.com/image.jpg" | ||
| alt="Example Image" | ||
| intersectionObserverSettings={{ | ||
| threshold: 0.5, | ||
| //...others options | ||
| }} | ||
| /> | ||
| ``` | ||
|
|
||
| > Modify the `intersectionObserverSettings` attribute with caution, as incorrect settings can disrupt the lazy loading | ||
| > mechanism and potentially lead to unexpected behavior. | ||
| 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,63 @@ | ||
| 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 = false, | ||
| fallback, | ||
| fallbackSrc, | ||
| errorFallback, | ||
| errorFallbackSrc, | ||
| className, | ||
| rootFallbackClassName, | ||
| rootErrorFallbackClassName, | ||
| intersectionObserverSettings, | ||
| ...otherProps | ||
| } = props; | ||
|
|
||
| const internalRef = useRef<HTMLImageElement | null>(null); | ||
|
|
||
| const { isLoading, isError, setObserverTargetRef } = useImage({ | ||
| src, | ||
| lazy, | ||
| intersectionObserverSettings | ||
| }); | ||
|
|
||
| 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, rootClassName?: string): JSX.Element => ( | ||
| <div ref={setRefs} className={rootClassName}> | ||
| {content} | ||
| </div> | ||
| ); | ||
|
|
||
| useImperativeHandle(forwardedRef, () => internalRef.current as HTMLImageElement); | ||
|
|
||
| if (fallback && isLoading) { | ||
| return renderFallback(fallback, rootFallbackClassName); | ||
| } | ||
|
|
||
| if (errorFallback && isError) { | ||
| return renderFallback(errorFallback, rootErrorFallbackClassName); | ||
| } | ||
|
|
||
| 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,34 @@ | ||
| import { Dispatch, ImgHTMLAttributes, ReactElement, SetStateAction } from 'react'; | ||
| import { Callback } from '@byndyusoft-ui/types'; | ||
| import { IUseIntersectionObserverOptions } from '@byndyusoft-ui/use-intersection-observer'; | ||
|
|
||
| export interface IImageProps extends ImgHTMLAttributes<HTMLImageElement> { | ||
| src: string; | ||
| className?: string; | ||
| rootFallbackClassName?: string; | ||
| rootErrorFallbackClassName?: string; | ||
| fallback?: ReactElement; | ||
| fallbackSrc?: string; | ||
| errorFallback?: ReactElement; | ||
| errorFallbackSrc?: string; | ||
| lazy?: boolean; | ||
| intersectionObserverSettings?: IUseIntersectionObserverOptions; | ||
| } | ||
|
|
||
| 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; | ||
| intersectionObserverSettings?: IUseIntersectionObserverOptions; | ||
| } | ||
|
|
||
| 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,44 @@ | ||
| import { Meta, Story, Canvas } 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.
Show resolved
Hide resolved
|
||
|
|
||
| # Image stories | ||
| ### Lazy fallback skeleton | ||
|
|
||
| <Canvas> | ||
| <Story id="components-image--lazy-fallback-skeleton" /> | ||
| </Canvas> | ||
|
|
||
| ### Lazy fallback src | ||
|
|
||
| <Canvas> | ||
| <Story id="components-image--lazy-fallback-src" /> | ||
| </Canvas> | ||
|
|
||
| ### Preload fallback skeleton | ||
|
|
||
| <Canvas> | ||
| <Story id="components-image--preload-fallback-skeleton" /> | ||
| </Canvas> | ||
|
|
||
| ### Preload fallback src | ||
|
|
||
| <Canvas> | ||
| <Story id="components-image--preload-fallback-src" /> | ||
| </Canvas> | ||
|
|
||
| ### Error fallback | ||
|
|
||
| <Canvas> | ||
| <Story id="components-image--error-fallback" /> | ||
| </Canvas> | ||
|
|
||
| ### Error fallback src | ||
|
|
||
| <Canvas> | ||
| <Story id="components-image--error-fallback-src" /> | ||
| </Canvas> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| .wrapper { | ||
| margin: auto; | ||
| display: flex; | ||
| gap: 12px; | ||
| flex-wrap: wrap; | ||
| width: 612px; | ||
| max-height: 600px; | ||
| } | ||
|
|
||
| .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; | ||
| } | ||
|
|
||
| .refresh_btn { | ||
| position: fixed; | ||
| top: 15px; | ||
| left: 15px; | ||
| z-index: 999; | ||
| cursor: pointer; | ||
| background: yellowgreen; | ||
| border-radius: 50%; | ||
| width: 60px; | ||
| height: 60px; | ||
| border: none; | ||
| box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.38); | ||
| } | ||
|
|
||
| .refresh_btn:hover { | ||
| scale: 1.03; | ||
| } | ||
|
|
||
| .refresh_btn:active { | ||
| scale: 1.09; | ||
| opacity: 0.5; | ||
| } | ||
|
|
||
| @keyframes shimmer { | ||
| 0% { | ||
| background-position: 200% 0; | ||
| } | ||
| 100% { | ||
| background-position: -200% 0; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.