Skip to content

Commit

Permalink
Feature: Page Designer Carousel Component (#977)
Browse files Browse the repository at this point in the history
* Initial Implementation

* Playing with indicator logic

* Fix scroll display value value.

* Clean up JSDoc

* Add tests

* Add logic to ensure controls are only shown when overflowing

* Move Carousel into "layouts"

* Update CHANGELOG.md

* Fix module import

* Update packages/template-retail-react-app/app/components/experience/layouts/carousel/index.jsx

Co-authored-by: Adam Raya <[email protected]>

* Remove testing page

---------

Co-authored-by: Adam Raya <[email protected]>
  • Loading branch information
bendvc and adamraya authored Feb 24, 2023
1 parent 043c7a9 commit cfa5811
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 136 deletions.
1 change: 1 addition & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## v2.7.0-dev (Jan 25, 2023)
- Add Page Designer carousel component [#977](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/977)
- Add Page Designer layout components [#993](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/993)
## v2.6.0 (Jan 25, 2023)
- Mega menu fixes [#875](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/875) and [#910](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/910)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {Fragment, useCallback, useMemo, useRef, useState} from 'react'
import PropTypes from 'prop-types'
import {
AspectRatio,
Box,
Heading,
IconButton,
Stack,
useBreakpoint,
useBreakpointValue
} from '@chakra-ui/react'
import {Component} from '../../component'
import {ChevronLeftIcon, ChevronRightIcon} from '../../../icons'
import {useEffect} from 'react'

/**
* Display child components in a carousel slider manner. Configurations include the number of
* children to display in view as well as whether or not to show controls and position indicators.
*
* @param {PageProps} props
* @param {string} props.textHeading - Heading text for the carousel.
* @param {boolean} props.xsCarouselIndicators - Show/Hide carousel indicators/pips on "xs" screens.
* @param {boolean} props.smCarouselIndicators - Show/Hide carousel indicators/pips on "sm" screens.
* @param {boolean} props.mdCarouselIndicators - Show/Hide carousel indicators/pips on "md" screens.
* @param {boolean} props.xsCarouselControls - Show/Hide carousel forward/back controls on "xs" screens.
* @param {boolean} props.smCarouselControls - Show/Hide carousel forward/back controls on "sm" screens.
* @param {number} props.xsCarouselSlidesToDisplay - Number of children that will be rendered in view on "xs" screens.
* @param {number} props.smCarouselSlidesToDisplay - Number of children that will be rendered in view on "sm" screens.
* @param {number} props.mdCarouselSlidesToDisplay - Number of children that will be rendered in view on "md" screens.
* @param {Object []} props.region - The regions passed internally to this component by the `commerce-sdk-react` Page component.
* @returns {React.ReactElement} - Carousel component.
*/
export const Carousel = (props = {}) => {
const scrollRef = useRef()
const breakpoint = useBreakpoint()
const [hasOverflow, setHasOverflow] = useState(false)

const {
textHeadline,
xsCarouselIndicators = false,
smCarouselIndicators = false,
mdCarouselIndicators = false,
xsCarouselControls = false,
smCarouselControls = false,
xsCarouselSlidesToDisplay = 1,
smCarouselSlidesToDisplay = 1,
mdCarouselSlidesToDisplay = 1,
// Internally Provided
regions
} = props

const controlDisplay = useMemo(() => {
return {
base: xsCarouselControls && hasOverflow ? 'block' : 'none',
sm: xsCarouselControls && hasOverflow ? 'block' : 'none',
md: smCarouselControls && hasOverflow ? 'block' : 'none',
lg: hasOverflow ? 'block' : 'none'
}
}, [hasOverflow])

const itemWidth = {
base: `calc(${100 / xsCarouselSlidesToDisplay}%)`,
sm: `calc(${100 / xsCarouselSlidesToDisplay}%)`,
md: `calc(${100 / smCarouselSlidesToDisplay}%)`,
lg: `calc(${100 / mdCarouselSlidesToDisplay}%)`
}

const overflowXScroll = {
base: xsCarouselIndicators ? 'block' : 'none',
sm: xsCarouselIndicators ? 'block' : 'none',
md: smCarouselIndicators ? 'block' : 'none',
lg: mdCarouselIndicators ? 'block' : 'none'
}
const overflowXScrollValue = useBreakpointValue(overflowXScroll)

const components = regions[0]?.components || []
const itemCount = components.length

// Scroll the container left or right by 100%. Passing no args or `1`
// scrolls to the right, and passing `-1` scrolls left.
const scroll = useCallback((direction = 1) => {
scrollRef.current?.scrollBy({
top: 0,
left: (direction * window.innerWidth) / itemCount,
behavior: 'smooth'
})
})

// Our indicator implementation uses the scrollbar to show the context of the current
// item selected. Because MacOS hides scroll bars after they come to rest we need to
// force them to show. Please note that this feature only works on web-kit browsers,
// for all other browsers the scroller/indicator will be shown.
const style = {
'.scroll-indicator::-webkit-scrollbar': {
display: overflowXScrollValue,
['-webkit-appearance']: `none`,
height: `8px`
},
'.scroll-indicator::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}
}

useEffect(() => {
const {clientWidth, scrollWidth} = scrollRef.current
setHasOverflow(scrollWidth > clientWidth)
}, [breakpoint, props])

return (
<Box className={'carousel'} sx={style} position="relative" data-testid="carousel">
<Stack className={'carousel-container'} data-testid="carousel-container" spacing={6}>
{textHeadline && (
<Heading as="h2" fontSize="xl" textAlign="center">
{textHeadline}
</Heading>
)}

<Stack
ref={scrollRef}
className={'carousel-container-items scroll-indicator'}
data-testid="carousel-container-items"
direction="row"
spacing={0}
wrap="nowrap"
overflowX="scroll"
sx={{
scrollPadding: 0,
scrollSnapType: 'x mandatory',
WebkitOverflowScrolling: 'touch'
}}
>
{components.map((component, index) => (
<Box
key={component?.id || index}
flex="0 0 auto"
width={itemWidth}
style={{scrollSnapAlign: 'start'}}
>
<AspectRatio ratio={1}>
<Component component={component} />
</AspectRatio>
</Box>
))}
</Stack>
</Stack>

{/* Button Controls */}
<Fragment>
<Box
display={controlDisplay}
position="absolute"
top="50%"
left={{base: 0, lg: 4}}
transform="translateY(-50%)"
>
<IconButton
data-testid="carousel-nav-left"
aria-label="Scroll carousel left"
icon={<ChevronLeftIcon color="black" />}
borderRadius="full"
colorScheme="whiteAlpha"
onClick={() => scroll(-1)}
/>
</Box>

<Box
display={controlDisplay}
position="absolute"
top="50%"
right={{base: 0, lg: 4}}
transform="translateY(-50%)"
>
<IconButton
data-testid="carousel-nav-right"
aria-label="Scroll carousel right"
icon={<ChevronRightIcon color="black" />}
borderRadius="full"
colorScheme="whiteAlpha"
onClick={() => scroll(1)}
/>
</Box>
</Fragment>
</Box>
)
}

Carousel.propTypes = {
regions: PropTypes.array.isRequired,
textHeadline: PropTypes.string,
xsCarouselIndicators: PropTypes.bool,
smCarouselIndicators: PropTypes.bool,
mdCarouselIndicators: PropTypes.bool,
xsCarouselControls: PropTypes.bool,
smCarouselControls: PropTypes.bool,
xsCarouselSlidesToDisplay: PropTypes.oneOf([1, 2, 3, 4, 5, 6]),
smCarouselSlidesToDisplay: PropTypes.oneOf([1, 2, 3, 4, 5, 6]),
mdCarouselSlidesToDisplay: PropTypes.oneOf([1, 2, 3, 4, 5, 6])
}

Carousel.displayName = 'Carousel'

export default Carousel
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {renderWithProviders, withPageProvider} from '../../../../utils/test-utils'
import Carousel from './index'

const SAMPLE_REGION = {
id: 'TEST_REGION',
components: [
{
id: 'TEST_COMPONENT',
typeId: 'test-component',
data: {}
}
]
}

test('Carousel renders without errors', () => {
const {getByTestId} = renderWithProviders(<Carousel regions={[]} />)

expect(getByTestId('carousel')).toBeDefined()
expect(getByTestId('carousel-container')).toBeDefined()
expect(getByTestId('carousel-container-items')).toBeDefined()
expect(getByTestId('carousel-nav-left')).toBeDefined()
expect(getByTestId('carousel-nav-left')).toBeDefined()
})

test('Carousel renders region/children without errors', () => {
const CarouselWithPageProvider = withPageProvider(Carousel)
const {getByTestId} = renderWithProviders(
<CarouselWithPageProvider regions={[SAMPLE_REGION]} />
)

expect(getByTestId('carousel')).toBeDefined()
expect(getByTestId('carousel-container-items').childElementCount).toEqual(1)
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

export * from './carousel'
export * from './mobileGrid1r1c'
export * from './mobileGrid2r1c'
export * from './mobileGrid2r2c'
Expand Down
38 changes: 0 additions & 38 deletions packages/template-retail-react-app/app/pages/page-viewer/index.jsx

This file was deleted.

93 changes: 0 additions & 93 deletions packages/template-retail-react-app/app/pages/page-viewer/utils.js

This file was deleted.

Loading

0 comments on commit cfa5811

Please sign in to comment.