diff --git a/__tests__/carousel.spec.tsx b/__tests__/carousel.spec.tsx index 23bfe23..84232d7 100644 --- a/__tests__/carousel.spec.tsx +++ b/__tests__/carousel.spec.tsx @@ -4,7 +4,7 @@ import { Carousel } from '../src/components/carousel'; import { defaultProps } from '../src/components/carousel/defaultProps'; import { carouselItemNodes, dynamicCarouselItemNodes } from './__fixtures__/nodes'; import * as helpers from '../src/helpers'; -import { SlideDirection } from '../src/types/carousel'; +import * as hooks from '../src/hooks'; describe('', () => { let mockGetPageX: jest.SpyInstance< @@ -303,51 +303,30 @@ describe('', () => { expect(button!.innerHTML).toEqual('1'); }); - it('should invoke beforeChange when beforeChange prop is defined on slide', async () => { - const onBeforeChange = (_: SlideDirection) => { - return; - }; - const stub = jest.fn(onBeforeChange); - const { getByTestId } = render( - , - ); - const carousel = getByTestId('carousel'); - const button = carousel.querySelector('button'); - - expect(button).not.toBeNull(); - - fireEvent.click(button!); - jest.runAllTimers(); - - expect(stub).toHaveBeenCalledTimes(1); - }); + it('should slide to right when click right button', async () => { + const paginationCallback = jest.fn(); + const mockUsePrevious = jest + .spyOn(hooks, 'usePrevious') + .mockImplementation(() => carouselItemNodes(2)); - it('should invoke afterChange when afterChange prop is defined on slide', async () => { - const onAfterChange = (_: SlideDirection) => { - return; - }; - const stub = jest.fn(onAfterChange); const { getByTestId } = render( , ); const carousel = getByTestId('carousel'); - const button = carousel.querySelector('button'); + const button = carousel.querySelectorAll('button')[1]; expect(button).not.toBeNull(); fireEvent.click(button!); jest.runAllTimers(); - expect(stub).toHaveBeenCalledTimes(1); + expect(paginationCallback).toHaveBeenCalledTimes(1); + expect(mockUsePrevious).toBeCalled(); }); }); diff --git a/__tests__/helpers.spec.tsx b/__tests__/helpers.spec.tsx index 40a4a8c..e1f05b5 100644 --- a/__tests__/helpers.spec.tsx +++ b/__tests__/helpers.spec.tsx @@ -82,10 +82,17 @@ describe('helpers', () => { const oldNodes = reactNodes('old', 5) as Item[]; const newNodes = reactNodes('new', 6) as Item[]; const expected = reactNodes('new', 5) as Item[]; + const prevChildren = undefined; const infinite = true; const slide = 5; - const result = helpers.updateNodes(oldNodes, newNodes, slide, infinite); + const result = helpers.updateNodes( + oldNodes, + newNodes, + prevChildren, + slide, + infinite, + ); expect(result).toEqual(expected); }); @@ -94,6 +101,7 @@ describe('helpers', () => { const oldNodes = reactNodes('old', 5) as Item[]; const infinite = true; const slide = 2; + const prevChildren = undefined; const newNodes = ([ { key: 14, @@ -123,8 +131,44 @@ describe('helpers', () => { }, ] as unknown) as Item[]; - const result = helpers.updateNodes(oldNodes, newNodes, slide, infinite); + const result = helpers.updateNodes( + oldNodes, + newNodes, + prevChildren, + slide, + infinite, + ); expect(result).toEqual(expectedNodes); }); + + it('should update nodes when previous children length is less than new children', async () => { + const oldNodes = reactNodes('old', 5) as Item[]; + const newNodes = reactNodes('new', 6) as Item[]; + const prevChildren = newNodes.slice(0, 5); + const expected = newNodes.slice(1, 6).concat(newNodes); + const infinite = true; + const slide = 5; + + const result = helpers.updateNodes( + oldNodes, + newNodes, + prevChildren, + slide, + infinite, + ); + + expect(result).toEqual(expected); + }); + + it('should get corre transform amount', async () => { + const width = 100; + const slideCount = 2; + const direction = SlideDirection.Left; + const expected = 200; + + const result = helpers.getTransformAmount(width, slideCount, direction); + + expect(result).toEqual(expected); + }); }); diff --git a/jest.config.js b/jest.config.js index 3524023..ee202b0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,10 +4,10 @@ module.exports = { collectCoverageFrom: ['src/**/*.{ts,tsx}'], coverageThreshold: { global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, + branches: 93, + functions: 99, + lines: 99, + statements: 99, }, }, coveragePathIgnorePatterns: ['/src/components/scrolling-carousel'], diff --git a/src/components/carousel/defaultProps.ts b/src/components/carousel/defaultProps.ts index 8fcdcb9..6088744 100644 --- a/src/components/carousel/defaultProps.ts +++ b/src/components/carousel/defaultProps.ts @@ -13,6 +13,6 @@ export const defaultProps: Required = { useArrowKeys: false, a11y: {}, dynamic: false, - beforeChange: null, - afterChange: null, + paginationCallback: null, + pageCount: 0, }; diff --git a/src/components/carousel/index.tsx b/src/components/carousel/index.tsx index 53c1273..623c649 100644 --- a/src/components/carousel/index.tsx +++ b/src/components/carousel/index.tsx @@ -1,4 +1,10 @@ -import React, { useState, FunctionComponent, KeyboardEvent } from 'react'; +import React, { + useState, + FunctionComponent, + KeyboardEvent, + useEffect, + useRef, +} from 'react'; import { Arrow } from '../arrow'; import { ItemProvider } from '../item'; import { @@ -13,6 +19,7 @@ import { import { SlideDirection, Item, ArrowKeys } from '../../types/carousel'; import { defaultProps } from './defaultProps'; import styles from '../../styles/styles.module.css'; +import { usePrevious } from '../../hooks'; export const Carousel: FunctionComponent = (userProps: CarouselProps) => { const props: Required = { ...defaultProps, ...userProps }; @@ -29,20 +36,35 @@ export const Carousel: FunctionComponent = (userProps: CarouselPr const [showArrow, setShowArrow] = useState( getShowArrow(props.children.length, props.show, props.infinite, current), ); + const prevChildren = usePrevious(userProps.children); + const [page, setPage] = useState(0); + const itemsRef = useRef(initItems(props.children, props.slide, props.infinite)); + const isPaginating = useRef(false); if (props.dynamic) { - React.useEffect(() => { + useEffect(() => { const newItems = updateNodes( - items, - userProps.children, + itemsRef.current, + props.children, + prevChildren, props.slide, props.infinite, ); + setItems(newItems); - }, userProps.children); + itemsRef.current = newItems; + if ( + page < props.pageCount && + prevChildren && + prevChildren?.length < props.children.length + ) { + slide(SlideDirection.Right); + setPage(page + 1); + } + }, [props.children]); } - const slide = (direction: SlideDirection, slide: number): void => { + const slide = (direction: SlideDirection) => { if ( animation.isSliding || (direction === SlideDirection.Right && !showArrow.right) || @@ -51,37 +73,55 @@ export const Carousel: FunctionComponent = (userProps: CarouselPr return; } - if (props.beforeChange) props.beforeChange(direction); + if ( + props.paginationCallback && + direction === SlideDirection.Right && + page < props.pageCount - 1 && + !isPaginating.current + ) { + isPaginating.current = true; + props.paginationCallback(direction); + return; + } + + const elements = props.children; - const next = getCurrent(current, slide, props.children.length, direction); + const next = getCurrent(current, props.slide, elements.length, direction); const rotated = props.infinite - ? rotateItems(props.children, items, next, props.show, slide, direction) + ? rotateItems(elements, items, next, props.show, props.slide, direction) : items; if (props.infinite && direction === SlideDirection.Right) { setItems(rotated); + itemsRef.current = rotated; } setAnimation({ - transform: animation.transform + getTransformAmount(width, slide, direction), + transform: + animation.transform + getTransformAmount(width, props.slide, direction), transition: props.transition, isSliding: true, }); setCurrent(next); - setShowArrow( - getShowArrow(props.children.length, props.show, props.infinite, next), - ); + setShowArrow(getShowArrow(elements.length, props.show, props.infinite, next)); setTimeout(() => { if (props.infinite) { - setItems(cleanItems(rotated, slide, direction)); + const cleanedItems = cleanItems( + direction === SlideDirection.Right ? itemsRef.current : rotated, + props.slide, + direction, + ); + setItems(cleanedItems); + itemsRef.current = cleanedItems; } setAnimation({ transform: props.infinite - ? getTransformAmount(width, slide, SlideDirection.Right) - : animation.transform + getTransformAmount(width, slide, direction), + ? getTransformAmount(width, props.slide, SlideDirection.Right) + : animation.transform + + getTransformAmount(width, props.slide, direction), transition: 0, isSliding: false, }); - if (props.afterChange) props.afterChange(direction); }, props.transition * 1_0_0_0); + isPaginating.current = false; }; const widthCallBack = (calculatedWidth: number) => { @@ -108,14 +148,14 @@ export const Carousel: FunctionComponent = (userProps: CarouselPr }; const slideCallback = (direction: SlideDirection) => { - slide(direction, props.slide); + slide(direction); }; const handleOnKeyDown = (e: KeyboardEvent) => { if (e.keyCode === ArrowKeys.Left) { - slide(SlideDirection.Left, props.slide); + slide(SlideDirection.Left); } else if (e.keyCode === ArrowKeys.Right) { - slide(SlideDirection.Right, props.slide); + slide(SlideDirection.Right); } }; @@ -128,25 +168,19 @@ export const Carousel: FunctionComponent = (userProps: CarouselPr className={`${styles.carouselBase} ${props.className}`} > {showArrow.left && ( - slide(SlideDirection.Left, props.slide)} - /> + slide(SlideDirection.Left)} /> )} {showArrow.right && ( - slide(SlideDirection.Right, props.slide)} - /> + slide(SlideDirection.Right)} /> )} ); @@ -165,8 +199,8 @@ export interface CarouselProps { useArrowKeys?: boolean; a11y?: { [key: string]: string }; dynamic?: boolean; - beforeChange?: ((direction: SlideDirection) => void) | null; - afterChange?: ((direction: SlideDirection) => void) | null; + paginationCallback?: ((direction: SlideDirection) => any) | null; + pageCount?: number; } export interface CarouselState { diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 5b5595a..949b971 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -14,7 +14,7 @@ export class Circular { prev(): T { const i = this.currentIndex; const arr = this.arr; - this.currentIndex = i > 0 ? i - 1 : arr.length - 1; + this.currentIndex = i > 0 && i < arr.length ? i - 1 : arr.length - 1; return this.current(); } @@ -147,9 +147,14 @@ export function getOuterWidth(el: HTMLElement) { export function updateNodes( oldItems: Item[], newItems: Item[], + prevChildren: Item[] | undefined, slide: number, infinite: boolean, ): Item[] { + if (prevChildren && prevChildren.length < newItems.length) { + return initItems(newItems, slide, infinite); + } + const matchedItems = oldItems.map((oldItem) => { return newItems.find((newItem) => oldItem.key === newItem.key) as Item; }); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d8d0c3d..206c393 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; export const useWindowWidthChange = (callBack: (changed: number) => any) => { const [windowWidth, setWindowWidth] = useState(window.innerWidth); @@ -13,3 +13,11 @@ export const useWindowWidthChange = (callBack: (changed: number) => any) => { }, []); return; }; + +export const usePrevious = (value: T) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; diff --git a/website/src/pages/index.js b/website/src/pages/index.js index f8b20b2..232c67a 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import classnames from 'classnames'; import Layout from '@theme/Layout'; import Link from '@docusaurus/Link'; @@ -8,7 +8,7 @@ import { Carousel, ScrollingCarousel } from '@trendyol-js/react-carousel'; import styles from './styles.module.css'; import { Redirect } from '@docusaurus/router'; -const slideData = [ +let slideData = [ { text: 'skyline', img: @@ -110,38 +110,7 @@ const slideData = [ 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRlGGm-Hh5so6PtScyr7lXrB9V13dFr7mtMJC5YlO9aaDPHfPQNRvzsFPK_&s', }, ]; -const features = [ - { - title: <>Easy to Use, - imageUrl: 'img/undraw_docusaurus_mountain.svg', - description: ( - <> - Docusaurus was designed from the ground up to be easily installed and used - to get your website up and running quickly. - - ), - }, - { - title: <>Focus on What Matters, - imageUrl: 'img/undraw_docusaurus_tree.svg', - description: ( - <> - Docusaurus lets you focus on your docs, and we'll do the chores. Go - ahead and move your docs into the docs directory. - - ), - }, - { - title: <>Powered by React, - imageUrl: 'img/undraw_docusaurus_react.svg', - description: ( - <> - Extend or customize your website layout by reusing React. Docusaurus can - be extended while reusing the same header and footer. - - ), - }, -]; + const features2 = (num) => { let i = 0; return new Array(num).fill(0).map(() => { @@ -154,6 +123,7 @@ const features2 = (num) => { function Feature({ imageUrl, title, description }) { const imgUrl = useBaseUrl(imageUrl); + return (
{imgUrl && ( @@ -168,8 +138,124 @@ function Feature({ imageUrl, title, description }) { } function Home() { - return ; + //return ; + const [features, setFeatures] = useState([ + { + id: 1, + title: <>1, + imageUrl: 'img/undraw_docusaurus_mountain.svg', + description: ( + <> + Docusaurus was designed from the ground up to be easily installed and + used to get your website up and running quickly. + + ), + }, + { + id: 2, + title: <>2, + imageUrl: 'img/undraw_docusaurus_tree.svg', + description: ( + <> + Docusaurus lets you focus on your docs, and we'll do the chores. + Go ahead and move your docs into the docs directory. + + ), + }, + { + id: 3, + title: <>3, + imageUrl: 'img/undraw_docusaurus_react.svg', + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus + can be extended while reusing the same header and footer. + + ), + }, + { + id: 4, + title: <>4, + imageUrl: 'img/undraw_docusaurus_react.svg', + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus + can be extended while reusing the same header and footer. + + ), + }, + { + id: 5, + title: <>5, + imageUrl: 'img/undraw_docusaurus_react.svg', + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus + can be extended while reusing the same header and footer. + + ), + }, + { + id: 6, + title: <>6, + imageUrl: 'img/undraw_docusaurus_react.svg', + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus + can be extended while reusing the same header and footer. + + ), + }, + { + id: 7, + title: <>7, + imageUrl: 'img/undraw_docusaurus_react.svg', + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus + can be extended while reusing the same header and footer. + + ), + }, + ]); + + const [added, setAdded] = useState(false); + const [addedCount, setAddedCount] = useState(1); + const [extraItems, setExtraItems] = useState([]); + const addItems = () => { + if (addedCount > 2) { + return []; + } + + const newItems = [...Array(20)].map((_, id) => { + const attr = { + id, + title: <>{id}, + imageUrl: 'img/undraw_docusaurus_react.svg', + description: ( + <> + Extend or customize your website layout by reusing React. + Docusaurus can be extended while reusing the same header and + footer. + + ), + }; + return ; + }); + + if (addedCount == 1) { + setExtraItems(newItems.slice(8, 11)); + } else if (addedCount == 2) { + setExtraItems(newItems.slice(11, 15)); + } + + setAdded(true); + setAddedCount(addedCount + 1); + return newItems; + }; + const context = useDocusaurusContext(); + const [dataa, setData] = useState(slideData); const { siteConfig = {} } = context; return (
-
- - {slideData.map((d) => ( -
- - {d.text} -
- ))} -
-
+
-
- - {features2(15).map((props, idx) => ( -
- {props.description} -
- ))} -
-
+
{features && features.length && (
- + + {features.map((props, idx) => ( - + ))}