Skip to content

Commit a29f63a

Browse files
committed
feat(Gallery): add a hook for opening the gallery from the custom content
1 parent 8266e89 commit a29f63a

19 files changed

+488
-27
lines changed

src/components/Gallery/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The children of the Gallery should be an array of [GalleryItem with the required
1212
| open | `Boolean` | | | | The modal opened state |
1313
| onOpenChange | `(open: boolean) => void` | | | | The modal toggle handler |
1414
| className | `String` | | | | The modal class |
15+
| container | `HTMLElement` | | | | The modal container |
1516
| emptyMessage | `String` | | | No data | No data message |
1617

1718
### GalleryItem

src/components/Gallery/__stories__/mockData.ts

+32-27
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
export const images = [
2-
'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg',
3-
'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg',
4-
'https://cs4.pikabu.ru/post_img/big/2015/02/27/6/1425024947_2006737473.jpeg',
5-
'https://i.pinimg.com/originals/ef/7b/97/ef7b9724ad06cd6dfce92193e95a5caa.jpg',
6-
'https://avatars.mds.yandex.net/i?id=ea31df78678a1b3f4f1fb7199090831d_l-5235412-images-thumbs&n=13',
7-
'https://i.ytimg.com/vi/WA63GQpLzjA/maxresdefault.jpg',
8-
'https://i.pinimg.com/originals/02/eb/fd/02ebfd63d5435ec87c7413b8b2428214.jpg',
9-
'https://mir-s3-cdn-cf.behance.net/project_modules/max_3840/2b800731080995.5640a39521da5.jpg',
10-
'https://pic.rutubelist.ru/video/7a/1b/7a1b88f88ff7a470ea6f8131d51c2c5c.jpg',
11-
'https://i.pinimg.com/originals/4b/c7/ed/4bc7ed612f2080303644deb0f857b70f.jpg',
12-
'https://img1.reactor.cc/pics/post/нейроарт-нейронные-сети-красивые-картинки-art-7821877.png',
13-
'https://steamuserimages-a.akamaihd.net/ugc/841461304090603934/D3243F5856FEAE2052FC7CDB748B5BB65E6B247A/?imw=512&imh=306&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true',
14-
'https://celes.club/uploads/posts/2022-06/1654752045_50-celes-club-p-multyashnii-kosmos-oboi-krasivie-53.jpg',
15-
// duplicate the list to show the previews scroll
16-
'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg',
17-
'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg',
18-
'https://cs4.pikabu.ru/post_img/big/2015/02/27/6/1425024947_2006737473.jpeg',
19-
'https://i.pinimg.com/originals/ef/7b/97/ef7b9724ad06cd6dfce92193e95a5caa.jpg',
20-
'https://avatars.mds.yandex.net/i?id=ea31df78678a1b3f4f1fb7199090831d_l-5235412-images-thumbs&n=13',
21-
'https://i.ytimg.com/vi/WA63GQpLzjA/maxresdefault.jpg',
22-
'https://i.pinimg.com/originals/02/eb/fd/02ebfd63d5435ec87c7413b8b2428214.jpg',
23-
'https://mir-s3-cdn-cf.behance.net/project_modules/max_3840/2b800731080995.5640a39521da5.jpg',
24-
'https://pic.rutubelist.ru/video/7a/1b/7a1b88f88ff7a470ea6f8131d51c2c5c.jpg',
25-
'https://i.pinimg.com/originals/4b/c7/ed/4bc7ed612f2080303644deb0f857b70f.jpg',
26-
'https://img1.reactor.cc/pics/post/нейроарт-нейронные-сети-красивые-картинки-art-7821877.png',
27-
'https://steamuserimages-a.akamaihd.net/ugc/841461304090603934/D3243F5856FEAE2052FC7CDB748B5BB65E6B247A/?imw=512&imh=306&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true',
28-
'https://celes.club/uploads/posts/2022-06/1654752045_50-celes-club-p-multyashnii-kosmos-oboi-krasivie-53.jpg',
2+
'https://i.pinimg.com/originals/e7/44/1a/e7441aebde7c4d5a5afc476d5fa87082.jpg',
3+
'https://i.pinimg.com/736x/46/81/f0/4681f0072c3b5b8a96fdf78c4e12037c.jpg',
4+
'https://i.pinimg.com/736x/ae/2a/a3/ae2aa360b9c18d0be08ef7279ab81638.jpg',
5+
'https://i.pinimg.com/originals/42/b4/eb/42b4ebb23e452387a84c3ad02295003f.jpg',
6+
'https://i.pinimg.com/736x/3a/e0/af/3ae0af7c666a284847d682156b8a6496.jpg',
7+
'https://i.pinimg.com/736x/30/91/28/3091283eeff577749c5e3c81a9af7ce1.jpg',
8+
'https://i.pinimg.com/736x/61/ce/22/61ce22263ae275efa9b9c7b02b3dea61.jpg',
9+
'https://i.pinimg.com/736x/72/70/a6/7270a6d918468fcce5c297afc281bf35.jpg',
10+
'https://i.pinimg.com/originals/cc/8d/cf/cc8dcfb1f6bcb54a2836019558027ef7.jpg',
11+
'https://i.pinimg.com/736x/cd/74/75/cd7475dd2fa19d339e147b627896c5d4.jpg',
12+
'https://i.pinimg.com/736x/aa/21/dd/aa21ddd2b3de5298953ca0762838951e.jpg',
13+
'https://i.pinimg.com/originals/9c/3a/16/9c3a161254af0369c2a32bae240da2ff.jpg',
14+
'https://i.pinimg.com/originals/d0/48/41/d048413c59db4dd7d1e332da991ce347.jpg',
15+
'https://i.pinimg.com/736x/a9/4a/4b/a94a4b6810787f415166bc9d0bef71eb.jpg',
16+
'https://i.pinimg.com/736x/9c/b2/d1/9cb2d19f5cf6bdb53b7c1f42f480db3f.jpg',
17+
'https://i.pinimg.com/736x/34/94/5e/34945e08218f073f4f1d3b147108b06a.jpg',
18+
'https://i.pinimg.com/originals/a6/30/93/a6309348c1dab1899d022b8759dc659b.jpg',
19+
'https://i.pinimg.com/736x/69/e0/aa/69e0aac5a17f8790da23dcc379e6fd05.jpg',
20+
'https://i.pinimg.com/736x/e3/f2/b4/e3f2b4f87a0ae9744a0b14c81710463c.jpg',
21+
'https://i.pinimg.com/736x/f0/d4/01/f0d4011c75b92f165dbab83c8654ebf1.jpg',
22+
'https://i.pinimg.com/736x/69/51/5a/69515a0da57d33b5df80d5af0c14ef7e.jpg',
23+
'https://i.pinimg.com/736x/07/b6/f5/07b6f527be3860832e55e6ec041766b6.jpg',
24+
'https://i.pinimg.com/originals/32/a3/30/32a33050f32ba74c2b66456d458cb792.jpg',
25+
'https://i.pinimg.com/originals/07/73/12/077312668712ae5aba52766b5b3e2b8e.jpg',
26+
'https://i.pinimg.com/736x/5d/15/35/5d1535227d2eb616b21113d69e5cf49b.jpg',
27+
'https://i.pinimg.com/736x/12/e5/5a/12e55a138ef6345d359d9f6cf0715a5d.jpg',
28+
'https://i.pinimg.com/736x/90/da/f6/90daf6e058bc377b1a3c1cf53d63bde7.jpg',
29+
'https://i.pinimg.com/originals/0f/e4/66/0fe4667784f51ac62a7fbb8c84b16f2f.jpg',
30+
'https://i.pinimg.com/736x/ed/11/1e/ed111e9003c2d972b7e09816deef26de.jpg',
31+
'https://i.pinimg.com/736x/cf/4e/ac/cf4eac1954a1c1b4c9d43dbb8f8752a8.jpg',
32+
'https://i.pinimg.com/originals/ee/50/90/ee509026a73aae62ef3ffd1860f2db5f.jpg',
33+
'https://i.pinimg.com/originals/d8/a0/53/d8a0536b3855ee1cdeaaf1eb3bb71aed.jpg',
2934
];
3035

3136
export type GalleryFile =

src/hooks/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './useGallery';
2+
export * from './useFilesGalleryFromContent';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## useFilesGalleryFromContent
2+
3+
The hook for opening the gallery from html content with images and videos
4+
5+
### PropTypes
6+
7+
_GalleryContextProvider_:
8+
9+
| Property | Type | Required | Values | Default | Description |
10+
| :----------- | :---------------------------- | :------- | :----- | :------ | :------------------ |
11+
| theme | `ThemeProviderProps['theme']` | | | `dark` | The gallery theme |
12+
| className | `String` | | | | The modal class |
13+
| container | `HTMLElement` | | | | The modal container |
14+
| emptyMessage | `String` | | | No data | No data message |
15+
16+
_useFilesGalleryFromContent_
17+
18+
| Property | Type | Required | Values | Default | Description |
19+
| :---------- | :----------------------------------- | :------- | :----- | :------ | :-------------------------------------------------------------------------------------------------------------------------- |
20+
| customFiles | `(GalleryItem & { url?: string })[]` | | | | The additional files list (pass the url to be able to exclude the items from content if they are found in the custom files) |
21+
22+
_useFilesGalleryFromContent returns function with args_:
23+
24+
| Property | Type | Required | Values | Default | Description |
25+
| :------- | :--------------------------------- | :------- | :----- | :------ | :-------------- |
26+
| event | `React.MouseEvent<HTMLDivElement>` | Yes | | | The click event |
27+
28+
### Usage
29+
30+
First you should wrap your content into the GalleryContextProvider to be able to use the hook
31+
32+
```tsx
33+
import {GalleryContextProvider} from '@gravity-ui/components';
34+
35+
<GalleryContextProvider theme="dark" emptyMessage="Seems like your gallery is empty!">
36+
children
37+
</GalleryContextProvider>;
38+
```
39+
40+
Then use the hook inside your component
41+
42+
```tsx
43+
import {useFilesGalleryFromContent, getGalleryItemImage} from '@gravity-ui/components';
44+
45+
const customImages = [
46+
'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg',
47+
'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg',
48+
];
49+
50+
const customFiles = customImages.map((image) => ({
51+
url: image,
52+
...getGalleryItemImage({src: image, name: image}),
53+
}));
54+
55+
const openFilesGalleryFromContent = useFilesGalleryFromContent(customFiles);
56+
57+
<div onClick={openFilesGalleryFromContent}>
58+
<img
59+
src="https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.jpg"
60+
alt="Corgi"
61+
/>
62+
<a href="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4">
63+
My video
64+
</a>
65+
</div>;
66+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {Text} from '@gravity-ui/uikit';
2+
3+
import {GalleryContextProvider} from '../../useGallery';
4+
import {useFilesGalleryFromContent} from '../useFilesGalleryFromContent';
5+
6+
const UseFilesGalleryFromContentExample = () => {
7+
const openFilesGalleryFromContent = useFilesGalleryFromContent();
8+
9+
return (
10+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
11+
<div onClick={openFilesGalleryFromContent}>
12+
<Text variant="subheader-3" as={'h2' as const}>
13+
Click image or video link to open the gallery
14+
</Text>
15+
<a href="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4">
16+
My video
17+
</a>
18+
<br />
19+
<img
20+
src="https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.jpg"
21+
alt="Corgi"
22+
/>
23+
</div>
24+
);
25+
};
26+
27+
export const UseFilesGalleryFromContentShowcase = () => {
28+
return (
29+
<GalleryContextProvider>
30+
<UseFilesGalleryFromContentExample />
31+
</GalleryContextProvider>
32+
);
33+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type {Meta} from '@storybook/react';
2+
3+
import {UseFilesGalleryFromContentShowcase} from './UseFilesGalleryFromContentShowcase';
4+
5+
export default {
6+
title: 'Hooks/useFilesGalleryFromContent',
7+
parameters: {
8+
a11y: {
9+
element: '#storybook-root',
10+
config: {
11+
rules: [
12+
{
13+
id: 'color-contrast',
14+
enabled: false,
15+
},
16+
],
17+
},
18+
},
19+
},
20+
} as Meta;
21+
22+
export const Showcase = UseFilesGalleryFromContentShowcase;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const supportedImageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'bmp'];
2+
3+
export const supportedVideoExtensions = ['mp4', 'webm', 'ogg'];
4+
5+
export const supportedExtensions = [...supportedImageExtensions, ...supportedVideoExtensions];
6+
7+
export const extensionRegex = /\w+?$/;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './useFilesGalleryFromContent';
2+
export * from './types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {GalleryItemProps} from '../../components';
2+
3+
export type GalleryItemPropsWithUrl = GalleryItemProps & {
4+
// pass the url to be able to exclude the items from content if they are found in the custom files
5+
url?: string;
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as React from 'react';
2+
3+
import {getGalleryItemImage, getGalleryItemVideo} from '../../components';
4+
import {useGallery} from '../useGallery';
5+
6+
import {extensionRegex, supportedExtensions, supportedVideoExtensions} from './constants';
7+
import {GalleryItemPropsWithUrl} from './types';
8+
9+
export function useFilesGalleryFromContent(customFiles?: GalleryItemPropsWithUrl[]) {
10+
const openFilesGallery = useGallery();
11+
12+
return React.useCallback(
13+
(event: React.MouseEvent<HTMLDivElement>) => {
14+
if (event.target instanceof HTMLElement) {
15+
let fileLink = '';
16+
17+
if (event.target.tagName === 'IMG' && !event.target.closest('a')) {
18+
fileLink = event.target.getAttribute('src') ?? '';
19+
} else if (event.target.tagName === 'A') {
20+
fileLink = event.target.getAttribute('href') ?? '';
21+
}
22+
23+
if (!fileLink) {
24+
return;
25+
}
26+
27+
const filesFromContent = [
28+
...(event.currentTarget?.querySelectorAll('img,a') ?? []),
29+
].reduce<GalleryItemPropsWithUrl[]>((result, element) => {
30+
const isImage = element.tagName === 'IMG';
31+
const link = isImage
32+
? element.getAttribute('src')
33+
: element.getAttribute('href');
34+
35+
if (link && !customFiles?.some((item) => item.url === link)) {
36+
const extension = link.match(extensionRegex)?.[0] || '';
37+
38+
if (isImage || supportedExtensions.includes(extension)) {
39+
const name =
40+
(isImage
41+
? element.getAttribute('alt')
42+
: element.getAttribute('title')) || '';
43+
44+
result.push({
45+
...(supportedVideoExtensions.includes(extension)
46+
? getGalleryItemVideo({src: link, name: name})
47+
: getGalleryItemImage({src: link, name: name})),
48+
url: link,
49+
});
50+
}
51+
}
52+
53+
return result;
54+
}, []);
55+
56+
const files = [...(customFiles ?? []), ...filesFromContent];
57+
58+
const initialItemIndex = files.findIndex((item) => item.url === fileLink);
59+
60+
if (initialItemIndex !== -1) {
61+
event.preventDefault();
62+
openFilesGallery(files, initialItemIndex);
63+
}
64+
}
65+
},
66+
[customFiles, openFilesGallery],
67+
);
68+
}

src/hooks/useGallery/Gallery.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as React from 'react';
2+
3+
export const Gallery = React.lazy(() =>
4+
import('../../components/Gallery/Gallery').then((module) => ({default: module.Gallery})),
5+
);
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
3+
import {GalleryItemProps} from '../../components';
4+
5+
export type GalleryContextType = {
6+
openGallery: (items: GalleryItemProps[], initialFileIndex?: number) => void;
7+
};
8+
9+
export const GalleryContext = React.createContext<GalleryContextType>({
10+
openGallery: () => {},
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react';
2+
3+
import {ThemeProvider, ThemeProviderProps} from '@gravity-ui/uikit';
4+
5+
import {GalleryItem, GalleryItemProps, GalleryProps} from '../../components';
6+
7+
import {Gallery} from './Gallery';
8+
import {GalleryContext, type GalleryContextType} from './GalleryContext';
9+
10+
export type GalleryContextProviderProps = React.PropsWithChildren<
11+
Pick<GalleryProps, 'container' | 'className' | 'emptyMessage'>
12+
> &
13+
Pick<ThemeProviderProps, 'theme'>;
14+
15+
export const GalleryContextProvider = ({
16+
children,
17+
container,
18+
emptyMessage,
19+
theme,
20+
className,
21+
}: GalleryContextProviderProps) => {
22+
const [isOpen, setIsOpen] = React.useState(false);
23+
const [{items, initialItemIndex}, setGalleryProps] = React.useState<{
24+
items: GalleryItemProps[];
25+
initialItemIndex: number;
26+
}>({items: [], initialItemIndex: 0});
27+
28+
const contextValue = React.useMemo<GalleryContextType>(
29+
() => ({
30+
openGallery: (items, initialFileIndex) => {
31+
setGalleryProps({
32+
items,
33+
initialItemIndex: initialFileIndex ?? 0,
34+
});
35+
setIsOpen(true);
36+
},
37+
}),
38+
[],
39+
);
40+
41+
return (
42+
<GalleryContext.Provider value={contextValue}>
43+
{children}
44+
{isOpen && (
45+
<React.Suspense fallback={null}>
46+
<ThemeProvider theme={theme}>
47+
<Gallery
48+
open={isOpen}
49+
onOpenChange={setIsOpen}
50+
container={container}
51+
className={className}
52+
emptyMessage={emptyMessage}
53+
initialItemIndex={initialItemIndex}
54+
>
55+
{items.map((file, index) => (
56+
<GalleryItem key={index} {...file} />
57+
))}
58+
</Gallery>
59+
</ThemeProvider>
60+
</React.Suspense>
61+
)}
62+
</GalleryContext.Provider>
63+
);
64+
};

0 commit comments

Comments
 (0)