Skip to content

Commit

Permalink
feat(Gallery): add Gallery component
Browse files Browse the repository at this point in the history
  • Loading branch information
kseniyakuzina committed Feb 5, 2025
1 parent 7b95b8e commit 78cb8b3
Show file tree
Hide file tree
Showing 41 changed files with 1,430 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
/src/components/StoreBadge @NikitaCG
/src/components/Stories @darkgenius
/src/components/ConfirmDialog @kseniya57
/src/components/Gallery @kseniya57
215 changes: 215 additions & 0 deletions src/components/Gallery/Gallery.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
@use '../variables';

$block: '.#{variables.$ns}gallery';
$filePreviewBlock: '.g-file-preview';

#{$block} {
--g-modal-margin: 0;

&__content {
display: flex;
flex-direction: column;

width: calc(100vw - 264px);
height: calc(100vh - 56px);
}

&__header {
display: flex;
align-items: start;

padding: var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-2) var(--g-spacing-5);

> * {
flex: 1;
min-width: 0;
}
}

&__navigation {
display: flex;
gap: var(--g-spacing-2);
align-items: center;
justify-content: center;
}

&__actions {
display: flex;
gap: var(--g-spacing-1);
align-items: stretch;
justify-content: flex-end;
}

&__active-item-info {
align-self: stretch;
align-items: center;
display: flex;
}

&__body {
position: relative;

display: flex;
align-items: center;
justify-content: center;

flex: 1;
min-height: 0;

padding: 0 var(--g-spacing-2);
}

&__body-navigation-button {
position: absolute;
inset-block: 0 60px;
z-index: 2;

width: 200px;
max-width: 20%;
padding: 0;
margin: 0;

appearance: none;
cursor: pointer;

background-color: transparent;
border: none;
outline: none;

&_direction_left {
inset-inline-start: 0;

cursor:
url('./assets/arrow-left.svg') 2 2,
default;
}

&_direction_right {
inset-inline-end: var(--g-spacing-7);

cursor:
url('./assets/arrow-right.svg') 2 2,
default;
}
}

&__footer {
padding: var(--g-spacing-2) var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5);
}

&__preview-list {
display: flex;
gap: var(--g-spacing-2);
align-items: stretch;
overflow: auto hidden;
-ms-overflow-style: none;
scrollbar-width: none;

&::-webkit-scrollbar {
display: none;
}
}

&__preview-list-item {
width: 48px;
min-width: 48px;
height: 48px;
border: 2px solid transparent;
border-radius: var(--g-border-radius-l);
padding: 0;
margin: 0;

appearance: none;
cursor: pointer;

background-color: transparent;
outline: none;
overflow: hidden;

&_selected {
border-color: var(--g-color-line-brand);
}
}

&_mode_full-screen {
overflow: hidden;

--g-modal-border-radius: 0;

#{$block} {
&__content {
width: 100vw;
height: 100vh;
}

&__body {
padding: 0;
}

&__header {
position: absolute;
inset-block-start: 0;
inset-inline: 0;
z-index: 3;

opacity: 0;

&:hover {
opacity: 1;
}
}

&__footer {
position: absolute;
inset-inline: 0;
inset-block-end: 0;
z-index: 1;

opacity: 0;
background-color: rgba(0, 0, 0, 0.45);

&:hover {
opacity: 1;
}
}
}

.g-root_theme_light,
.g-root_theme_light-hc {
#{$block}__header {
background-color: var(--g-color-private-white-450);
}
}

.g-root_theme_dark,
.g-root_theme_dark-hc {
#{$block}__header {
background-color: var(--g-color-private-black-450);
}
}
}

#{$filePreviewBlock} {
&[class] {
width: 100%;
height: 100%;
}

&__card {
width: 100%;
min-width: 100%;
height: 100%;
padding: 0;
}

&__image,
&__icon {
width: 100%;
height: 100%;
}

&__name {
display: none;
}
}
}
152 changes: 152 additions & 0 deletions src/components/Gallery/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as React from 'react';

import {ArrowLeft, ArrowRight, Xmark} from '@gravity-ui/icons';
import type {ModalProps} from '@gravity-ui/uikit';
import {Button, Icon, Modal, Text, ThemeProvider, useThemeValue} from '@gravity-ui/uikit';

import {block} from '../utils/cn';

