From 74bcec219bc578e5143c2180362000df548b8a90 Mon Sep 17 00:00:00 2001 From: Dylan <99700808+dkilgore-eightfold@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:30:44 -0800 Subject: [PATCH] feat: carousel: adds size overlaycontrols and button props (#774) * feat: carousel: adds size overlaycontrols and button props * chore: carousel: address pr comments by adding a variable and comments --- src/components/Carousel/Carousel.stories.tsx | 22 +- src/components/Carousel/Carousel.tsx | 301 +++++++++++++----- src/components/Carousel/Carousel.types.ts | 38 ++- .../Carousel/ScrollMenu/ScrollMenu.tsx | 19 +- src/components/Carousel/Slide/Slide.tsx | 4 +- src/components/Carousel/Tests/Scroll.test.tsx | 92 ++++++ .../Carousel/Tests/ScrollMenu.test.tsx | 17 + src/components/Carousel/Tests/Slide.test.tsx | 85 ++++- .../Tests/__snapshots__/Scroll.test.tsx.snap | 274 ++++++++++++++++ .../__snapshots__/ScrollMenu.test.tsx.snap | 83 ++++- .../Tests/__snapshots__/Slide.test.tsx.snap | 10 +- .../Carousel/Utilities/scrollBySingleItem.tsx | 7 +- src/components/Carousel/carousel.module.scss | 62 ++++ src/octuple.ts | 2 + 14 files changed, 903 insertions(+), 113 deletions(-) create mode 100644 src/components/Carousel/Tests/Scroll.test.tsx create mode 100644 src/components/Carousel/Tests/__snapshots__/Scroll.test.tsx.snap diff --git a/src/components/Carousel/Carousel.stories.tsx b/src/components/Carousel/Carousel.stories.tsx index 7f8749a81..931bfc17e 100644 --- a/src/components/Carousel/Carousel.stories.tsx +++ b/src/components/Carousel/Carousel.stories.tsx @@ -1,7 +1,13 @@ import React, { useRef, useState } from 'react'; import { Stories } from '@storybook/addon-docs'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { autoScrollApiType, Carousel, Slide, VisibilityContext } from './'; +import { + autoScrollApiType, + Carousel, + CarouselSize, + Slide, + VisibilityContext, +} from './'; import { Button, ButtonShape, ButtonSize, ButtonVariant } from '../Button'; import { Card } from '../Card'; import { IconName } from '../Icon'; @@ -37,7 +43,12 @@ export default { ), }, }, - argTypes: {}, + argTypes: { + size: { + options: [CarouselSize.Large, CarouselSize.Medium, CarouselSize.Small], + control: { type: 'radio' }, + }, + }, } as ComponentMeta; const Slide_Story: ComponentStory = (args) => ( @@ -86,10 +97,7 @@ interface SampleItem { key: string; } -const sampleList: SampleItem[] = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, -].map((i) => ({ +const sampleList: SampleItem[] = [1, 2, 3, 4, 5, 6, 7, 8].map((i) => ({ name: `Item ${i}`, key: `key-${i}`, })); @@ -246,6 +254,8 @@ const carouselArgs: Object = { classNames: 'my-carousel', controls: true, 'data-test-id': 'myCarouselTestyId', + overlayControls: true, + size: CarouselSize.Large, }; Slider.args = { diff --git a/src/components/Carousel/Carousel.tsx b/src/components/Carousel/Carousel.tsx index f69613503..e178270a7 100644 --- a/src/components/Carousel/Carousel.tsx +++ b/src/components/Carousel/Carousel.tsx @@ -11,16 +11,21 @@ import React, { import { CarouselContext, CarouselProps, + CarouselSize, CustomScrollBehavior, DataType, DEFAULT_GAP_WIDTH, + DEFAULT_TRANSITION_DURATION, IntersectionObserverItem, ItemOrElement, - OCCLUSION_AVOIDANCE_BUFFER, + OCCLUSION_AVOIDANCE_BUFFER_LARGE, + OCCLUSION_AVOIDANCE_BUFFER_MEDIUM, + OCCLUSION_AVOIDANCE_BUFFER_SMALL, scrollToItemOptions, } from './Carousel.types'; import { ScrollMenu, VisibilityContext } from './ScrollMenu/ScrollMenu'; -import { ButtonShape, ButtonSize, SecondaryButton } from '../Button'; +import { Size } from '../ConfigProvider'; +import { Button, ButtonShape, ButtonSize, ButtonVariant } from '../Button'; import { IconName } from '../Icon'; import { Pagination, @@ -44,8 +49,6 @@ import styles from './carousel.module.scss'; type scrollVisibilityApiType = React.ContextType; -const SCROLL_LOCK_WAIT_IN_MILLISECONDS: number = 40; - const isVisible = (element: HTMLDivElement): boolean => { const rect: DOMRect = element.getBoundingClientRect() || { bottom: 0, @@ -89,14 +92,18 @@ export const Carousel: FC = React.forwardRef( locale = enUS, loop = true, nextIconButtonAriaLabel: defaultNextIconButtonAriaLabel, + nextButtonProps, onMouseEnter, onMouseLeave, onPivotEnd, onPivotStart, + overlayControls = true, pagination = true, pause = 'hover', previousIconButtonAriaLabel: defaultPreviousIconButtonAriaLabel, + previousButtonProps, single = false, + size = CarouselSize.Large, transition = 'push', type = 'slide', 'data-test-id': dataTestId, @@ -179,10 +186,10 @@ export const Carousel: FC = React.forwardRef( }, [animating]); useEffect(() => { - window.addEventListener('scroll', handleScroll, { passive: true }); + window?.addEventListener('scroll', handleScroll, { passive: true }); return () => { - window.removeEventListener('scroll', handleScroll); + window?.removeEventListener('scroll', handleScroll); }; }); @@ -203,13 +210,82 @@ export const Carousel: FC = React.forwardRef( const carouselClassNames: string = mergeClasses( styles.carousel, + { [styles.carouselLarge]: size === CarouselSize.Large }, + { [styles.carouselMedium]: size === CarouselSize.Medium }, + { [styles.carouselSmall]: size === CarouselSize.Small }, + { [styles.carouselOverlayControls]: !!overlayControls }, { [styles.carouselRtl]: htmlDir === 'rtl' }, { [styles.carouselSlider]: type === 'slide' }, { [styles.carouselFade]: transition === 'crossfade' }, classNames ); + const carouselSizeToButtonSizeMap = new Map< + CarouselSize, + ButtonSize | Size + >([ + [CarouselSize.Large, ButtonSize.Large], + [CarouselSize.Medium, ButtonSize.Medium], + [CarouselSize.Small, ButtonSize.Small], + ]); + + const carouselSizeToOcclusionBufferMap = new Map([ + [CarouselSize.Large, OCCLUSION_AVOIDANCE_BUFFER_LARGE], + [CarouselSize.Medium, OCCLUSION_AVOIDANCE_BUFFER_MEDIUM], + [CarouselSize.Small, OCCLUSION_AVOIDANCE_BUFFER_SMALL], + ]); + + const getFirstItemScrollOffset = ( + gapWidth: number, + overlay: boolean, + size: CarouselSize + ): number => { + let offset: number = 0; + if (overlay) { + offset = gapWidth * 2 - carouselSizeToOcclusionBufferMap.get(size); + } else { + offset = gapWidth; + } + return offset; + }; + + const getLastItemScrollOffset = ( + containerPadding: number, + gapWidth: number, + overlay: boolean + ): number => { + let offset: number = 0; + if (overlay) { + offset = + gapWidth * 3 - + containerPadding - + carouselSizeToOcclusionBufferMap.get(size); + } else { + offset = gapWidth * 2 - containerPadding; + } + return offset; + }; + + const getItemScrollOffset = ( + gapWidth: number, + overlay: boolean + ): number => { + let offset: number = 0; + if (overlay) { + // When overlayControls is true, the gapWidth is doubled. + // This was previously handled by the scrollBySingleItem utility, but now it's handled here. + // To avoid occlusion, the offset is increased by the gapWidth on both sides of each inner item. + offset = gapWidth * 2; + } else { + offset = gapWidth; + } + return offset; + }; + const handleCycle = useCallback((): void => { + if (type === 'scroll') { + return; + } handlePause(); if (!auto || (!loop && active === itemsNumber - 1)) { @@ -222,12 +298,22 @@ export const Carousel: FC = React.forwardRef( typeof customInterval === 'number' ? customInterval : interval ); } - }, [onMouseLeave]); - - const handlePause = useCallback( - (): void => pause && data.timeout && clearTimeout(data.timeout), - [onMouseEnter] - ); + }, [ + onMouseLeave, + loop, + active, + itemsNumber, + interval, + customInterval, + data.timeout, + ]); + + const handlePause = useCallback((): void => { + if (type === 'scroll') { + return; + } + pause && data.timeout && clearTimeout(data.timeout); + }, [onMouseEnter, pause, data.timeout]); const nextItemWhenVisible = (): void => { if ( @@ -323,10 +409,22 @@ export const Carousel: FC = React.forwardRef( apiObj: scrollVisibilityApiType, event: React.WheelEvent ): Promise => { + if (scrollLock) { + return Promise.resolve(); + } + setScrollLock(true); const touchpadHorizontal: boolean = await isTouchpadHorizontalScroll( event ); const touchpadVertical: boolean = await isTouchpadVerticalScroll(event); + const transitionDuration: number = + props.carouselScrollMenuProps?.transitionDuration || + DEFAULT_TRANSITION_DURATION; + clearTimeout(timerRef.current); + timerRef.current = setTimeout( + () => setScrollLock(false), + transitionDuration + ); if (event.deltaY < 0 || event.deltaX < 0) { // When not touchpad, handle the scroll if (!touchpadHorizontal || !touchpadVertical) { @@ -344,10 +442,24 @@ export const Carousel: FC = React.forwardRef( apiObj: scrollVisibilityApiType, event: React.WheelEvent ): Promise => { + if (scrollLock) { + return Promise.resolve(); + } + setScrollLock(true); + const gapWidth: number = + props.carouselScrollMenuProps?.gap || DEFAULT_GAP_WIDTH; const touchpadHorizontal: boolean = await isTouchpadHorizontalScroll( event ); const touchpadVertical: boolean = await isTouchpadVerticalScroll(event); + const transitionDuration: number = + props.carouselScrollMenuProps?.transitionDuration || + DEFAULT_TRANSITION_DURATION; + clearTimeout(timerRef.current); + timerRef.current = setTimeout( + () => setScrollLock(false), + transitionDuration + ); if (event.deltaY < 0 || event.deltaX < 0) { // When not touchpad, handle the scroll if (!touchpadHorizontal || !touchpadVertical) { @@ -355,26 +467,27 @@ export const Carousel: FC = React.forwardRef( apiObj.getNextElement(), 'smooth', htmlDir === 'rtl' ? 'previous' : 'next', - !!props.carouselScrollMenuProps?.gap - ? props.carouselScrollMenuProps?.gap - : DEFAULT_GAP_WIDTH, + overlayControls ? gapWidth : 0, apiObj.isFirstItemVisible - ? -DEFAULT_GAP_WIDTH - : OCCLUSION_AVOIDANCE_BUFFER + ? getFirstItemScrollOffset(gapWidth, overlayControls, size) + : getItemScrollOffset(gapWidth, overlayControls) ); } } else if (event.deltaY > 0 || event.deltaX > 0) { // When not touchpad, handle the scroll if (!touchpadHorizontal || !touchpadVertical) { - const gapWidth: number = !!props.carouselScrollMenuProps?.gap - ? props.carouselScrollMenuProps?.gap - : DEFAULT_GAP_WIDTH; apiObj.scrollBySingleItem( apiObj.getPrevElement(), 'smooth', htmlDir === 'rtl' ? 'next' : 'previous', - gapWidth, - apiObj.isLastItemVisible ? gapWidth : OCCLUSION_AVOIDANCE_BUFFER + overlayControls ? gapWidth : 0, + apiObj.isLastItemVisible + ? getLastItemScrollOffset( + props.carouselScrollMenuProps?.containerPadding || 8, + gapWidth, + overlayControls + ) + : getItemScrollOffset(gapWidth, overlayControls) ); } } @@ -384,19 +497,22 @@ export const Carousel: FC = React.forwardRef( apiObj: scrollVisibilityApiType, event: React.WheelEvent ): void => { - // Prevent spamming of scroll. + if (scrollLock) { + return; + } + setScrollLock(true); + const transitionDuration: number = + props.carouselScrollMenuProps?.transitionDuration || + DEFAULT_TRANSITION_DURATION; clearTimeout(timerRef.current); timerRef.current = setTimeout( () => setScrollLock(false), - SCROLL_LOCK_WAIT_IN_MILLISECONDS + transitionDuration ); - if (!scrollLock) { - setScrollLock(true); - if (_single) { - handleSingleItemScrollOnWheel(apiObj, event); - } else { - handleGroupScrollOnWheel(apiObj, event); - } + if (_single) { + handleSingleItemScrollOnWheel(apiObj, event); + } else { + handleGroupScrollOnWheel(apiObj, event); } }; @@ -415,7 +531,7 @@ export const Carousel: FC = React.forwardRef( const nextButton = ( getNextElement?: () => IntersectionObserverItem, isFirstItemVisible?: boolean, - nextDisabled?: boolean, + disabled?: boolean, scrollBySingleItem?: ( target?: ItemOrElement, behavior?: CustomScrollBehavior, @@ -430,26 +546,33 @@ export const Carousel: FC = React.forwardRef( { duration, ease, boundary }?: scrollToItemOptions ) => unknown ): JSX.Element => { + const gapWidth: number = + props.carouselScrollMenuProps?.gap || DEFAULT_GAP_WIDTH; + const nextDisabled = (): boolean => { + return props.type === 'slide' + ? !props.loop && active === itemsNumber - 1 + : disabled; + }; + const nextHidden = (): boolean => { + return overlayControls ? disabled : false; + }; + const transitionDuration: number = + props.carouselScrollMenuProps?.transitionDuration || + DEFAULT_TRANSITION_DURATION; return ( <> - {!nextDisabled && ( + {!nextHidden() && ( <> - {props.type === 'scroll' && ( + {props.type === 'scroll' && overlayControls && (
)} - = React.forwardRef( : IconName.mdiChevronRight, }} key="carousel-next" - onClick={() => + onClick={() => { + if (scrollLock) { + return; + } + setScrollLock(true); + clearTimeout(timerRef.current); + timerRef.current = setTimeout( + () => setScrollLock(false), + transitionDuration + ); props.type === 'scroll' ? _single ? scrollBySingleItem( getNextElement(), 'smooth', htmlDir === 'rtl' ? 'previous' : 'next', - !!props.carouselScrollMenuProps?.gap - ? props.carouselScrollMenuProps?.gap - : DEFAULT_GAP_WIDTH, + overlayControls ? gapWidth : 0, isFirstItemVisible - ? -DEFAULT_GAP_WIDTH - : OCCLUSION_AVOIDANCE_BUFFER + ? getFirstItemScrollOffset( + gapWidth, + overlayControls, + size + ) + : getItemScrollOffset(gapWidth, overlayControls) ) : scrollNextGroup() - : handleControlClick('next') - } - ref={nextButtonRef} + : handleControlClick('next'); + }} shape={ButtonShape.Rectangle} - size={ButtonSize.Medium} + size={carouselSizeToButtonSizeMap.get(size)} + variant={ButtonVariant.Secondary} + {...nextButtonProps} + ref={nextButtonRef} /> )} @@ -488,7 +624,7 @@ export const Carousel: FC = React.forwardRef( const previousButton = ( getPrevElement?: () => IntersectionObserverItem, isLastItemVisible?: boolean, - previousDisabled?: boolean, + disabled?: boolean, scrollBySingleItem?: ( target?: ItemOrElement, behavior?: CustomScrollBehavior, @@ -503,27 +639,31 @@ export const Carousel: FC = React.forwardRef( { duration, ease, boundary }?: scrollToItemOptions ) => unknown ): JSX.Element => { - const gapWidth: number = !!props.carouselScrollMenuProps?.gap - ? props.carouselScrollMenuProps?.gap - : DEFAULT_GAP_WIDTH; + const gapWidth: number = + props.carouselScrollMenuProps?.gap || DEFAULT_GAP_WIDTH; + const previousDisabled = (): boolean => { + return props.type === 'slide' ? !props.loop && active === 0 : disabled; + }; + const previousHidden = (): boolean => { + return overlayControls ? disabled : false; + }; + const transitionDuration: number = + props.carouselScrollMenuProps?.transitionDuration || + DEFAULT_TRANSITION_DURATION; return ( <> - {!previousDisabled && ( + {!previousHidden() && ( <> - {props.type === 'scroll' && ( + {props.type === 'scroll' && overlayControls && (
)} - = React.forwardRef( : IconName.mdiChevronLeft, }} key="carousel-previous" - onClick={() => + onClick={() => { + if (scrollLock) { + return; + } + setScrollLock(true); + clearTimeout(timerRef.current); + timerRef.current = setTimeout( + () => setScrollLock(false), + transitionDuration + ); props.type === 'scroll' ? _single ? scrollBySingleItem( getPrevElement(), 'smooth', htmlDir === 'rtl' ? 'next' : 'previous', - gapWidth, + overlayControls ? gapWidth : 0, isLastItemVisible - ? gapWidth - : OCCLUSION_AVOIDANCE_BUFFER + ? getLastItemScrollOffset( + props.carouselScrollMenuProps + ?.containerPadding || 8, + gapWidth, + overlayControls + ) + : getItemScrollOffset(gapWidth, overlayControls) ) : scrollPrevGroup() - : handleControlClick('previous') - } - ref={previousButtonRef} + : handleControlClick('previous'); + }} shape={ButtonShape.Rectangle} - size={ButtonSize.Medium} + size={carouselSizeToButtonSizeMap.get(size)} + variant={ButtonVariant.Secondary} + {...previousButtonProps} + ref={previousButtonRef} /> )} @@ -691,6 +847,7 @@ export const Carousel: FC = React.forwardRef( controls={controls} nextButton={() => autoScrollButton('next')} onWheel={handleOnWheel} + overlayControls={overlayControls} previousButton={() => autoScrollButton('previous')} rtl={htmlDir === 'rtl'} {...carouselScrollMenuProps} diff --git a/src/components/Carousel/Carousel.types.ts b/src/components/Carousel/Carousel.types.ts index 75bd06f84..5b98046ce 100644 --- a/src/components/Carousel/Carousel.types.ts +++ b/src/components/Carousel/Carousel.types.ts @@ -11,9 +11,13 @@ import { observerOptions as defaultObserverOptions } from './Settings'; import { autoScrollApiType } from './autoScrollApi'; import { OcBaseProps } from '../OcBase'; import { PaginationLocale } from '../Pagination'; +import { ButtonProps } from '../Button'; export const DEFAULT_GAP_WIDTH: number = 4; -export const OCCLUSION_AVOIDANCE_BUFFER: number = 72; +export const DEFAULT_TRANSITION_DURATION: number = 400; +export const OCCLUSION_AVOIDANCE_BUFFER_LARGE: number = 72; +export const OCCLUSION_AVOIDANCE_BUFFER_MEDIUM: number = 56; +export const OCCLUSION_AVOIDANCE_BUFFER_SMALL: number = 44; export type CarouselTransition = 'push' | 'crossfade'; export type CarouselType = 'slide' | 'scroll'; @@ -31,6 +35,12 @@ export type ItemType = React.ReactElement<{ }>; export type visibleElements = string[]; +export enum CarouselSize { + Large = 'large', + Medium = 'medium', + Small = 'small', +} + export interface DataType { /** * The interval timeout. @@ -58,8 +68,6 @@ export const VisibilityContext: React.Context = export const dataKeyAttribute: string = 'data-key'; export const dataIndexAttribute: string = 'data-index'; export const id: string = 'itemId'; -export const innerWrapperClassName: string = - 'carousel-auto-scroll-inner-wrapper'; export const itemClassName: string = 'carousel-scroll-menu-item'; export const separatorClassName: string = 'carousel-scroll-menu-separator'; export const separatorString: string = '-separator'; @@ -110,6 +118,10 @@ export interface CarouselProps * @default 'Next' */ nextIconButtonAriaLabel?: string; + /** + * The next icon button props. + */ + nextButtonProps?: ButtonProps; /** * Callback fired on mouse enter event. */ @@ -122,6 +134,11 @@ export interface CarouselProps * Callback fired when a Carousel transition starts. */ onPivotStart?: (active: number, direction: string) => void; + /** + * Overlay the controls over the Carousel. + * @default true + */ + overlayControls?: boolean; /** * Adds Pagination at the bottom of the Carousel. * @default true @@ -139,12 +156,21 @@ export interface CarouselProps * @default 'Previous' */ previousIconButtonAriaLabel?: string; + /** + * The previous icon button props. + */ + previousButtonProps?: ButtonProps; /** * Whether to scroll by 1 item. * Use when type is 'scroll' * @default false */ single?: boolean; + /** + * The size of the Carousel. + * @default CarouselSize.Large + */ + size?: CarouselSize; /** * Set type of slide transition. * @default 'push' @@ -296,6 +322,11 @@ export interface ScrollMenuProps * Options for intersection observer. */ options?: Partial; + /** + * Overlay the controls over the Carousel. + * @default true + */ + overlayControls?: boolean; /** * Previous button component. */ @@ -338,6 +369,7 @@ export interface ScrollMenuProps transitionBehavior?: string | Function; /** * Duration of transition. + * @default 400 */ transitionDuration?: number; /** diff --git a/src/components/Carousel/ScrollMenu/ScrollMenu.tsx b/src/components/Carousel/ScrollMenu/ScrollMenu.tsx index b6acd3d77..52352aabf 100644 --- a/src/components/Carousel/ScrollMenu/ScrollMenu.tsx +++ b/src/components/Carousel/ScrollMenu/ScrollMenu.tsx @@ -9,7 +9,7 @@ import React, { useState, } from 'react'; import { - innerWrapperClassName, + DEFAULT_TRANSITION_DURATION, ScrollMenuProps, VisibilityContext, } from '../Carousel.types'; @@ -47,13 +47,14 @@ export const ScrollMenu: FC = forwardRef( onUpdate = (): void => void 0, onWheel = (): void => void 0, options = defaultObserverOptions, + overlayControls, previousButton: _leftArrow, rtl, scrollContainerClassNames, scrollWrapperClassNames, separatorClassNames, transitionBehavior, - transitionDuration = 400, + transitionDuration = DEFAULT_TRANSITION_DURATION, transitionEase, 'data-test-id': dataTestId, ...rest @@ -163,8 +164,12 @@ export const ScrollMenu: FC = forwardRef( data-test-id={dataTestId} > -
- {!context?.isFirstItemVisible && controls && LeftArrow} + {controls && !overlayControls && LeftArrow} +
+ {!context?.isFirstItemVisible && + controls && + overlayControls && + LeftArrow} = forwardRef( {children} - {!context?.isLastItemVisible && controls && RightArrow} + {!context?.isLastItemVisible && + controls && + overlayControls && + RightArrow}
+ {controls && !overlayControls && RightArrow}
); diff --git a/src/components/Carousel/Slide/Slide.tsx b/src/components/Carousel/Slide/Slide.tsx index 6366b27bd..fb0a3bd72 100644 --- a/src/components/Carousel/Slide/Slide.tsx +++ b/src/components/Carousel/Slide/Slide.tsx @@ -5,6 +5,8 @@ import { mergeClasses } from '../../../shared/utilities'; import styles from '../carousel.module.scss'; +export const SLIDE_TRANSITION_DURATION: number = 200; + export const Slide: FC = React.forwardRef( (props: CarouselSlideProps, ref: Ref) => { const { @@ -48,7 +50,7 @@ export const Slide: FC = React.forwardRef( ] ); } - }, 0); + }, SLIDE_TRANSITION_DURATION); } prevActive.current = active; diff --git a/src/components/Carousel/Tests/Scroll.test.tsx b/src/components/Carousel/Tests/Scroll.test.tsx new file mode 100644 index 000000000..db68c86bf --- /dev/null +++ b/src/components/Carousel/Tests/Scroll.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import Enzyme from 'enzyme'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; +import MatchMediaMock from 'jest-matchmedia-mock'; +import { Carousel } from '..'; +import { Card } from '../../Card'; +import { ButtonShape } from '../../Button'; +import { useIntersectionObserver } from '../Hooks/useIntersectionObserver'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +Enzyme.configure({ adapter: new Adapter() }); + +let matchMedia: any; + +jest.mock('../Hooks/useIntersectionObserver'); + +interface SampleItem { + name: string; + key: string; +} + +const sampleList: SampleItem[] = [1, 2, 3, 4, 5, 6, 7, 8].map((i) => ({ + name: `test${i}`, + key: `key${i}`, +})); +// eslint-disable-next-line radar/no-duplicate-string +const defaultItemsWithSeparators = [ + 'test1', + 'item1-separator', + 'test2', + 'item2-separator', + 'test3', + 'item3-separator', + 'test4', + 'item4-separator', + 'test5', + 'item5-separator', + 'test6', + 'item6-separator', + 'test7', + 'item7-separator', + 'test8', +]; + +describe('Scroll', () => { + beforeAll(() => { + matchMedia = new MatchMediaMock(); + }); + afterEach(() => { + matchMedia.clear(); + }); + + test('Carousel button props', () => { + (useIntersectionObserver as jest.Mock).mockReturnValue({ + visibleElementsWithSeparators: defaultItemsWithSeparators, + }); + const { container } = render( + ( + +
+ {item.name} +
+
+ )), + containerPadding: 8, + gap: 24, + }} + nextButtonProps={{ shape: ButtonShape.Round }} + previousButtonProps={{ shape: ButtonShape.Round }} + type="scroll" + /> + ); + const buttonNext = container.querySelector('.carousel-next'); + const buttonPrevious = container.querySelector('.carousel-previous'); + + expect(buttonNext).toHaveClass('round-shape'); + expect(buttonPrevious).toHaveClass('round-shape'); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/Carousel/Tests/ScrollMenu.test.tsx b/src/components/Carousel/Tests/ScrollMenu.test.tsx index 0a7538c37..18acbe4b3 100644 --- a/src/components/Carousel/Tests/ScrollMenu.test.tsx +++ b/src/components/Carousel/Tests/ScrollMenu.test.tsx @@ -324,6 +324,23 @@ describe('ScrollMenu', () => { expect(container).toMatchSnapshot(); }); + + test('overlayControls is false', () => { + (useIntersectionObserver as jest.Mock).mockReturnValue({ + visibleElementsWithSeparators: defaultItemsWithSeparators, + }); + + const { container } = setup({ + previousButton: PreviousButton, + nextButton: NextButton, + overlayControls: false, + }); + + expect(container.querySelector('.carousel-next-mask')).toBeFalsy(); + expect(container.querySelector('.carousel-previous-mask')).toBeFalsy(); + + expect(container).toMatchSnapshot(); + }); }); describe('Events', () => { diff --git a/src/components/Carousel/Tests/Slide.test.tsx b/src/components/Carousel/Tests/Slide.test.tsx index 6b90bbef4..bbe6d2158 100644 --- a/src/components/Carousel/Tests/Slide.test.tsx +++ b/src/components/Carousel/Tests/Slide.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Enzyme from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MatchMediaMock from 'jest-matchmedia-mock'; -import { Carousel, Slide } from '..'; +import { Carousel, CarouselSize, Slide } from '..'; import { fireEvent, render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; @@ -93,7 +93,7 @@ describe('Slide', () => { expect(slide2.classList.contains('active')).toBe(false); }); - test('Carousel click on button', () => { + test('Carousel click on next button', () => { jest.useFakeTimers(); const { container } = render( @@ -115,15 +115,88 @@ describe('Slide', () => { fireEvent.transitionEnd(slide1); fireEvent.transitionEnd(slide2); - expect(slide1).not.toHaveClass('active'); expect(slide2).toHaveClass('active'); + expect(slide2).toHaveClass('carousel-slide'); + expect(slide1.classList.contains('active')).toBe(false); + expect(slide1).toHaveClass('carousel-slide'); + }); + + test('Carousel click on previous button', () => { + jest.useFakeTimers(); + const { container } = render( + + Slide-1 + Slide-2 + Slide-3 + + ); + const slide1 = container.querySelector('.slide-1'); + const slide3 = container.querySelector('.slide-3'); + + expect(slide1).toHaveClass('active'); + expect(slide1).toHaveClass('carousel-slide'); + expect(slide3.classList.contains('active')).toBe(false); + expect(slide3).toHaveClass('carousel-slide'); const buttonPrev = container.querySelector('.carousel-previous'); buttonPrev && fireEvent.click(buttonPrev); fireEvent.transitionEnd(slide1); - fireEvent.transitionEnd(slide2); + fireEvent.transitionEnd(slide3); - expect(slide1.classList.contains('active')).toBe(true); - expect(slide2.classList.contains('active')).toBe(false); + expect(slide3).toHaveClass('active'); + expect(slide3).toHaveClass('carousel-slide'); + expect(slide1.classList.contains('active')).toBe(false); + expect(slide1).toHaveClass('carousel-slide'); + }); + + test('Carousel default size is large', () => { + const { container } = render( + + Slide-1 + Slide-2 + Slide-3 + + ); + const buttonNext = container.querySelector('.carousel-next'); + const carousel = container.querySelector('.carousel'); + const buttonPrev = container.querySelector('.carousel-previous'); + + expect(carousel).toHaveClass('carousel-large'); + expect(buttonNext).toHaveClass('button-large'); + expect(buttonPrev).toHaveClass('button-large'); + }); + + test('Carousel size is medium', () => { + const { container } = render( + + Slide-1 + Slide-2 + Slide-3 + + ); + const buttonNext = container.querySelector('.carousel-next'); + const carousel = container.querySelector('.carousel'); + const buttonPrev = container.querySelector('.carousel-previous'); + + expect(carousel).toHaveClass('carousel-medium'); + expect(buttonNext).toHaveClass('button-medium'); + expect(buttonPrev).toHaveClass('button-medium'); + }); + + test('Carousel size is small', () => { + const { container } = render( + + Slide-1 + Slide-2 + Slide-3 + + ); + const buttonNext = container.querySelector('.carousel-next'); + const carousel = container.querySelector('.carousel'); + const buttonPrev = container.querySelector('.carousel-previous'); + + expect(carousel).toHaveClass('carousel-small'); + expect(buttonNext).toHaveClass('button-small'); + expect(buttonPrev).toHaveClass('button-small'); }); }); diff --git a/src/components/Carousel/Tests/__snapshots__/Scroll.test.tsx.snap b/src/components/Carousel/Tests/__snapshots__/Scroll.test.tsx.snap new file mode 100644 index 000000000..12093ad3b --- /dev/null +++ b/src/components/Carousel/Tests/__snapshots__/Scroll.test.tsx.snap @@ -0,0 +1,274 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Scroll Carousel button props 1`] = ` +
+