Skip to content

Commit

Permalink
Merge pull request #2011 from graphcommerce-org/feature/gallery-thumb…
Browse files Browse the repository at this point in the history
…nails-with-hover

Gallery thumbnails (GCOM-1100)
  • Loading branch information
paales authored Nov 8, 2023
2 parents bd36fc0 + 3bd5b73 commit 880e37b
Show file tree
Hide file tree
Showing 15 changed files with 287 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-apes-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/framer-scroller': patch
---

Added a new Image Gallery as a plugin
14 changes: 13 additions & 1 deletion docs/framework/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ Example: '/product/'
Allow the site to be indexed by search engines.
If false, the robots.txt file will be set to disallow all.

#### `sidebarGallery: [SidebarGalleryConfig](#SidebarGalleryConfig)`

Configuration for the SidebarGallery component

#### `wishlistHideForGuests: Boolean`

Hide the wishlist functionality for guests.
Expand Down Expand Up @@ -393,4 +397,12 @@ provided that the gallery images for the selected variant differ from the curren

When a variant is selected the URL of the product will be changed in the address bar.

This only happens when the actual variant is can be accessed by the URL.
This only happens when the actual variant is can be accessed by the URL.

### SidebarGalleryConfig

SidebarGalleryConfig will contain all configuration values for the Sidebar Gallery component.

#### `paginationVariant: [SidebarGalleryPaginationVariant](#SidebarGalleryPaginationVariant)`

Variant used for the pagination
4 changes: 3 additions & 1 deletion packages/demo-magento-graphcommerce/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"react-dom": "^18.2.0"
},
"dependencies": {
"@graphcommerce/next-ui": "7.1.0-canary.32",
"@graphcommerce/framer-scroller": "7.1.0-canary.32",
"@graphcommerce/magento-product": "7.1.0-canary.32",
"@graphcommerce/magento-product-configurable": "7.1.0-canary.32"
}
}
}
99 changes: 99 additions & 0 deletions packages/framer-scroller/components/ScrollerThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useMotionValueValue } from '@graphcommerce/framer-utils'
import { Image, ImageProps } from '@graphcommerce/image'
// eslint-disable-next-line import/no-extraneous-dependencies
import { extendableComponent, responsiveVal } from '@graphcommerce/next-ui/Styles'
import { alpha, styled, useTheme } from '@mui/material'
import { m, motionValue, useTransform } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { useScrollerContext } from '../hooks/useScrollerContext'

const name = 'ScrollerThumbnail'
const parts = ['thumbnail'] as const
type OwnerProps = { active: boolean }

const { withState } = extendableComponent<OwnerProps, typeof name, typeof parts>(name, parts)

type ScrollerThumbnailProps = {
idx: number
image: Pick<ImageProps, 'src' | 'height' | 'width'>
}

const MotionBox = styled(m.div)({})