import type {GalleryItemProps} from './GalleryItem';
import {FilesGalleryFallbackText} from './components/FallbackText';
import type {UseNavigationProps} from './hooks/useNavigation';
import {useNavigation} from './hooks/useNavigation';
import {i18n} from './i18n';
import {getInvertedTheme} from './utils/getInvertedTheme';

import './Gallery.scss';

const cnGallery = block('gallery');

const emptyItems: GalleryItemProps[] = [];

export type GalleryProps = {
fullScreen?: boolean;
modalClassName?: string;
className?: string;
children?: React.ReactElement<GalleryItemProps>[];
invertTheme?: boolean;
noItemsMessage?: string;
} & Pick<ModalProps, 'open' | 'container'> &
Required<Pick<ModalProps, 'onOpenChange'>> &
Pick<UseNavigationProps, 'initialItemIndex'>;

export const Gallery = ({
initialItemIndex,
open,
onOpenChange,
fullScreen,
container,
modalClassName,
className,
invertTheme,
children,
noItemsMessage,
}: GalleryProps) => {
const items = children ? React.Children.map(children, (child) => child.props) : emptyItems;
const theme = useThemeValue();

const {activeItemIndex, setActiveItemIndex, handleGoToNext, handleGoToPrevious} = useNavigation(
{
itemsCount: items.length,
initialItemIndex,
selectedPreviewItemClass: `.${cnGallery('preview-list-item')}_selected`,
},
);

const handleClose = React.useCallback(() => {
onOpenChange?.(false);
}, [onOpenChange]);

const activeItem = items[activeItemIndex] || items[0];
console.log(activeItem);
return (
<Modal
container={container}
className={cnGallery({mode: fullScreen ? 'full-screen' : 'default'}, modalClassName)}
open={open}
onOpenChange={onOpenChange}
disableEscapeKeyDown={fullScreen}
>
<ThemeProvider theme={invertTheme ? getInvertedTheme(theme) : theme}>
<div className={cnGallery('content', className)}>
<div className={cnGallery('header')}>
<div className={cnGallery('active-item-info')}>{activeItem?.meta}</div>
{items.length > 0 && (
<div className={cnGallery('navigation')}>
<Button size="l" view="flat" onClick={handleGoToPrevious}>
<Icon data={ArrowLeft} />
</Button>
<Text color="secondary" variant="body-1">
{activeItemIndex + 1}/{items.length}
</Text>
<Button size="l" view="flat" onClick={handleGoToNext}>
<Icon data={ArrowRight} />
</Button>
</div>
)}
<div className={cnGallery('actions')}>
{activeItem?.actions}
<Button
size="l"
view="flat"
aria-label={i18n('close')}
onClick={handleClose}
>
<Icon data={Xmark} />
</Button>
</div>
</div>
<div key={activeItemIndex} className={cnGallery('body')}>
{!items.length && (
<FilesGalleryFallbackText>
{noItemsMessage ?? i18n('no-items')}
</FilesGalleryFallbackText>
)}
{activeItem?.view}
{activeItem && !activeItem.interactive && (
<React.Fragment>
<button
onClick={handleGoToPrevious}
type="button"
className={cnGallery('body-navigation-button', {
direction: 'left',
})}
/>
<button
onClick={handleGoToNext}
type="button"
className={cnGallery('body-navigation-button', {
direction: 'right',
})}
/>
</React.Fragment>
)}
</div>
{!fullScreen && (
<div className={cnGallery('footer')}>
<div className={cnGallery('preview-list')}>
{items.map((item, index) => {
const handleClick = () => {
setActiveItemIndex(index);
};

const selected = activeItemIndex === index;

return (
<button
key={index}
onClick={handleClick}
className={cnGallery('preview-list-item', {selected})}
>
{item.thumbnail}
</button>
);
})}
</div>
</div>
)}
</div>
</ThemeProvider>
</Modal>
);
};
13 changes: 13 additions & 0 deletions src/components/Gallery/GalleryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';

export type GalleryItemProps = {
view: React.ReactNode;
thumbnail: React.ReactNode;
meta?: React.ReactNode;
actions?: React.ReactNode[];
interactive?: boolean;
};

export const GalleryItem = (_props: GalleryItemProps) => {
return null;
};
Loading

0 comments on commit 78cb8b3

Please sign in to comment.