From e0f35c188401819dc505cf1d77e5c54468e9cce7 Mon Sep 17 00:00:00 2001 From: Giovanni Schroevers Date: Fri, 2 Jun 2023 13:19:45 +0200 Subject: [PATCH 1/6] feat(GCOM-1052): add GridGallery component --- .../ProductPageGallery/GridGallary.tsx | 115 ++++++++++++++++++ .../ProductPageGallery/ProductPageGallery.tsx | 7 ++ 2 files changed, 122 insertions(+) create mode 100644 packages/magento-product/components/ProductPageGallery/GridGallary.tsx diff --git a/packages/magento-product/components/ProductPageGallery/GridGallary.tsx b/packages/magento-product/components/ProductPageGallery/GridGallary.tsx new file mode 100644 index 0000000000..9b2b508860 --- /dev/null +++ b/packages/magento-product/components/ProductPageGallery/GridGallary.tsx @@ -0,0 +1,115 @@ +import { MotionImageAspectProps } from '@graphcommerce/framer-scroller' +import { Image } from '@graphcommerce/image' +import { Row, extendableComponent, responsiveVal } from '@graphcommerce/next-ui' +import { Box, Container, SxProps, Theme, useTheme } from '@mui/material' + +export type SidebarGalleryProps = { + sidebar: React.ReactNode + images: MotionImageAspectProps[] + sx?: SxProps +} + +const name = 'SidebarGallery' as const +const parts = [ + 'row', + 'root', + 'scrollerContainer', + 'scroller', + 'sidebarWrapper', + 'sidebar', + 'bottomCenter', + 'sliderButtons', + 'toggleIcon', + 'topRight', + 'centerLeft', + 'centerRight', + 'dots', +] as const + +export function GridGallery(props: SidebarGalleryProps) { + const { sidebar, images, sx } = props + + const { classes } = extendableComponent(name, parts) + const theme = useTheme() + + return ( + + 3 && { + '& picture:nth-last-child(-n+4)': { + width: 'calc(100% / 3)', + }, + }), + }} + > + {images.map((image, idx) => ( + {image.alt + ))} + + + + {sidebar} + + + + ) +} diff --git a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx index d36aca7734..0b0fec25f9 100644 --- a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx @@ -4,6 +4,7 @@ import { SidebarGalleryProps, TypeRenderer, } from '@graphcommerce/next-ui' +import { GridGallery } from './GridGallary' import { ProductPageGalleryFragment } from './ProductPageGallery.gql' export type ProductPageGalleryRenderers = TypeRenderer< @@ -32,6 +33,12 @@ export function ProductPageGallery(props: ProductPageGalleryProps) { } }) ?? [] + const gridGalleryEnabled = true + + if (gridGalleryEnabled) { + return + } + return ( Date: Mon, 18 Dec 2023 09:31:39 +0100 Subject: [PATCH 2/6] [GCOM-1052]: Renamed GridGallery --- .../ProductPageGallery/{GridGallary.tsx => GridGallery.tsx} | 2 +- .../components/ProductPageGallery/ProductPageGallery.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/magento-product/components/ProductPageGallery/{GridGallary.tsx => GridGallery.tsx} (97%) diff --git a/packages/magento-product/components/ProductPageGallery/GridGallary.tsx b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx similarity index 97% rename from packages/magento-product/components/ProductPageGallery/GridGallary.tsx rename to packages/magento-product/components/ProductPageGallery/GridGallery.tsx index 9b2b508860..9032d843e5 100644 --- a/packages/magento-product/components/ProductPageGallery/GridGallary.tsx +++ b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx @@ -1,7 +1,7 @@ import { MotionImageAspectProps } from '@graphcommerce/framer-scroller' import { Image } from '@graphcommerce/image' import { Row, extendableComponent, responsiveVal } from '@graphcommerce/next-ui' -import { Box, Container, SxProps, Theme, useTheme } from '@mui/material' +import { Box, SxProps, Theme, useTheme } from '@mui/material' export type SidebarGalleryProps = { sidebar: React.ReactNode diff --git a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx index 0b0fec25f9..7ba562c239 100644 --- a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx @@ -4,7 +4,7 @@ import { SidebarGalleryProps, TypeRenderer, } from '@graphcommerce/next-ui' -import { GridGallery } from './GridGallary' +import { GridGallery } from './GridGallery' import { ProductPageGalleryFragment } from './ProductPageGallery.gql' export type ProductPageGalleryRenderers = TypeRenderer< From 54b3f57825fcc688a507df90b614f533141a0509 Mon Sep 17 00:00:00 2001 From: Mike Keehnen Date: Thu, 21 Dec 2023 12:45:20 +0100 Subject: [PATCH 3/6] [GCOM-1052]: Added GridGallery base component. No zoom yet --- docs/framework/config.md | 2 +- .../ProductPageGallery/GridGallery.tsx | 76 ++++++++++-------- .../ProductPageGallery/ProductPageGallery.tsx | 14 +++- .../magento-product/hooks/useStickyEffect.ts | 50 ++++++++++++ packages/next-ui/Config.graphqls | 1 + .../next-ui/FramerScroller/SidebarGallery.tsx | 72 +++-------------- packages/next-ui/hooks/useGalleryZoom.ts | 80 +++++++++++++++++++ .../next-config/dist/generated/config.js | 1 + .../next-config/src/generated/config.ts | 3 +- 9 files changed, 202 insertions(+), 97 deletions(-) create mode 100644 packages/magento-product/hooks/useStickyEffect.ts create mode 100644 packages/next-ui/hooks/useGalleryZoom.ts diff --git a/docs/framework/config.md b/docs/framework/config.md index 8979e76fc0..55562bcbdd 100644 --- a/docs/framework/config.md +++ b/docs/framework/config.md @@ -425,6 +425,6 @@ Number of recently viewed products to be stored in localStorage SidebarGalleryConfig will contain all configuration values for the Sidebar Gallery component. -#### paginationVariant: 'DOTS' | 'THUMBNAILS_BOTTOM' +#### paginationVariant: 'DOTS' | 'GRID' | 'THUMBNAILS_BOTTOM' Variant used for the pagination \ No newline at end of file diff --git a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx index 9032d843e5..ed60a96656 100644 --- a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx @@ -1,13 +1,12 @@ -import { MotionImageAspectProps } from '@graphcommerce/framer-scroller' import { Image } from '@graphcommerce/image' -import { Row, extendableComponent, responsiveVal } from '@graphcommerce/next-ui' -import { Box, SxProps, Theme, useTheme } from '@mui/material' - -export type SidebarGalleryProps = { - sidebar: React.ReactNode - images: MotionImageAspectProps[] - sx?: SxProps -} +import { + Row, + SidebarGalleryProps, + extendableComponent, + responsiveVal, +} from '@graphcommerce/next-ui' +import { Box, useTheme } from '@mui/material' +import { useStickyEffect } from '../../hooks/useStickyEffect' const name = 'SidebarGallery' as const const parts = [ @@ -26,12 +25,22 @@ const parts = [ 'dots', ] as const +type OwnerState = { zoomed: boolean; disableZoom: boolean } + +const { withState, selectors } = extendableComponent( + name, + parts, +) + export function GridGallery(props: SidebarGalleryProps) { const { sidebar, images, sx } = props - const { classes } = extendableComponent(name, parts) const theme = useTheme() + const classes = withState({ zoomed: false, disableZoom: false }) + + const [marginRef, sidebarRef, wrapperRef] = useStickyEffect() + return ( *:last-of-type:nth-of-type(odd)': { + gridColumn: '1 / -1', }, - - ...(images.length % 2 !== 0 && - images.length > 3 && { - '& picture:nth-last-child(-n+4)': { - width: 'calc(100% / 3)', - }, - }), }} > {images.map((image, idx) => ( @@ -75,11 +74,20 @@ export function GridGallery(props: SidebarGalleryProps) { height={image.height} loading={idx === 0 ? 'eager' : 'lazy'} alt={image.alt || `Product Image ${idx}` || ''} - sx={{ width: '100%', height: 'auto', display: 'block' }} + sx={[ + { + width: '100%', + display: 'block', + aspectRatio: 1, + filter: 'contrast(0.95)', + objectFit: 'cover', + }, + ]} /> ))} + {sidebar} + {sidebar} ) } + +GridGallery.selectors = selectors diff --git a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx index 7ba562c239..64bf5dd0f9 100644 --- a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx @@ -4,6 +4,7 @@ import { SidebarGalleryProps, TypeRenderer, } from '@graphcommerce/next-ui' +import { Theme, useMediaQuery } from '@mui/material' import { GridGallery } from './GridGallery' import { ProductPageGalleryFragment } from './ProductPageGallery.gql' @@ -19,6 +20,7 @@ export type ProductPageGalleryProps = Omit((theme) => theme.breakpoints.down('sm')) const images = media_gallery @@ -33,10 +35,18 @@ export function ProductPageGallery(props: ProductPageGalleryProps) { } }) ?? [] - const gridGalleryEnabled = true + const gridGalleryEnabled = + import.meta.graphCommerce.sidebarGallery?.paginationVariant === 'GRID' && !isMobile if (gridGalleryEnabled) { - return + return ( + + ) } return ( diff --git a/packages/magento-product/hooks/useStickyEffect.ts b/packages/magento-product/hooks/useStickyEffect.ts new file mode 100644 index 0000000000..ac53ebf4fc --- /dev/null +++ b/packages/magento-product/hooks/useStickyEffect.ts @@ -0,0 +1,50 @@ +import { useEffect, useRef } from 'react' + +export function useStickyEffect() { + const marginRef = useRef(null) + const sidebarRef = useRef(null) + const wrapperRef = useRef(null) + + const lastScrollTop = useRef(typeof document === 'undefined' ? 0 : document.body.scrollTop) + + const handleScroll = () => { + const marginElement = marginRef.current + const sidebarElement = sidebarRef.current + const wrapperElement = wrapperRef.current + + if (!(marginElement && sidebarElement && wrapperElement)) return + + const { height: sidebarHeight } = sidebarElement.getBoundingClientRect() + const { scrollTop } = document.documentElement + const scrollDirection = scrollTop > lastScrollTop.current ? 1 : 0 + + if (lastScrollTop.current === scrollTop || sidebarHeight <= window.innerHeight) { + sidebarElement.style.top = '0px' + sidebarElement.style.bottom = '' + marginElement.style.marginTop = '' + return + } + + const wrapperBounds = wrapperElement.getBoundingClientRect() + const sidebarBounds = sidebarElement.getBoundingClientRect() + + marginElement.style.marginTop = `${sidebarBounds.top - wrapperBounds.top}px` + + if (scrollDirection === 1) { + sidebarElement.style.bottom = '' + sidebarElement.style.top = `-${sidebarHeight - window.innerHeight}px` + } else { + sidebarElement.style.top = '' + sidebarElement.style.bottom = `-${sidebarHeight - window.innerHeight}px` + } + + lastScrollTop.current = scrollTop + } + + useEffect(() => { + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + return [marginRef, sidebarRef, wrapperRef] +} diff --git a/packages/next-ui/Config.graphqls b/packages/next-ui/Config.graphqls index 2bef3570cd..2db26f21a0 100644 --- a/packages/next-ui/Config.graphqls +++ b/packages/next-ui/Config.graphqls @@ -11,6 +11,7 @@ Enumeration of all possible positions for the sidebar gallery thumbnails. enum SidebarGalleryPaginationVariant { DOTS THUMBNAILS_BOTTOM + GRID } """ diff --git a/packages/next-ui/FramerScroller/SidebarGallery.tsx b/packages/next-ui/FramerScroller/SidebarGallery.tsx index 590b302a92..9828ec4769 100644 --- a/packages/next-ui/FramerScroller/SidebarGallery.tsx +++ b/packages/next-ui/FramerScroller/SidebarGallery.tsx @@ -1,4 +1,3 @@ -import { usePrevPageRouter } from '@graphcommerce/framer-next-pages/hooks/usePrevPageRouter' import { MotionImageAspect, MotionImageAspectProps, @@ -6,7 +5,6 @@ import { ScrollerDots, ScrollerButton, ScrollerProvider, - unstable_usePreventScroll as usePreventScroll, ScrollerButtonProps, ScrollerThumbnails, } from '@graphcommerce/framer-scroller' @@ -20,13 +18,13 @@ import { Theme, Unstable_TrapFocus as TrapFocus, } from '@mui/material' -import { m, useDomEvent, useMotionValue } from 'framer-motion' -import { useRouter } from 'next/router' -import React, { useEffect, useRef } from 'react' +import { m } from 'framer-motion' +import React from 'react' import { IconSvg } from '../IconSvg' import { Row } from '../Row/Row' import { extendableComponent } from '../Styles' import { responsiveVal } from '../Styles/responsiveVal' +import { useGalleryZoom } from '../hooks/useGalleryZoom' import { iconChevronLeft, iconChevronRight, iconFullscreen, iconFullscreenExit } from '../icons' const MotionBox = styled(m.div)({}) @@ -72,62 +70,16 @@ export function SidebarGallery(props: SidebarGalleryProps) { showButtons, disableZoom = false, } = props - - const router = useRouter() - const prevRoute = usePrevPageRouter() - // const classes = useMergedClasses(useStyles({ clientHeight, aspectRatio }).classes, props.classes) - - const route = `#${routeHash}` - // We're using the URL to manage the state of the gallery. - const zoomed = router.asPath.endsWith(route) - usePreventScroll(zoomed) - - // cleanup if someone enters the page with #gallery - useEffect(() => { - if (!prevRoute?.pathname && zoomed) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - router.replace(router.asPath.replace(route, '')) - } - }, [prevRoute?.pathname, route, router, zoomed]) - - const toggle = () => { - if (disableZoom) { - return - } - if (!zoomed) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - router.push(route, undefined, { shallow: true }) - window.scrollTo({ top: 0, behavior: 'smooth' }) - } else { - router.back() - } - } - - const classes = withState({ zoomed, disableZoom }) const theme = useTheme() - const windowRef = useRef(typeof window !== 'undefined' ? window : null) + const { maxHeight, onMouseDownScroller, onMouseUpScroller, ratio, zoomed, toggle } = + useGalleryZoom({ + disableZoom, + height, + routeHash, + width, + }) - const handleEscapeKey = (e: KeyboardEvent | Event) => { - if (zoomed && (e as KeyboardEvent)?.key === 'Escape') toggle() - } - - const dragStart = useMotionValue(0) - const onMouseDownScroller: React.MouseEventHandler = (e) => { - if (dragStart.get() === e.clientX) return - dragStart.set(e.clientX) - } - const onMouseUpScroller: React.MouseEventHandler = (e) => { - const currentDragLoc = e.clientX - if (Math.abs(currentDragLoc - dragStart.get()) < 8) toggle() - } - - useDomEvent(windowRef, 'keyup', handleEscapeKey, { passive: true }) - - const headerHeight = `${theme.appShell.headerHeightSm} - ${theme.spacings.sm} * 2` - const galleryMargin = theme.spacings.lg - - const maxHeight = `calc(100vh - ${headerHeight} - ${galleryMargin})` - const ratio = `calc(${height} / ${width} * 100%)` + const classes = withState({ zoomed, disableZoom }) const hasImages = images.length > 0 @@ -258,7 +210,7 @@ export function SidebarGallery(props: SidebarGalleryProps) { size='small' className={classes.toggleIcon} disabled={!hasImages} - onMouseUp={toggle} + onMouseUp={() => toggle()} aria-label='Toggle Fullscreen' sx={{ boxShadow: 6 }} > diff --git a/packages/next-ui/hooks/useGalleryZoom.ts b/packages/next-ui/hooks/useGalleryZoom.ts new file mode 100644 index 0000000000..a69dd0c474 --- /dev/null +++ b/packages/next-ui/hooks/useGalleryZoom.ts @@ -0,0 +1,80 @@ +import { usePrevPageRouter } from '@graphcommerce/framer-next-pages' +import { unstable_usePreventScroll as usePreventScroll } from '@graphcommerce/framer-scroller' +import { useTheme } from '@mui/material' +import { useDomEvent, useMotionValue } from 'framer-motion' +import { useRouter } from 'next/router' +import { useEffect, useRef } from 'react' + +type UseGalleryZoomProps = { + disableZoom?: boolean + routeHash: string + width: number + height: number +} + +export function useGalleryZoom(props: UseGalleryZoomProps) { + const { disableZoom, routeHash, width, height } = props + const router = useRouter() + const prevRoute = usePrevPageRouter() + // const classes = useMergedClasses(useStyles({ clientHeight, aspectRatio }).classes, props.classes) + + const route = `#${routeHash}` + // We're using the URL to manage the state of the gallery. + const zoomed = router.asPath.endsWith(route) + usePreventScroll(zoomed) + + // cleanup if someone enters the page with #gallery + useEffect(() => { + if (!prevRoute?.pathname && zoomed) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + router.replace(router.asPath.replace(route, '')) + } + }, [prevRoute?.pathname, route, router, zoomed]) + + const toggle = () => { + if (disableZoom) { + return + } + if (!zoomed) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + router.push(route, undefined, { shallow: true }) + window.scrollTo({ top: 0, behavior: 'smooth' }) + } else { + router.back() + } + } + + const theme = useTheme() + const windowRef = useRef(typeof window !== 'undefined' ? window : null) + + const handleEscapeKey = (e: KeyboardEvent | Event) => { + if (zoomed && (e as KeyboardEvent)?.key === 'Escape') toggle() + } + + const dragStart = useMotionValue(0) + const onMouseDownScroller: React.MouseEventHandler = (e) => { + if (dragStart.get() === e.clientX) return + dragStart.set(e.clientX) + } + const onMouseUpScroller: React.MouseEventHandler = (e) => { + const currentDragLoc = e.clientX + if (Math.abs(currentDragLoc - dragStart.get()) < 8) toggle() + } + + useDomEvent(windowRef, 'keyup', handleEscapeKey, { passive: true }) + + const headerHeight = `${theme.appShell.headerHeightSm} - ${theme.spacings.sm} * 2` + const galleryMargin = theme.spacings.lg + + const maxHeight = `calc(100vh - ${headerHeight} - ${galleryMargin})` + const ratio = `calc(${height} / ${width} * 100%)` + + return { + maxHeight, + ratio, + onMouseDownScroller, + onMouseUpScroller, + zoomed, + toggle, + } +} diff --git a/packagesDev/next-config/dist/generated/config.js b/packagesDev/next-config/dist/generated/config.js index 05666c3ba8..e97ea2be96 100644 --- a/packagesDev/next-config/dist/generated/config.js +++ b/packagesDev/next-config/dist/generated/config.js @@ -56,6 +56,7 @@ const ProductFiltersLayoutSchema = _zod.z.enum([ ]); const SidebarGalleryPaginationVariantSchema = _zod.z.enum([ "DOTS", + "GRID", "THUMBNAILS_BOTTOM" ]); function GraphCommerceConfigSchema() { diff --git a/packagesDev/next-config/src/generated/config.ts b/packagesDev/next-config/src/generated/config.ts index bdc644ee0e..76c18182f3 100644 --- a/packagesDev/next-config/src/generated/config.ts +++ b/packagesDev/next-config/src/generated/config.ts @@ -413,6 +413,7 @@ export type SidebarGalleryConfig = { /** Enumeration of all possible positions for the sidebar gallery thumbnails. */ export type SidebarGalleryPaginationVariant = | 'DOTS' + | 'GRID' | 'THUMBNAILS_BOTTOM'; @@ -430,7 +431,7 @@ export const CompareVariantSchema = z.enum(['CHECKBOX', 'ICON']); export const ProductFiltersLayoutSchema = z.enum(['DEFAULT', 'SIDEBAR']); -export const SidebarGalleryPaginationVariantSchema = z.enum(['DOTS', 'THUMBNAILS_BOTTOM']); +export const SidebarGalleryPaginationVariantSchema = z.enum(['DOTS', 'GRID', 'THUMBNAILS_BOTTOM']); export function GraphCommerceConfigSchema(): z.ZodObject> { return z.object({ From c3fd48c77e2ec169ad4b4eb6e7f16d0e90b48a54 Mon Sep 17 00:00:00 2001 From: Mike Keehnen Date: Thu, 21 Dec 2023 12:57:01 +0100 Subject: [PATCH 4/6] [GCOM-1052]: remove duplicate sidebar --- .../components/ProductPageGallery/GridGallery.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx index ed60a96656..42b59ac98e 100644 --- a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx @@ -115,7 +115,6 @@ export function GridGallery(props: SidebarGalleryProps) { }} > {sidebar} - {sidebar} From 9784ab1aeb9c912c1ed73cf36bcc22b8b337143f Mon Sep 17 00:00:00 2001 From: Mike Keehnen Date: Thu, 21 Dec 2023 15:38:17 +0100 Subject: [PATCH 5/6] [GCOM-1052]: Load first two images eager, the rest lazy --- .../components/ProductPageGallery/GridGallery.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx index 42b59ac98e..ea6992a99f 100644 --- a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx @@ -72,7 +72,7 @@ export function GridGallery(props: SidebarGalleryProps) { src={image.src} width={image.width} height={image.height} - loading={idx === 0 ? 'eager' : 'lazy'} + loading={idx < 2 ? 'eager' : 'lazy'} alt={image.alt || `Product Image ${idx}` || ''} sx={[ { From b9c9c53c0a19faa4e275c13d418ab7d7b4974fb7 Mon Sep 17 00:00:00 2001 From: Mike Keehnen Date: Wed, 17 Jan 2024 16:47:29 +0100 Subject: [PATCH 6/6] [GCOM-1052]: Progress on zoom animation --- .vscode/settings.json | 6 +- .../magento-graphcms/pages/blog/[url].tsx | 3 +- .../components/ScrollerProvider.tsx | 2 + .../ContentTypes/Slider/AutoScroll.tsx | 2 +- .../ProductPageGallery/GridGallery.tsx | 124 ----- .../ProductPageGallery/ProductPageGallery.tsx | 17 - .../next-ui/FramerScroller/GalleryZoom.tsx | 12 + .../next-ui/FramerScroller/SidebarGallery.tsx | 467 +++++++++--------- .../FramerScroller/hooks/useStickyEffect.ts | 51 ++ packages/next-ui/hooks/useGalleryZoom.ts | 31 +- packages/next-ui/package.json | 1 + .../plugins/SidebarGalleryGridPlugin.tsx | 68 +++ yarn.lock | 1 + 13 files changed, 396 insertions(+), 389 deletions(-) delete mode 100644 packages/magento-product/components/ProductPageGallery/GridGallery.tsx create mode 100644 packages/next-ui/FramerScroller/GalleryZoom.tsx create mode 100644 packages/next-ui/FramerScroller/hooks/useStickyEffect.ts create mode 100644 packages/next-ui/plugins/SidebarGalleryGridPlugin.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index c0edc2635c..7fa3b31471 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,12 @@ "editor.formatOnSave": true, "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.codeActionsOnSave": { "source.fixAll.eslint": false }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "never" + }, "editor.snippetSuggestions": "none", "emmet.showExpandedAbbreviation": "never", - "editor.wordBasedSuggestions": false, + "editor.wordBasedSuggestions": "off", "javascript.suggest.names": false, "typescript.tsdk": "node_modules/typescript/lib", "search.exclude": { diff --git a/examples/magento-graphcms/pages/blog/[url].tsx b/examples/magento-graphcms/pages/blog/[url].tsx index 2b0437f257..07ccc7fc05 100644 --- a/examples/magento-graphcms/pages/blog/[url].tsx +++ b/examples/magento-graphcms/pages/blog/[url].tsx @@ -1,6 +1,6 @@ import { PageOptions } from '@graphcommerce/framer-next-pages' import { hygraphPageContent, HygraphPagesQuery } from '@graphcommerce/graphcms-ui' -import { StoreConfigDocument } from '@graphcommerce/magento-store' +import { redirectOrNotFound, StoreConfigDocument } from '@graphcommerce/magento-store' import { PageMeta, BlogTitle, @@ -94,6 +94,7 @@ export const getStaticProps: GetPageStaticProps = async ({ locale, params }) => query: BlogListDocument, variables: { currentUrl: [`blog/${urlKey}`], first: limit }, }) + if (!(await page).data.pages?.[0]) return { notFound: true } return { diff --git a/packages/framer-scroller/components/ScrollerProvider.tsx b/packages/framer-scroller/components/ScrollerProvider.tsx index d6676309d6..04c856b089 100644 --- a/packages/framer-scroller/components/ScrollerProvider.tsx +++ b/packages/framer-scroller/components/ScrollerProvider.tsx @@ -166,6 +166,8 @@ export function ScrollerProvider(props: ScrollerProviderProps) { const itemsArr: unknown[] = items.get().slice() itemsArr.length = count + console.log('Registering children', itemsArr) + items.set( itemsArr.fill(undefined).map((_, i) => ({ visibility: motionValue(i === 0 ? 1 : 0), diff --git a/packages/magento-pagebuilder/ContentTypes/Slider/AutoScroll.tsx b/packages/magento-pagebuilder/ContentTypes/Slider/AutoScroll.tsx index e8a289dfab..622fbafbc2 100644 --- a/packages/magento-pagebuilder/ContentTypes/Slider/AutoScroll.tsx +++ b/packages/magento-pagebuilder/ContentTypes/Slider/AutoScroll.tsx @@ -7,7 +7,7 @@ type AutoScrollProps = { } export function AutoScroll(props: AutoScrollProps) { - const { getScrollSnapPositions, getSnapPosition, scrollerRef, scroll } = useScrollerContext() + const { getScrollSnapPositions, getSnapPosition, scroll } = useScrollerContext() const scrollTo = useScrollTo() const { pause = false, timePerSlide = 7500 } = props const { xProgress } = scroll diff --git a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx b/packages/magento-product/components/ProductPageGallery/GridGallery.tsx deleted file mode 100644 index ea6992a99f..0000000000 --- a/packages/magento-product/components/ProductPageGallery/GridGallery.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Image } from '@graphcommerce/image' -import { - Row, - SidebarGalleryProps, - extendableComponent, - responsiveVal, -} from '@graphcommerce/next-ui' -import { Box, useTheme } from '@mui/material' -import { useStickyEffect } from '../../hooks/useStickyEffect' - -const name = 'SidebarGallery' as const -const parts = [ - 'row', - 'root', - 'scrollerContainer', - 'scroller', - 'sidebarWrapper', - 'sidebar', - 'bottomCenter', - 'sliderButtons', - 'toggleIcon', - 'topRight', - 'centerLeft', - 'centerRight', - 'dots', -] as const - -type OwnerState = { zoomed: boolean; disableZoom: boolean } - -const { withState, selectors } = extendableComponent( - name, - parts, -) - -export function GridGallery(props: SidebarGalleryProps) { - const { sidebar, images, sx } = props - - const theme = useTheme() - - const classes = withState({ zoomed: false, disableZoom: false }) - - const [marginRef, sidebarRef, wrapperRef] = useStickyEffect() - - return ( - - *:last-of-type:nth-of-type(odd)': { - gridColumn: '1 / -1', - }, - }} - > - {images.map((image, idx) => ( - {image.alt - ))} - - - - - {sidebar} - - - - ) -} - -GridGallery.selectors = selectors diff --git a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx index 64bf5dd0f9..d36aca7734 100644 --- a/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx +++ b/packages/magento-product/components/ProductPageGallery/ProductPageGallery.tsx @@ -4,8 +4,6 @@ import { SidebarGalleryProps, TypeRenderer, } from '@graphcommerce/next-ui' -import { Theme, useMediaQuery } from '@mui/material' -import { GridGallery } from './GridGallery' import { ProductPageGalleryFragment } from './ProductPageGallery.gql' export type ProductPageGalleryRenderers = TypeRenderer< @@ -20,7 +18,6 @@ export type ProductPageGalleryProps = Omit((theme) => theme.breakpoints.down('sm')) const images = media_gallery @@ -35,20 +32,6 @@ export function ProductPageGallery(props: ProductPageGalleryProps) { } }) ?? [] - const gridGalleryEnabled = - import.meta.graphCommerce.sidebarGallery?.paginationVariant === 'GRID' && !isMobile - - if (gridGalleryEnabled) { - return ( - - ) - } - return ( ) => React.ReactNode +} + +export function GalleryZoom(props: GalleryZoomProps) { + const { children, ...rest } = props + const galleryZoom = useGalleryZoom(rest) + return <>{children?.(galleryZoom)} +} diff --git a/packages/next-ui/FramerScroller/SidebarGallery.tsx b/packages/next-ui/FramerScroller/SidebarGallery.tsx index 9828ec4769..c0363c8b4d 100644 --- a/packages/next-ui/FramerScroller/SidebarGallery.tsx +++ b/packages/next-ui/FramerScroller/SidebarGallery.tsx @@ -19,13 +19,15 @@ import { Unstable_TrapFocus as TrapFocus, } from '@mui/material' import { m } from 'framer-motion' +import { useRouter } from 'next/router' import React from 'react' import { IconSvg } from '../IconSvg' import { Row } from '../Row/Row' import { extendableComponent } from '../Styles' import { responsiveVal } from '../Styles/responsiveVal' -import { useGalleryZoom } from '../hooks/useGalleryZoom' import { iconChevronLeft, iconChevronRight, iconFullscreen, iconFullscreenExit } from '../icons' +import { GalleryZoom } from './GalleryZoom' +import { useStickyEffect } from './hooks/useStickyEffect' const MotionBox = styled(m.div)({}) @@ -71,13 +73,10 @@ export function SidebarGallery(props: SidebarGalleryProps) { disableZoom = false, } = props const theme = useTheme() - const { maxHeight, onMouseDownScroller, onMouseUpScroller, ratio, zoomed, toggle } = - useGalleryZoom({ - disableZoom, - height, - routeHash, - width, - }) + const router = useRouter() + const route = `#${routeHash}` + const zoomed = router.asPath.split('@')[0].endsWith(route) + const [marginRef, sidebarRef, wrapperRef] = useStickyEffect() const classes = withState({ zoomed, disableZoom }) @@ -85,257 +84,257 @@ export function SidebarGallery(props: SidebarGalleryProps) { return ( - - - + + {({ maxHeight, onMouseDownScroller, onMouseUpScroller, ratio, toggle }) => ( + { - if (!zoomed) document.body.style.overflow = '' - }} > - - {images.map((image, idx) => ( - + { + if (!zoomed) document.body.style.overflow = '' + }} + > + + {images.map((image, idx) => ( + toggle(idx)} + src={image.src} + width={image.width} + height={image.height} + loading={idx === 0 ? 'eager' : 'lazy'} + sx={{ display: 'block', objectFit: 'contain' }} + sizes={{ + 0: '100vw', + [theme.breakpoints.values.md]: zoomed ? '100vw' : '60vw', + }} + alt={image.alt || `Product Image ${idx}` || undefined} + dontReportWronglySizedImages + /> + ))} + + + {!disableZoom && ( + toggle()} + aria-label='Toggle Fullscreen' + sx={{ boxShadow: 6 }} + > + {!zoomed ? ( + + ) : ( + + )} + + )} + + + + + + + - ))} - - - {!disableZoom && ( - toggle()} - aria-label='Toggle Fullscreen' - sx={{ boxShadow: 6 }} > - {!zoomed ? ( - + + + + + + *': { + pointerEvents: 'all', + }, + }} + > + {import.meta.graphCommerce.sidebarGallery?.paginationVariant === + 'THUMBNAILS_BOTTOM' ? ( + ) : ( - + )} - - )} - + + + - - - - - - + - - - - - *': { - pointerEvents: 'all', - }, - }} - > - {import.meta.graphCommerce.sidebarGallery?.paginationVariant === - 'THUMBNAILS_BOTTOM' ? ( - - ) : ( - - )} + {sidebar} + - - - - {sidebar} - - - - + + )} + ) } diff --git a/packages/next-ui/FramerScroller/hooks/useStickyEffect.ts b/packages/next-ui/FramerScroller/hooks/useStickyEffect.ts new file mode 100644 index 0000000000..e8b8a5fe59 --- /dev/null +++ b/packages/next-ui/FramerScroller/hooks/useStickyEffect.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef } from 'react' + +export function useStickyEffect() { + const marginRef = useRef(null) + const sidebarRef = useRef(null) + const wrapperRef = useRef(null) + + const lastScrollTop = useRef(typeof document === 'undefined' ? 0 : document.body.scrollTop) + + const handleScroll = () => { + const marginElement = marginRef.current + const sidebarElement = sidebarRef.current + const wrapperElement = wrapperRef.current + + if (!(marginElement && sidebarElement && wrapperElement)) return + + const { height: sidebarHeight } = sidebarElement.getBoundingClientRect() + + const { scrollTop } = document.documentElement + const scrollDirection = scrollTop > lastScrollTop.current ? 1 : 0 + + if (lastScrollTop.current === scrollTop || sidebarHeight <= window.innerHeight) { + sidebarElement.style.top = '0px' + sidebarElement.style.bottom = '' + marginElement.style.marginTop = '' + return + } + + const wrapperBounds = wrapperElement.getBoundingClientRect() + const sidebarBounds = sidebarElement.getBoundingClientRect() + + marginElement.style.marginTop = `${sidebarBounds.top - wrapperBounds.top}px` + + if (scrollDirection === 1) { + sidebarElement.style.bottom = '' + sidebarElement.style.top = `-${sidebarHeight - window.innerHeight}px` + } else { + sidebarElement.style.top = '' + sidebarElement.style.bottom = `-${sidebarHeight - window.innerHeight}px` + } + + lastScrollTop.current = scrollTop + } + + useEffect(() => { + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + return [marginRef, sidebarRef, wrapperRef] +} diff --git a/packages/next-ui/hooks/useGalleryZoom.ts b/packages/next-ui/hooks/useGalleryZoom.ts index a69dd0c474..14c7dc36e1 100644 --- a/packages/next-ui/hooks/useGalleryZoom.ts +++ b/packages/next-ui/hooks/useGalleryZoom.ts @@ -1,11 +1,14 @@ import { usePrevPageRouter } from '@graphcommerce/framer-next-pages' -import { unstable_usePreventScroll as usePreventScroll } from '@graphcommerce/framer-scroller' +import { + unstable_usePreventScroll as usePreventScroll, + useScrollerContext, +} from '@graphcommerce/framer-scroller' import { useTheme } from '@mui/material' -import { useDomEvent, useMotionValue } from 'framer-motion' +import { useDomEvent, useMotionValue, useMotionValueEvent } from 'framer-motion' import { useRouter } from 'next/router' import { useEffect, useRef } from 'react' -type UseGalleryZoomProps = { +export type UseGalleryZoomProps = { disableZoom?: boolean routeHash: string width: number @@ -16,12 +19,15 @@ export function useGalleryZoom(props: UseGalleryZoomProps) { const { disableZoom, routeHash, width, height } = props const router = useRouter() const prevRoute = usePrevPageRouter() - // const classes = useMergedClasses(useStyles({ clientHeight, aspectRatio }).classes, props.classes) + const { getScrollSnapPositions, scrollerRef, scroll, disableSnap, enableSnap } = + useScrollerContext() const route = `#${routeHash}` // We're using the URL to manage the state of the gallery. - const zoomed = router.asPath.endsWith(route) - usePreventScroll(zoomed) + const zoomed = router.asPath.split('@')[0].endsWith(route) + // usePreventScroll(zoomed) + + useMotionValueEvent(scroll.x, 'change', (v) => console.log(v)) // cleanup if someone enters the page with #gallery useEffect(() => { @@ -31,14 +37,20 @@ export function useGalleryZoom(props: UseGalleryZoomProps) { } }, [prevRoute?.pathname, route, router, zoomed]) - const toggle = () => { + const toggle = (index?: number) => { if (disableZoom) { return } if (!zoomed) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - router.push(route, undefined, { shallow: true }) + const scroller = scrollerRef.current + if (index && scroller) { + disableSnap() + scroller.scrollLeft = 1000 + scroll.x.set(1000) + } window.scrollTo({ top: 0, behavior: 'smooth' }) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + router.push(`${route}${index ? `@${index}` : ''}`, undefined, { shallow: true }) } else { router.back() } @@ -74,7 +86,6 @@ export function useGalleryZoom(props: UseGalleryZoomProps) { ratio, onMouseDownScroller, onMouseUpScroller, - zoomed, toggle, } } diff --git a/packages/next-ui/package.json b/packages/next-ui/package.json index 4fba3aca58..cee0e2ed62 100644 --- a/packages/next-ui/package.json +++ b/packages/next-ui/package.json @@ -31,6 +31,7 @@ "@graphcommerce/framer-scroller": "^7.1.0-canary.65", "@graphcommerce/framer-utils": "^7.1.0-canary.65", "@graphcommerce/image": "^7.1.0-canary.65", + "@graphcommerce/next-config": "^7.1.0-canary.65", "@graphcommerce/prettier-config-pwa": "^7.1.0-canary.65", "@graphcommerce/typescript-config-pwa": "^7.1.0-canary.65", "@lingui/core": "^4.2.1", diff --git a/packages/next-ui/plugins/SidebarGalleryGridPlugin.tsx b/packages/next-ui/plugins/SidebarGalleryGridPlugin.tsx new file mode 100644 index 0000000000..59580696b1 --- /dev/null +++ b/packages/next-ui/plugins/SidebarGalleryGridPlugin.tsx @@ -0,0 +1,68 @@ +import { IfConfig, PluginProps } from '@graphcommerce/next-config' +import { SxProps, Theme, useTheme } from '@mui/material' +import { useRouter } from 'next/router' +import { SidebarGalleryProps } from '../FramerScroller' + +export const component = 'SidebarGallery' +export const exported = '@graphcommerce/next-ui' +export const ifConfig: IfConfig = 'sidebarGallery.paginationVariant' + +function SidebarGalleryGridPlugin(props: PluginProps) { + const { Prev, sx = {}, disableZoom, routeHash = 'gallery', ...rest } = props + const theme = useTheme() + const router = useRouter() + const newSx = Array.isArray(sx) ? sx : [sx] + const route = `#${routeHash}` + const zoomed = router.asPath.split('@')[0].endsWith(route) + + const gridSx: SxProps = { + '& .ScrollerDots-root': { display: 'none' }, + [theme.breakpoints.up('md')]: { + height: 'unset', + top: 'unset', + '& .SidebarGallery-scrollerContainer': { + paddingTop: 'unset', + height: 'unset', + }, + '& .Scroller-root': { + position: 'relative', + }, + '& div > .mdSnapDirInline': { + overflowY: 'unset', + overflowX: 'unset', + }, + '& div > .mdGridDirInline': { + gridAutoColumns: 'unset', + gridAutoRows: 'unset', + gridTemplateRows: 'unset', + gridAutoFlow: 'unset', + gridTemplateColumns: 'repeat(2, 1fr)', + '& > *:last-of-type:nth-of-type(odd)': { + gridColumn: '1 / -1', + }, + '& picture': { + aspectRatio: '1 !important', + maxHeight: 'unset', + width: 'unset', + height: 'unset', + top: 'unset', + left: 'unset', + transform: 'unset', + position: 'unset', + '&::after': { + minWidth: 'unset', + }, + '& img': { + objectFit: 'cover', + }, + }, + }, + }, + } + if (import.meta.graphCommerce.sidebarGallery?.paginationVariant === 'GRID' && !zoomed) + newSx.push(gridSx) + + return +} + +export const Plugin = SidebarGalleryGridPlugin diff --git a/yarn.lock b/yarn.lock index 072ef19310..d24a897142 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4266,6 +4266,7 @@ __metadata: "@graphcommerce/framer-scroller": ^7.1.0-canary.65 "@graphcommerce/framer-utils": ^7.1.0-canary.65 "@graphcommerce/image": ^7.1.0-canary.65 + "@graphcommerce/next-config": ^7.1.0-canary.65 "@graphcommerce/prettier-config-pwa": ^7.1.0-canary.65 "@graphcommerce/typescript-config-pwa": ^7.1.0-canary.65 "@lingui/core": ^4.2.1