export function ScrollerThumbnail(props: ScrollerThumbnailProps) {
const { idx, image } = props
const { scrollerRef, scroll, getScrollSnapPositions, items } = useScrollerContext()
const found = useMotionValueValue(items, (v) => v.find((_, index) => index === idx))
// This ensures that the first item in the scroller is selected by default.
// The opacity property is set to 0 by default.
const item = found ?? {
visibility: idx === 0 ? motionValue(1) : motionValue(0),
opacity: motionValue(0),
}
const active = useMotionValueValue(item.visibility, (v) => v >= 0.5)
const theme = useTheme()

const ref = useRef<HTMLDivElement>(null)

const boxShadow = useTransform(
item.visibility,
[1, 0],
[
`inset 0 0 0 2px ${theme.palette.primary.main}, 0 0 0 4px ${alpha(
theme.palette.primary.main,
theme.palette.action.hoverOpacity,
)}`,
`inset 0 0 0 2px #ffffff00, 0 0 0 4px #ffffff00`,
],
)

const classes = withState({ active })

const scrollIntoView = () =>
ref.current?.scrollIntoView({ block: 'nearest', inline: 'center', behavior: 'auto' })

useEffect(() => {
if (active && ref.current) {
// This is a hack to ensure that the scroll animation is finished.
setTimeout(() => scrollIntoView(), 1)
}
}, [active])

if (!image) return null

return (
<MotionBox
ref={ref}
className={classes.thumbnail}
onClick={() => {
if (!scrollerRef.current) return
scroll.animating.set(true)
const { x } = getScrollSnapPositions()
scrollerRef.current.scrollLeft = x[idx]
scroll.x.set(x[idx])
scroll.animating.set(false)
}}
layout='position'
style={{ boxShadow }}
sx={{
padding: '2px',
mx: `calc(${theme.spacing(1)} / 2)`,
borderRadius: theme.shape.borderRadius,
}}
>
<Image
{...image}
loading='eager'
sx={{
height: responsiveVal(35, 90),
width: 'auto',
display: 'block',
pointerEvents: 'none',
objectFit: 'cover',
borderRadius: theme.shape.borderRadius - 0.7,
}}
sizes={responsiveVal(35, 90)}
/>
</MotionBox>
)
}
31 changes: 31 additions & 0 deletions packages/framer-scroller/components/ScrollerThumbnails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ImageProps } from '@graphcommerce/image'
import { ButtonProps, SxProps, Theme } from '@mui/material'
import { ScrollerThumbnail } from './ScrollerThumbnail'
import { ThumbnailContainer } from './ThumbnailContainer'

export type ThumbnailsProps = {
buttonProps?: Omit<ButtonProps, 'onClick' | 'children'>
sx?: SxProps<Theme>
images: Pick<ImageProps, 'src' | 'height' | 'width'>[]
}

const componentName = 'ScrollerThumbnails'

export function ScrollerThumbnails(props: ThumbnailsProps) {
const { images, sx = [], ...buttonProps } = props
return (
<ThumbnailContainer sx={sx}>
{images.map((item, i) => (
<ScrollerThumbnail
// eslint-disable-next-line react/no-array-index-key
key={`${i}-image`}
idx={i}
image={item}
{...buttonProps}
/>
))}
</ThumbnailContainer>
)
}

ScrollerThumbnails.displayName = componentName
43 changes: 43 additions & 0 deletions packages/framer-scroller/components/ThumbnailContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { styled, SxProps, Theme } from '@mui/material'
import { m, PanHandlers } from 'framer-motion'
import React, { useRef } from 'react'

const MotionBox = styled(m.div)({})

type ThumbnailContainerProps = {
children: React.ReactNode
sx?: SxProps<Theme>
}

