Skip to content

Commit 78cb8b3

Browse files
committed
feat(Gallery): add Gallery component
1 parent 7b95b8e commit 78cb8b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1430
-0
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
/src/components/StoreBadge @NikitaCG
1414
/src/components/Stories @darkgenius
1515
/src/components/ConfirmDialog @kseniya57
16+
/src/components/Gallery @kseniya57

src/components/Gallery/Gallery.scss

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
@use '../variables';
2+
3+
$block: '.#{variables.$ns}gallery';
4+
$filePreviewBlock: '.g-file-preview';
5+
6+
#{$block} {
7+
--g-modal-margin: 0;
8+
9+
&__content {
10+
display: flex;
11+
flex-direction: column;
12+
13+
width: calc(100vw - 264px);
14+
height: calc(100vh - 56px);
15+
}
16+
17+
&__header {
18+
display: flex;
19+
align-items: start;
20+
21+
padding: var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-2) var(--g-spacing-5);
22+
23+
> * {
24+
flex: 1;
25+
min-width: 0;
26+
}
27+
}
28+
29+
&__navigation {
30+
display: flex;
31+
gap: var(--g-spacing-2);
32+
align-items: center;
33+
justify-content: center;
34+
}
35+
36+
&__actions {
37+
display: flex;
38+
gap: var(--g-spacing-1);
39+
align-items: stretch;
40+
justify-content: flex-end;
41+
}
42+
43+
&__active-item-info {
44+
align-self: stretch;
45+
align-items: center;
46+
display: flex;
47+
}
48+
49+
&__body {
50+
position: relative;
51+
52+
display: flex;
53+
align-items: center;
54+
justify-content: center;
55+
56+
flex: 1;
57+
min-height: 0;
58+
59+
padding: 0 var(--g-spacing-2);
60+
}
61+
62+
&__body-navigation-button {
63+
position: absolute;
64+
inset-block: 0 60px;
65+
z-index: 2;
66+
67+
width: 200px;
68+
max-width: 20%;
69+
padding: 0;
70+
margin: 0;
71+
72+
appearance: none;
73+
cursor: pointer;
74+
75+
background-color: transparent;
76+
border: none;
77+
outline: none;
78+
79+
&_direction_left {
80+
inset-inline-start: 0;
81+
82+
cursor:
83+
url('./assets/arrow-left.svg') 2 2,
84+
default;
85+
}
86+
87+
&_direction_right {
88+
inset-inline-end: var(--g-spacing-7);
89+
90+
cursor:
91+
url('./assets/arrow-right.svg') 2 2,
92+
default;
93+
}
94+
}
95+
96+
&__footer {
97+
padding: var(--g-spacing-2) var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5);
98+
}
99+
100+
&__preview-list {
101+
display: flex;
102+
gap: var(--g-spacing-2);
103+
align-items: stretch;
104+
overflow: auto hidden;
105+
-ms-overflow-style: none;
106+
scrollbar-width: none;
107+
108+
&::-webkit-scrollbar {
109+
display: none;
110+
}
111+
}
112+
113+
&__preview-list-item {
114+
width: 48px;
115+
min-width: 48px;
116+
height: 48px;
117+
border: 2px solid transparent;
118+
border-radius: var(--g-border-radius-l);
119+
padding: 0;
120+
margin: 0;
121+
122+
appearance: none;
123+
cursor: pointer;
124+
125+
background-color: transparent;
126+
outline: none;
127+
overflow: hidden;
128+
129+
&_selected {
130+
border-color: var(--g-color-line-brand);
131+
}
132+
}
133+
134+
&_mode_full-screen {
135+
overflow: hidden;
136+
137+
--g-modal-border-radius: 0;
138+
139+
#{$block} {
140+
&__content {
141+
width: 100vw;
142+
height: 100vh;
143+
}
144+
145+
&__body {
146+
padding: 0;
147+
}
148+
149+
&__header {
150+
position: absolute;
151+
inset-block-start: 0;
152+
inset-inline: 0;
153+
z-index: 3;
154+
155+
opacity: 0;
156+
157+
&:hover {
158+
opacity: 1;
159+
}
160+
}
161+
162+
&__footer {
163+
position: absolute;
164+
inset-inline: 0;
165+
inset-block-end: 0;
166+
z-index: 1;
167+
168+
opacity: 0;
169+
background-color: rgba(0, 0, 0, 0.45);
170+
171+
&:hover {
172+
opacity: 1;
173+
}
174+
}
175+
}
176+
177+
.g-root_theme_light,
178+
.g-root_theme_light-hc {
179+
#{$block}__header {
180+
background-color: var(--g-color-private-white-450);
181+
}
182+
}
183+
184+
.g-root_theme_dark,
185+
.g-root_theme_dark-hc {
186+
#{$block}__header {
187+
background-color: var(--g-color-private-black-450);
188+
}
189+
}
190+
}
191+
192+
#{$filePreviewBlock} {
193+
&[class] {
194+
width: 100%;
195+
height: 100%;
196+
}
197+
198+
&__card {
199+
width: 100%;
200+
min-width: 100%;
201+
height: 100%;
202+
padding: 0;
203+
}
204+
205+
&__image,
206+
&__icon {
207+
width: 100%;
208+
height: 100%;
209+
}
210+
211+
&__name {
212+
display: none;
213+
}
214+
}
215+
}

