Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sticky AddToCart Component on Product Page (GCOM-1194) #2036

Draft
wants to merge 28 commits into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
386d146
chore(GCOM-1194): create a ProductPageWrapper component so we can mod…
carlocarels90 Aug 22, 2023
7c6d478
chore(GCOM-1194): create a plugin StickyAddProductsToCartButton for P…
carlocarels90 Aug 22, 2023
df6b9a3
chore(GCOM-1194): create a StickyAddToCart component
carlocarels90 Aug 22, 2023
2baa32a
refactor: rename props
carlocarels90 Aug 22, 2023
6a5be77
chore(GCOM-1194): Add 'forceScrolled' option to show the ‘scrolled' s…
carlocarels90 Aug 24, 2023
2bbeb6a
chore: add url to the ProductPageGallery fragment
carlocarels90 Aug 24, 2023
2a2b5f4
chore(GCOM-1194): Pass a reference from the AddProductToCartButton to…
carlocarels90 Aug 24, 2023
56ae512
chore: make the forceScrolled optional
carlocarels90 Aug 25, 2023
4439bd8
chore(GCOM-1194): create a enableStickyAddToCart config
carlocarels90 Aug 25, 2023
0c0292d
chore: add default value for the forceScrolled option
carlocarels90 Aug 25, 2023
32161a0
chore(GCOM-1194): create ConfigurableStickyAddToCart plugin for enhan…
carlocarels90 Aug 25, 2023
3a82c31
chore(GCOM-1194): move StickyAddToCart to the AddProductsToCartForm s…
carlocarels90 Aug 25, 2023
0d2c0db
chore(GCOM-1194): add styling for the stick add to cart + remove the …
carlocarels90 Aug 25, 2023
2f22eba
refactor: remove unused files
carlocarels90 Aug 25, 2023
58011ed
refactor: move changes to a plugin
carlocarels90 Aug 25, 2023
c490771
refactor: remove unused files
carlocarels90 Aug 28, 2023
e9cf329
refactor: undo changes for the product page.
carlocarels90 Aug 28, 2023
2ff9d4e
refactor: remove forceScrolled option
carlocarels90 Aug 31, 2023
c8b86a7
chore(GCOM-1194): calculate the cartButtonRef offset to fill the swit…
carlocarels90 Aug 31, 2023
5a2f1d7
chore(GCOM-1194): add StickyAddToCartDestination when the enableStick…
carlocarels90 Aug 31, 2023
4284880
chore(GCOM-1194): add transform animation
carlocarels90 Aug 31, 2023
1fbe69f
chore(GCOM-1194): inject missing StickyAddToCart data to the UseAddPr…
carlocarels90 Aug 31, 2023
623f911
refactor: remove unused plugin
carlocarels90 Aug 31, 2023
f84716c
chore(GCOM-1194): create plugin sends the cartButtonRef within the cr…
carlocarels90 Aug 31, 2023
15a5b96
chore: hide product name on mobile, since it’s duplicated
carlocarels90 Aug 31, 2023
fd5714a
refactor: replace useLayoutEffect for useEffect + move createPortal o…
carlocarels90 Sep 1, 2023
4c8001e
chore: add changeset
carlocarels90 Sep 1, 2023
e81ffd5
refactor: remove forwardedRef and duplicated hook
carlocarels90 Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/famous-queens-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@graphcommerce/magento-product': minor
'@graphcommerce/magento-product-configurable': patch
'@graphcommerce/next-config': patch
'@graphcommerce/next-ui': patch
'@graphcommerce/docs': patch
---

When the 'enableStickyAddToCart' option is enabled, a sticky bar will appear if the default add-to-cart button goes out of the visible viewport.
6 changes: 6 additions & 0 deletions docs/framework/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ Enables some demo specific code that is probably not useful for a project:
- Adds "dominant_color" attribute swatches to the product list items.
- Creates a big list items in the product list.