export function ThumbnailContainer(props: ThumbnailContainerProps) {
const { children, sx } = props
const containerRef = useRef<HTMLDivElement>(null)
const onPan: PanHandlers['onPan'] = (_, info) => {
containerRef.current?.scrollBy({ left: -info.delta.x })
}

return (
<MotionBox
ref={containerRef}
onPan={onPan}
layout
sx={[
{
padding: '4px',
userSelect: 'none',
cursor: 'grab',
overflow: 'none',
overflowX: 'auto',
display: 'flex',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{children}
</MotionBox>
)
}
2 changes: 2 additions & 0 deletions packages/framer-scroller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export * from './components/ScrollerButton'
export * from './components/ScrollerDots'
export * from './components/ScrollerPageCounter'
export * from './components/ScrollerProvider'
export * from './components/ScrollerThumbnails'
export * from './components/ScrollerThumbnail'

export * from './hooks/useScrollerContext'
// export * from './hooks/useScroller'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,25 @@ export function ProductPageGallery(props: ProductPageGalleryProps) {
const { product, children, aspectRatio: [width, height] = [1532, 1678], ...sidebarProps } = props
const { media_gallery } = product

const images =
media_gallery
?.filter(nonNullable)
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
.map((item) => {
if (item.__typename === 'ProductImage')
return { src: item.url ?? '', alt: item.label || undefined, width, height }
return {
src: '',
alt: `{${item.__typename} not yet supported}`,
}
}) ?? []

return (
<SidebarGallery
{...sidebarProps}
sidebar={children}
aspectRatio={[width, height]}
images={
media_gallery
?.filter(nonNullable)
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
.map((item) => {
if (item.__typename === 'ProductImage')
return { src: item.url ?? '', alt: item.label || undefined, width, height }
return {
src: '',
alt: `{${item.__typename} not yet supported}`,
}
}) ?? []
}
images={images}
/>
)
}
24 changes: 24 additions & 0 deletions packages/next-ui/Config.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
extend input GraphCommerceConfig {
"""
Configuration for the SidebarGallery component
"""
sidebarGallery: SidebarGalleryConfig
}

"""
Enumeration of all possible positions for the sidebar gallery thumbnails.
"""
enum SidebarGalleryPaginationVariant {
DOTS
THUMBNAILS_BOTTOM
}

"""
SidebarGalleryConfig will contain all configuration values for the Sidebar Gallery component.
"""
input SidebarGalleryConfig {
"""
Variant used for the pagination
"""
paginationVariant: SidebarGalleryPaginationVariant
}
20 changes: 12 additions & 8 deletions packages/next-ui/FramerScroller/SidebarGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import {
MotionImageAspect,
MotionImageAspectProps,
Scroller,
ScrollerButton,
ScrollerButtonProps,
ScrollerDots,
ScrollerButton,
ScrollerProvider,
unstable_usePreventScroll as usePreventScroll,
ScrollerButtonProps,
ScrollerThumbnails,
} from '@graphcommerce/framer-scroller'
import { dvh } from '@graphcommerce/framer-utils'
import {
Expand All @@ -30,7 +31,7 @@ import { iconChevronLeft, iconChevronRight, iconFullscreen, iconFullscreenExit }

const MotionBox = styled(m.div)({})

type OwnerState = { zoomed: boolean }
type OwnerState = { zoomed: boolean; disableZoom: boolean }
const name = 'SidebarGallery' as const
const parts = [
'row',
Expand Down Expand Up @@ -68,8 +69,8 @@ export function SidebarGallery(props: SidebarGalleryProps) {
aspectRatio: [width, height] = [1, 1],
sx,
routeHash = 'gallery',
disableZoom,
showButtons,
disableZoom = false,
} = props

const router = useRouter()
Expand Down Expand Up @@ -102,7 +103,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
}
}

const classes = withState({ zoomed })
const classes = withState({ zoomed, disableZoom })
const theme = useTheme()
const windowRef = useRef(typeof window !== 'undefined' ? window : null)

Expand Down Expand Up @@ -317,7 +318,6 @@ export function SidebarGallery(props: SidebarGalleryProps) {
className={classes.bottomCenter}
sx={{
display: 'flex',
px: theme.page.horizontal,
gap: theme.spacings.xxs,
position: 'absolute',
bottom: theme.spacings.xxs,
Expand All @@ -329,11 +329,15 @@ export function SidebarGallery(props: SidebarGalleryProps) {
},
}}
>
<ScrollerDots layout='position' layoutDependency={zoomed} />
{import.meta.graphCommerce.sidebarGallery?.paginationVariant ===
'THUMBNAILS_BOTTOM' ? (
<ScrollerThumbnails images={images} />
) : (
<ScrollerDots />
)}
</Box>
</MotionBox>
</TrapFocus>

<Box
className={classes.sidebarWrapper}
sx={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function flattenKeys(value, initialPathPrefix, stringify) {
const deep = value[key];
return flattenKeys(deep, `${initialPathPrefix}.${key}`, stringify);
})
.reduce((acc, path) => ({ ...acc, ...path })),
.reduce((acc, path) => ({ ...acc, ...path }), {}),
};
}
throw Error(`Unexpected value: ${value}`);
Expand Down
Loading

0 comments on commit 880e37b

Please sign in to comment.