diff --git a/src/components/Gallery/Gallery.tsx b/src/components/Gallery/Gallery.tsx index 560954e3..dd660bb3 100644 --- a/src/components/Gallery/Gallery.tsx +++ b/src/components/Gallery/Gallery.tsx @@ -8,6 +8,7 @@ import {GalleryFallbackText} from './components/FallbackText'; import {GalleryHeader} from './components/GalleryHeader/GalleryHeader'; import {NavigationButton} from './components/NavigationButton/NavigationButton'; import {BODY_CONTENT_CLASS_NAME, cnGallery} from './constants'; +import {GalleryContextProvider} from './contexts/GalleryContext'; import {useFullScreen} from './hooks/useFullScreen'; import {useMobileGestures} from './hooks/useMobileGestures/useMobileGestures'; import type {UseNavigationProps} from './hooks/useNavigation'; @@ -46,6 +47,7 @@ export const Gallery = ({ ); const [hiddenHeader, setHiddenHeader] = React.useState(false); + const [isViewInteracting, setIsViewInteracting] = React.useState(false); React.useEffect(() => { setItemRefs(Array.from({length: itemsCount}, () => React.createRef())); @@ -58,6 +60,14 @@ export const Gallery = ({ }, ); + React.useEffect(() => { + setIsViewInteracting(false); + }, [activeItemIndex]); + + React.useEffect(() => { + if (isViewInteracting) setHiddenHeader(true); + }, [isViewInteracting]); + const {fullScreen, setFullScreen} = useFullScreen(); const handleBackClick = React.useCallback(() => { @@ -95,12 +105,13 @@ export const Gallery = ({ onSwipeLeft: handleGoToNext, onSwipeRight: handleGoToPrevious, onTap: handleTap, + disabled: isViewInteracting, }); const withNavigation = items.length > 1; const showNavigationButtons = - withNavigation && !isMobile && activeItem && !activeItem.interactive; + withNavigation && !isMobile && activeItem && !activeItem.interactive && !isViewInteracting; const showFooter = !fullScreen && !isMobile; const mode = getMode(isMobile, fullScreen); @@ -152,7 +163,12 @@ export const Gallery = ({ {emptyMessage ?? t('no-items')} )} - {activeItem?.view} + + {activeItem?.view} + {showNavigationButtons && ( diff --git a/src/components/Gallery/GalleryItem.tsx b/src/components/Gallery/GalleryItem.tsx index d1db1852..7ed9f6f2 100644 --- a/src/components/Gallery/GalleryItem.tsx +++ b/src/components/Gallery/GalleryItem.tsx @@ -1,15 +1,21 @@ -import * as React from 'react'; +import type * as React from 'react'; import {ButtonProps} from '@gravity-ui/uikit'; +import type {TProps, WithTFn} from './i18n'; + export type GalleryItemAction = { id: string; title: string; + /** @internal */ + __titleT?: WithTFn; hotkey?: string; onClick?: () => void; href?: string; icon: React.ReactNode; render?: (props: ButtonProps) => React.ReactNode; + /** @internal */ + __renderT?: (props: ButtonProps, tProps: TProps) => React.ReactNode; }; export type GalleryItemProps = { diff --git a/src/components/Gallery/README.md b/src/components/Gallery/README.md index 46d36372..55c8e287 100644 --- a/src/components/Gallery/README.md +++ b/src/components/Gallery/README.md @@ -4,6 +4,14 @@ The base component for rendering galleries of any type of data. The component is responsible for the gallery navigation (keyboard arrows, body side click and header arrow click). The children of the Gallery should be an array of [GalleryItem with the required properties](#GalleryItem) for rendering the gallery item view. +### Features + +- **Navigation**: Keyboard arrows, body side click, and header arrow click +- **Image Zoom**: Built-in zoom and pan functionality for images (desktop and mobile) +- **Swipe Gestures**: Mobile swipe navigation (automatically disabled during zoom interaction) +- **Fullscreen Mode**: Toggle fullscreen view +- **Custom Actions**: Add custom action buttons for each gallery item + ### PropTypes | Property | Type | Required | Values | Default | Description | @@ -25,6 +33,28 @@ The children of the Gallery should be an array of [GalleryItem with the required | actions | `ReactNode[]` | | | | The array of the gallery item action buttons | | interactive | `boolean` | | | | Provide true if the gallery item is interactive and the navigation by body click should not work | +### Image Zoom + +Gallery includes built-in zoom functionality for images via the [`useImageZoom`](./hooks/useImageZoom/README.md) hook: + +**Desktop:** + +- Click to toggle 1x ↔ 2x zoom +- Drag to pan when zoomed + +**Mobile:** + +- Double tap to toggle 1x ↔ 3x zoom +- Pinch to zoom (1.0 - 3.0) +- Single finger drag to pan when zoomed +- Swipe gestures automatically disabled during zoom interaction + +See [`useImageZoom` documentation](./hooks/useImageZoom/README.md) for more details. + +### Gallery Context + +Gallery provides a context for child views to communicate interaction state. See [`GalleryContext` documentation](./contexts/README.md) for details. + ### Default gallery item props We export some utility functions for getting the gallery item props: diff --git a/src/components/Gallery/__stories__/Gallery.stories.tsx b/src/components/Gallery/__stories__/Gallery.stories.tsx index bc6ae8e1..0c5e94ee 100644 --- a/src/components/Gallery/__stories__/Gallery.stories.tsx +++ b/src/components/Gallery/__stories__/Gallery.stories.tsx @@ -337,3 +337,39 @@ const SingleItemGalleryTemplate: StoryFn = () => { }; export const SingleItemGallery = SingleItemGalleryTemplate.bind({}); + +const SmallImagesTemplate: StoryFn = () => { + const [open, setOpen] = React.useState(false); + + const handleToggle = React.useCallback(() => { + setOpen(false); + }, []); + + const handleOpen = React.useCallback(() => { + setOpen(true); + }, []); + + return ( + + + + + + + + ); +}; + +export const SmallImages = SmallImagesTemplate.bind({}); diff --git a/src/components/Gallery/components/DesktopGalleryHeader/DesktopGalleryHeader.tsx b/src/components/Gallery/components/DesktopGalleryHeader/DesktopGalleryHeader.tsx index 4ca88445..7f517b0a 100644 --- a/src/components/Gallery/components/DesktopGalleryHeader/DesktopGalleryHeader.tsx +++ b/src/components/Gallery/components/DesktopGalleryHeader/DesktopGalleryHeader.tsx @@ -63,6 +63,7 @@ export const DesktopGalleryHeader = ({ )}
{actions?.map((action) => { + const title = action.__titleT ? action.__titleT({t}) : action.title; const buttonProps: ButtonProps = { type: 'button', size: 'l', @@ -70,18 +71,36 @@ export const DesktopGalleryHeader = ({ onClick: action.onClick, href: action.href, target: '__blank', - 'aria-label': action.title, + 'aria-label': title, children: action.icon, }; - return action.render ? ( - - {action.render(buttonProps)} - - ) : ( - -