#### `enableStickyAddToCart: Boolean`

Enable this option to activate the sticky add-to-cart bar on the product page.
When enabled, a sticky bar will appear if the default add-to-cart button goes out of the visible viewport.
This allows users to easily access the add-to-cart functionality even as they scroll through the page.

#### `googleAnalyticsId: String`

See https://support.google.com/analytics/answer/9539598?hl=en
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AddToCartItemSelector, StickyAddToCart } from '@graphcommerce/magento-product'
import type { IfConfig, ReactPlugin } from '@graphcommerce/next-config'
import { useConfigurableSelectedVariant } from '../../hooks'

export const component = 'StickyAddToCart'
export const exported = '@graphcommerce/magento-product/components/StickyAddToCart/StickyAddToCart'
export const ifConfig: IfConfig = 'enableStickyAddToCart'

type PluginType = ReactPlugin<typeof StickyAddToCart, AddToCartItemSelector>

const ConfigurableStickyAddToCart: PluginType = (props) => {
const { Prev, product, ...rest } = props
const variant = useConfigurableSelectedVariant({ url_key: product.url_key, index: 0 })

return <Prev product={variant ?? product} {...rest} />
}

export const Plugin = ConfigurableStickyAddToCart
7 changes: 7 additions & 0 deletions packages/magento-product/Config.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ extend input GraphCommerceConfig {
Default: 'false'
"""
crossSellsHideCartItems: Boolean = false

"""
Enable this option to activate the sticky add-to-cart bar on the product page.
When enabled, a sticky bar will appear if the default add-to-cart button goes out of the visible viewport.
This allows users to easily access the add-to-cart functionality even as they scroll through the page.
"""
enableStickyAddToCart: Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ export function AddProductsToCartForm(props: AddProductsToCartFormProps) {
value={useMemo(() => ({ ...form, redirect }), [form, redirect])}
>
<Box component='form' onSubmit={submit} noValidate sx={sx} className={name}>
{import.meta.graphCommerce.enableStickyAddToCart && (
<Box
sx={(theme) => ({
position: { xs: 'sticky', md: 'absolute' },
height: '100%',
width: '100%',
margin: 'auto',
inset: 0,
top: { xs: theme.appShell.headerHeightSm, md: 0 },
zIndex: '1',
pointerEvents: 'none',
'& > *': { pointerEvents: 'all' },
})}
id='StickyAddToCartDestination'
/>
)}
{children}
</Box>
{disableSuccessSnackbar ? null : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ fragment ProductPageGallery on ProductInterface {
label
position
disabled
url
...ProductImage
...ProductVideo
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Image } from '@graphcommerce/image'
import { LayoutHeader, responsiveVal } from '@graphcommerce/next-ui'
import { Box, Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import { AddProductsToCartButton, AddProductsToCartError } from '../AddProductsToCart'
import {
ProductPageAddToCartQuantityRow,
ProductPageAddToCartRowProps,
} from '../ProductPage/ProductPageAddToCartRow'
import { ProductPageName } from '../ProductPageName'
import { ProductPagePrice } from '../ProductPagePrice'

export type StickyAddToCartProps = Partial<ProductPageAddToCartRowProps> & {
cartButtonRef?: React.RefObject<HTMLElement>
}

export function StickyAddToCart(props: StickyAddToCartProps) {
const { product, cartButtonRef } = props

const mainImage = product?.media_gallery?.[0]
const [switchPointPosition, setSwitchPointPosition] = useState<number>()

useEffect(() => {
if (!cartButtonRef?.current) return () => {}

const handleResize = () => {
const cartButtonBottomRelativeToViewport =
cartButtonRef?.current?.getBoundingClientRect().bottom

if (cartButtonBottomRelativeToViewport) {
// Calculate the distance from the top of the page to the bottom position of cartButtonRef
// We add the current scroll position to get an absolute distance
const cartButtonAbsolutePosition = cartButtonBottomRelativeToViewport + window.scrollY

// Set the switchPoint value to the calculated absolute position
setSwitchPointPosition(cartButtonAbsolutePosition)
}
}

window.addEventListener('resize', handleResize)
handleResize()

return () => {
window.removeEventListener('resize', handleResize)
}
}, [cartButtonRef])

return (
product &&
switchPointPosition && (
<LayoutHeader
hideBackButton
switchPoint={switchPointPosition}
size='responsive'
sx={(theme) => ({
width: { md: '60vw', lg: '65vw' },
inset: 0,
margin: 'auto',
transform: {
xs: 'none',
md: `translateY(calc(${theme.appShell.appBarHeightMd} - ${theme.appShell.headerHeightSm} - 10px))`,
},

[theme.breakpoints.down('md')]: {
height: `calc(${theme.appShell.headerHeightSm} + 15px)`,
mt: 0,
},

'& .LayoutHeaderContent-content': {
display: 'flex',
inset: 0,
margin: 'auto',
px: '12px',
},

'& .LayoutHeaderContent-center > div': {
width: '100%',
},

'& .LayoutHeaderContent-bg': {
borderRadius: { xs: 0, md: theme.shape.borderRadius },
boxShadow: theme.shadows[6],
transform: 'translateY(-50px)',
transition: '0.3s ease-in-out',

'&.scrolled': {
opacity: 1,
transform: 'none',
},

[theme.breakpoints.down('md')]: {
height: `calc(${theme.appShell.headerHeightSm} + 15px)`,
},
},

'& .LayoutHeaderContent-center': {
display: 'flex',
width: '100%',
transform: 'translateY(-50px)',
transition: '0.3s ease-in-out',

'&.scrolled': {
opacity: 1,
transform: 'none',
},
},

'& .LayoutHeaderContent-right': {
display: 'none',
},
})}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: (theme) => theme.spacings.sm,
}}
>
{mainImage?.url && (
<Image
alt={mainImage.label ?? ''}
layout='fixed'
width={60}
height={60}
src={mainImage.url}
sx={{
verticalAlign: 'middle',
width: responsiveVal(46, 60),
height: responsiveVal(46, 60),
}}
/>
)}
<Typography
variant='h4'
sx={(theme) => ({
display: { xs: 'none', md: 'block' },
fontSize: { xs: theme.typography.h5.fontSize, md: theme.typography.h4.fontSize },
fontWeight: 'bold',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
})}
>
<ProductPageName product={{ name: product?.name }} />
</Typography>
<ProductPageAddToCartQuantityRow product={product} sx={{ ml: 'auto' }}>
<AddProductsToCartError>
<Typography
component='div'
variant='h3'
lineHeight='1'
sx={{
display: { lg: 'flex' },
gap: { xs: 1, lg: 2 },
typography: { xs: 'body1', md: 'h6', lg: 'h5' },
lineHeight: { xs: '1.2em', md: '1.7em' },
'& .incl-vat': {
opacity: 0.4,
},
}}
>
<ProductPagePrice product={product} />
</Typography>
</AddProductsToCartError>
</ProductPageAddToCartQuantityRow>
<AddProductsToCartButton color='primary' size='medium' product={product} sx={{}} />
</Box>
</LayoutHeader>
)
)
}
1 change: 1 addition & 0 deletions packages/magento-product/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ export * from './ProductStaticPaths/getSitemapPaths'
export * from './ProductUpsells/UpsellProducts.gql'
export * from './ProductWeight/ProductWeight'
export * from './ProductListPrice'
export * from './StickyAddToCart/StickyAddToCart'
4 changes: 4 additions & 0 deletions packages/magento-product/graphql/StickyAddToCart.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fragment StickyAddToCart on ProductInterface @inject(into: ["UseAddProductsToCartAction"]) {
...ProductPageGallery
...ProductPagePrice
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { IfConfig, ReactPlugin } from '@graphcommerce/next-config'
import { Box } from '@mui/material'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { ProductPageAddToCartRow, StickyAddToCart } from '../../components'

export const component = 'ProductPageAddToCartActionsRow'
export const exported =
'@graphcommerce/magento-product/components/ProductPage/ProductPageAddToCartRow'
export const ifConfig: IfConfig = 'enableStickyAddToCart'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plugin is self-referencing. So the plugin is targeting the current package. Shouldn't we make this a simple if statement somewhere? @carlocarels90 / @mikekeehnen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but we could also make it a separate package right @paales? This would make it a plug and play solution

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikekeehnen if we do this, we don't need the config file I guess? Because it will apply only when you add this package to your project?


type PluginType = ReactPlugin<typeof ProductPageAddToCartRow>

const StickyProductPageAddToCartRow: PluginType = (props) => {
const { Prev, children, product, ...rest } = props

const cartButtonRef = useRef<HTMLElement | null>(null)
const [target, setTarget] = useState<HTMLElement | null>(null)

const stickyAddToCart = <StickyAddToCart product={product} cartButtonRef={cartButtonRef} />

useEffect(() => {
const stickyTarget = globalThis?.document?.getElementById('StickyAddToCartDestination')
if (stickyTarget) {
setTarget(stickyTarget)
}
}, [])

return (
<>
{target && createPortal(stickyAddToCart, target)}
<Box ref={cartButtonRef}>
<Prev product={product} {...rest}>
{children}
</Prev>
</Box>
</>
)
}

export const Plugin = StickyProductPageAddToCartRow
9 changes: 8 additions & 1 deletion packages/next-ui/Layout/components/LayoutHeaderContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
const scroll = useScrollY()
const scrolled = useMotionValueValue(scroll, (y) => y >= switchPoint)

const classes = withState({ floatingSm, floatingMd, scrolled, divider: !!divider, size, bgColor })
const classes = withState({
floatingSm,
floatingMd,
scrolled,
divider: !!divider,
size,
bgColor,
})

return (
<>
Expand Down
1 change: 1 addition & 0 deletions packagesDev/next-config/dist/generated/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function GraphCommerceConfigSchema() {
customerRequireEmailConfirmation: _zod.z.boolean().nullish(),
debug: GraphCommerceDebugConfigSchema().nullish(),
demoMode: _zod.z.boolean().nullish(),
enableStickyAddToCart: _zod.z.boolean().nullish(),
googleAnalyticsId: _zod.z.string().nullish(),
googleRecaptchaKey: _zod.z.string().nullish(),
googleTagmanagerId: _zod.z.string().nullish(),
Expand Down
7 changes: 7 additions & 0 deletions packagesDev/next-config/src/generated/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ export type GraphCommerceConfig = {
* - Creates a big list items in the product list.
*/
demoMode?: InputMaybe<Scalars['Boolean']['input']>;
/**
* Enable this option to activate the sticky add-to-cart bar on the product page.
* When enabled, a sticky bar will appear if the default add-to-cart button goes out of the visible viewport.
* This allows users to easily access the add-to-cart functionality even as they scroll through the page.
*/
enableStickyAddToCart?: InputMaybe<Scalars['Boolean']['input']>;
/**
* See https://support.google.com/analytics/answer/9539598?hl=en
*
Expand Down Expand Up @@ -427,6 +433,7 @@ export function GraphCommerceConfigSchema(): z.ZodObject<Properties<GraphCommerc
customerRequireEmailConfirmation: z.boolean().nullish(),
debug: GraphCommerceDebugConfigSchema().nullish(),
demoMode: z.boolean().nullish(),
enableStickyAddToCart: z.boolean().nullish(),
googleAnalyticsId: z.string().nullish(),
googleRecaptchaKey: z.string().nullish(),
googleTagmanagerId: z.string().nullish(),
Expand Down
Loading