src/components/Gallery/Gallery.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import * as React from 'react';
2+
3+
import {ArrowLeft, ArrowRight, Xmark} from '@gravity-ui/icons';
4+
import type {ModalProps} from '@gravity-ui/uikit';
5+
import {Button, Icon, Modal, Text, ThemeProvider, useThemeValue} from '@gravity-ui/uikit';
6+
7+
import {block} from '../utils/cn';
8+
9+
import type {GalleryItemProps} from './GalleryItem';
10+
import {FilesGalleryFallbackText} from './components/FallbackText';
11+
import type {UseNavigationProps} from './hooks/useNavigation';
12+
import {useNavigation} from './hooks/useNavigation';
13+
import {i18n} from './i18n';
14+
import {getInvertedTheme} from './utils/getInvertedTheme';
15+
16+
import './Gallery.scss';
17+
18+
const cnGallery = block('gallery');
19+
20+
const emptyItems: GalleryItemProps[] = [];
21+
22+
export type GalleryProps = {
23+
fullScreen?: boolean;
24+
modalClassName?: string;
25+
className?: string;
26+
children?: React.ReactElement<GalleryItemProps>[];
27+
invertTheme?: boolean;
28+
noItemsMessage?: string;
29+
} & Pick<ModalProps, 'open' | 'container'> &
30+
Required<Pick<ModalProps, 'onOpenChange'>> &
31+
Pick<UseNavigationProps, 'initialItemIndex'>;
32+
33+
export const Gallery = ({
34+
initialItemIndex,
35+
open,
36+
onOpenChange,
37+
fullScreen,
38+
container,
39+
modalClassName,
40+
className,
41+
invertTheme,
42+
children,
43+
noItemsMessage,
44+
}: GalleryProps) => {
45+
const items = children ? React.Children.map(children, (child) => child.props) : emptyItems;
46+
const theme = useThemeValue();
47+
48+
const {activeItemIndex, setActiveItemIndex, handleGoToNext, handleGoToPrevious} = useNavigation(
49+
{
50+
itemsCount: items.length,
51+
initialItemIndex,
52+
selectedPreviewItemClass: `.${cnGallery('preview-list-item')}_selected`,
53+
},
54+
);
55+
56+
const handleClose = React.useCallback(() => {
57+
onOpenChange?.(false);
58+
}, [onOpenChange]);
59+
60+
const activeItem = items[activeItemIndex] || items[0];
61+
console.log(activeItem);
62+
return (
63+
<Modal
64+
container={container}
65+
className={cnGallery({mode: fullScreen ? 'full-screen' : 'default'}, modalClassName)}
66+
open={open}
67+
onOpenChange={onOpenChange}
68+
disableEscapeKeyDown={fullScreen}
69+
>
70+
<ThemeProvider theme={invertTheme ? getInvertedTheme(theme) : theme}>
71+
<div className={cnGallery('content', className)}>
72+
<div className={cnGallery('header')}>
73+
<div className={cnGallery('active-item-info')}>{activeItem?.meta}</div>
74+
{items.length > 0 && (
75+
<div className={cnGallery('navigation')}>
76+
<Button size="l" view="flat" onClick={handleGoToPrevious}>
77+
<Icon data={ArrowLeft} />
78+
</Button>
79+
<Text color="secondary" variant="body-1">
80+
{activeItemIndex + 1}/{items.length}
81+
</Text>
82+
<Button size="l" view="flat" onClick={handleGoToNext}>
83+
<Icon data={ArrowRight} />
84+
</Button>
85+
</div>
86+
)}
87+
<div className={cnGallery('actions')}>
88+
{activeItem?.actions}
89+
<Button
90+
size="l"
91+
view="flat"
92+
aria-label={i18n('close')}
93+
onClick={handleClose}
94+
>
95+
<Icon data={Xmark} />
96+
</Button>
97+
</div>
98+
</div>
99+
<div key={activeItemIndex} className={cnGallery('body')}>
100+
{!items.length && (
101+
<FilesGalleryFallbackText>
102+
{noItemsMessage ?? i18n('no-items')}
103+
</FilesGalleryFallbackText>
104+
)}
105+
{activeItem?.view}
106+
{activeItem && !activeItem.interactive && (
107+
<React.Fragment>
108+
<button
109+
onClick={handleGoToPrevious}
110+
type="button"
111+
className={cnGallery('body-navigation-button', {
112+
direction: 'left',
113+
})}
114+
/>
115+
<button
116+
onClick={handleGoToNext}
117+
type="button"
118+
className={cnGallery('body-navigation-button', {
119+
direction: 'right',
120+
})}
121+
/>
122+
</React.Fragment>
123+
)}
124+
</div>
125+
{!fullScreen && (
126+
<div className={cnGallery('footer')}>
127+
<div className={cnGallery('preview-list')}>
128+
{items.map((item, index) => {
129+
const handleClick = () => {
130+
setActiveItemIndex(index);
131+
};
132+
133+
const selected = activeItemIndex === index;
134+
135+
return (
136+
<button
137+
key={index}
138+
onClick={handleClick}
139+
className={cnGallery('preview-list-item', {selected})}
140+
>
141+
{item.thumbnail}
142+
</button>
143+
);
144+
})}
145+
</div>
146+
</div>
147+
)}
148+
</div>
149+
</ThemeProvider>
150+
</Modal>
151+
);
152+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as React from 'react';
2+
3+
export type GalleryItemProps = {
4+
view: React.ReactNode;
5+
thumbnail: React.ReactNode;
6+
meta?: React.ReactNode;
7+
actions?: React.ReactNode[];
8+
interactive?: boolean;
9+
};
10+
11+
export const GalleryItem = (_props: GalleryItemProps) => {
12+
return null;
13+
};

0 commit comments

Comments
 (0)