Skip to content

Commit ac0cb65

Browse files
committed
feat(Gallery): add a hook for opening the gallery from the custom content
1 parent 6639b0c commit ac0cb65

19 files changed

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

src/hooks/useGallery/README.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## useGallery
2+
3+
The hook for opening the gallery
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+
_openFilesGallery returns function with args_:
17+
18+
| Property | Type | Required | Values | Default | Description |
19+
| :--------------- | :-------------- | :------- | :----- | :------ | :--------------------- |
20+
| items | `GalleryItem[]` | Yes | | | The gallery items |
21+
| initialItemIndex | `number` | | | 0 | The initial item index |
22+
23+
### Usage
24+
25+
First you should wrap your content into the GalleryContextProvider to be able to use the hook
26+
27+
```tsx
28+
import {GalleryContextProvider} from '@gravity-ui/components';
29+
30+
<GalleryContextProvider theme="dark" emptyMessage="Seems like your gallery is empty!">
31+
children
32+
</GalleryContextProvider>;
33+
```
34+
35+
Then use the hook inside your custom hooks or components
36+
37+
```tsx
38+
import {useGallery, getGalleryItemImage} from '@gravity-ui/components';
39+
40+
const openGallery = useGallery();
41+
42+
const images = [
43+
'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg',
44+
'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg',
45+
];
46+
47+
openGallery(
48+
images.map((image) => getGalleryItemImage({src: image, name: image})),
49+
2,
50+
);
51+
```

0 commit comments

Comments
 (0)