Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
99b4e51
feat(Image): add image component with fallback
glebfomin28 Oct 6, 2024
7129eb7
feat(hooks/use-intersection-observer): add hook and stories
glebfomin28 Nov 3, 2024
7b4163a
feat(hooks/useIntersectionObserver): added tests for observe.ts
Nov 5, 2024
44cd601
Merge branch 'refs/heads/master' into feature/useIntersectionObserver
Nov 5, 2024
9d6de33
feat(hooks/useIntersectionObserver): added configuration and readme
Nov 5, 2024
34dd9f8
feat(hooks/useIntersectionObserver): minor
Nov 5, 2024
819e63c
chore(hooks/useIntersectionObserver): update readme, rename variables
Nov 11, 2024
e9aecbc
feat(hooks/useIntersectionObserver): add new tests and minor fixes
Nov 11, 2024
9a0f4b6
chore(hooks/useIntersectionObserver): removed unused imports
Nov 11, 2024
dbd14a2
chore(hooks/useIntersectionObserver): update tests, stories
Nov 13, 2024
72c3977
chore(hooks/useIntersectionObserver): readme
Nov 13, 2024
db5ca42
chore(hooks/useIntersectionObserver): deleted comments
Nov 13, 2024
c7a0937
chore(hooks/useIntersectionObserver): rename tests helpers
Nov 15, 2024
9684f90
Merge branch 'refs/heads/feature/useIntersectionObserver' into featur…
Nov 18, 2024
7d9e152
feat(image): integration with useIntersectionObserver
Nov 18, 2024
ce4c89e
feat(image): add tests and readme
Nov 19, 2024
5859ffa
chore(useIntersectionObserver): import tests helpers
Nov 19, 2024
300d006
chore(useIntersectionObserver): import tests helpers
Nov 20, 2024
3b95421
chore(useIntersectionObserver): delete import tests helpers
Nov 20, 2024
aa04f83
chore(image): update readme, add stories docs
Nov 20, 2024
0a70d56
chore(image): improve code structure and readability
Nov 20, 2024
16db302
chore(image): update readme
Nov 20, 2024
78fc796
chore(image): add mocks for testing
Nov 22, 2024
5889695
chore(image): add return types and fix errors
Nov 28, 2024
a6f3327
chore(use-intersection-observer): add return types
Nov 28, 2024
90adf7c
fix(use-intersection-observer): fix eslint errors
glebfomin28 Nov 29, 2024
7f05f64
Merge branch 'feature/useIntersectionObserver' into feature/image-com…
glebfomin28 Nov 29, 2024
f5c6955
chore(image): update component, stories, readme
Dec 4, 2024
a928956
fix(image): readme
Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/image/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src
109 changes: 109 additions & 0 deletions components/image/README.md
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"
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.
37 changes: 37 additions & 0 deletions components/image/package.json
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"
}
}
63 changes: 63 additions & 0 deletions components/image/src/Image.tsx
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;
34 changes: 34 additions & 0 deletions components/image/src/Image.types.ts
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>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мб вынести этот тип в пакет с типами?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно. Задам этот вопрос на созвоне стрима

Copy link
Contributor

Choose a reason for hiding this comment

The 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>]
>;
44 changes: 44 additions & 0 deletions components/image/src/__stories__/Image.stories.mdx
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>

# 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>
59 changes: 59 additions & 0 deletions components/image/src/__stories__/Image.stories.module.css
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;
}
}
Loading