use-carousel-hook
Advanced tools
| export {}; |
| "use strict"; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const react_hooks_1 = require("@testing-library/react-hooks"); | ||
| const useCarousel_1 = __importDefault(require("./useCarousel")); | ||
| const childCount = 5; | ||
| const mockCarousel = document.createElement('ul'); | ||
| Object.defineProperties(mockCarousel, { | ||
| offsetWidth: { | ||
| get: () => 200 | ||
| }, | ||
| scrollWidth: { | ||
| get: () => 200 * childCount | ||
| }, | ||
| scrollTo: { | ||
| get: () => jest.fn() | ||
| } | ||
| }); | ||
| for (let i = 0; i < childCount; i++) { | ||
| const child = document.createElement('li'); | ||
| Object.defineProperties(child, { | ||
| offsetLeft: { | ||
| get: () => 200 * i | ||
| }, | ||
| offsetWidth: { | ||
| get: () => 200 | ||
| } | ||
| }); | ||
| mockCarousel.appendChild(child); | ||
| } | ||
| test('calling next() increases current by 1', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.next(); | ||
| }); | ||
| expect(result.current.current).toBe(1); | ||
| }); | ||
| test('calling next(2) increases current by 2', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.next(2); | ||
| }); | ||
| expect(result.current.current).toBe(2); | ||
| }); | ||
| test('carousel goes back to the start when going to next slide on maxIndex', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.next(childCount + 1); | ||
| }); | ||
| expect(result.current.current).toBe(0); | ||
| }); | ||
| test('calling previous() decreases curernt by 1', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.next(2); | ||
| }); | ||
| expect(result.current.current).toBe(2); | ||
| react_hooks_1.act(() => { | ||
| result.current.previous(); | ||
| }); | ||
| expect(result.current.current).toBe(1); | ||
| }); | ||
| test('calling previous(2) decreases current by 2', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.next(4); | ||
| }); | ||
| expect(result.current.current).toBe(4); | ||
| react_hooks_1.act(() => { | ||
| result.current.previous(2); | ||
| }); | ||
| expect(result.current.current).toBe(2); | ||
| }); | ||
| test('setting current with setCurrent', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.setCurrent(3); | ||
| }); | ||
| expect(result.current.current).toBe(3); | ||
| }); | ||
| test('resets current to 0 when not 0 with reset()', () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel } })); | ||
| react_hooks_1.act(() => { | ||
| result.current.setCurrent(2); | ||
| }); | ||
| expect(result.current.current).toBe(2); | ||
| react_hooks_1.act(() => { | ||
| result.current.reset(); | ||
| }); | ||
| expect(result.current.current).toBe(0); | ||
| }); | ||
| test(`doesn't error with scrollBehavior 'auto'`, () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default({ initialRef: { current: mockCarousel }, scrollBehavior: 'auto' })); | ||
| expect(result.current.current).toBe(0); | ||
| react_hooks_1.act(() => { | ||
| result.current.next(); | ||
| }); | ||
| expect(result.current.current).toBe(1); | ||
| }); | ||
| test(`doesn't error without initialRef`, () => { | ||
| const { result } = react_hooks_1.renderHook(() => useCarousel_1.default()); | ||
| expect(result.current.current).toBe(0); | ||
| react_hooks_1.act(() => { | ||
| result.current.setCurrent(1); | ||
| }); | ||
| expect(result.current.current).toBe(1); | ||
| }); |
| module.exports = { | ||
| rootDir: './', | ||
| preset: 'ts-jest', | ||
| testMatch: ['**/src/**/*.test.ts?(x)'], | ||
| transform: { | ||
| '^.+\\.(ts|tsx)$': '<rootDir>/node_modules/ts-jest' | ||
| }, | ||
| testEnvironment: 'jsdom' | ||
| }; |
@@ -6,2 +6,3 @@ import React from 'react'; | ||
| direction?: 'horizontal' | 'vertical'; | ||
| initialRef?: React.RefObject<HTMLElement>; | ||
| } | ||
@@ -13,6 +14,6 @@ interface CarouselPosition { | ||
| interface CarouselHook { | ||
| ref: React.MutableRefObject<HTMLElement>; | ||
| previous: () => void; | ||
| next: () => void; | ||
| setCurrent: (current: number) => void; | ||
| ref: React.RefObject<HTMLElement>; | ||
| previous: (amount?: number, interaction?: Interaction) => void; | ||
| next: (amount?: number, interaction?: Interaction) => void; | ||
| setCurrent: (current: number, interaction?: Interaction) => void; | ||
| reset: () => void; | ||
@@ -23,3 +24,3 @@ position: CarouselPosition; | ||
| } | ||
| declare const useCarousel: (options?: Options | undefined) => CarouselHook; | ||
| declare const useCarousel: (options?: Options) => CarouselHook; | ||
| export default useCarousel; |
+44
-9
@@ -9,4 +9,6 @@ "use strict"; | ||
| const reducer_1 = __importDefault(require("./reducer")); | ||
| const useCarousel = (options) => { | ||
| const ref = react_1.useRef(); | ||
| const useCarousel = (options = {}) => { | ||
| const ref = options.initialRef | ||
| ? options.initialRef | ||
| : react_1.useRef(); | ||
| const [{ current, interaction }, dispatch] = react_1.useReducer(reducer_1.default, { | ||
@@ -17,8 +19,11 @@ current: 0, | ||
| const [maxIndex, setMaxIndex] = react_1.useState(-1); | ||
| const [isUserScroll, setIsUserScroll] = react_1.useState(null); | ||
| const [position, setPosition] = react_1.useState({ isAtStart: true, isAtEnd: false }); | ||
| const [inView, setInView] = react_1.useState([0]); | ||
| const updateMaxIndex = react_1.useCallback(() => { | ||
| if (!ref.current) | ||
| return; | ||
| const el = ref.current; | ||
| const children = [...el.children]; | ||
| const lastScrollableChild = children.findIndex(child => (options === null || options === void 0 ? void 0 : options.direction) === 'vertical' | ||
| const lastScrollableChild = children.findIndex(child => options.direction === 'vertical' | ||
| ? child.offsetTop >= el.scrollHeight - el.offsetHeight | ||
@@ -29,2 +34,4 @@ : child.offsetLeft >= el.scrollWidth - el.offsetWidth); | ||
| const updateInView = () => { | ||
| if (!ref.current) | ||
| return; | ||
| const carouselEl = ref.current; | ||
@@ -38,3 +45,3 @@ const childEls = [...carouselEl.children]; | ||
| for (let index = current + 1; index < childEls.length; index++) { | ||
| if ((options === null || options === void 0 ? void 0 : options.direction) === 'vertical') { | ||
| if (options.direction === 'vertical') { | ||
| if (childEls[index].offsetTop + childEls[index].offsetHeight <= carouselBottom) { | ||
@@ -59,2 +66,4 @@ inView.push(index); | ||
| const updateCurrentOnScroll = (current) => { | ||
| if (!ref.current) | ||
| return; | ||
| const carouselEl = ref.current; | ||
@@ -71,3 +80,18 @@ for (let index = 0; index < carouselEl.children.length; index++) { | ||
| }; | ||
| // Add events to handle setting of isUserScroll | ||
| react_1.useEffect(() => { | ||
| if (!ref.current) | ||
| return; | ||
| const mouseDownHandler = () => setIsUserScroll(true); | ||
| const mouseUpHandler = () => setIsUserScroll(false); | ||
| ref.current.addEventListener('mousedown', mouseDownHandler); | ||
| ref.current.addEventListener('mouseup', mouseUpHandler); | ||
| return () => { | ||
| if (!ref.current) | ||
| return; | ||
| ref.current.removeEventListener('mousedown', mouseDownHandler); | ||
| ref.current.removeEventListener('mouseup', mouseUpHandler); | ||
| }; | ||
| }, []); | ||
| react_1.useEffect(() => { | ||
| window.addEventListener('resize', updateMaxIndex); | ||
@@ -81,8 +105,14 @@ window.addEventListener('resize', updateInView); | ||
| react_1.useEffect(() => { | ||
| if (!ref.current) | ||
| return; | ||
| // When current updates, attach a new event handler with the updated | ||
| // current value to call updateCurrentOnScroll | ||
| const scrollHandler = () => updateCurrentOnScroll(current); | ||
| const scrollHandler = () => isUserScroll && updateCurrentOnScroll(current); | ||
| ref.current.addEventListener('scroll', scrollHandler); | ||
| return () => ref.current.removeEventListener('scroll', scrollHandler); | ||
| }, [current]); | ||
| return () => { | ||
| if (!ref.current) | ||
| return; | ||
| ref.current.removeEventListener('scroll', scrollHandler); | ||
| }; | ||
| }, [current, isUserScroll]); | ||
| react_1.useEffect(() => { | ||
@@ -93,2 +123,4 @@ updateMaxIndex(); | ||
| did_update_1.default(() => { | ||
| if (!ref.current) | ||
| return; | ||
| // If interaction is scroll, the carousel doesn't need to handle the scrolling | ||
@@ -99,3 +131,3 @@ if (interaction !== 'scroll') { | ||
| // Scroll to the element | ||
| if ((options === null || options === void 0 ? void 0 : options.direction) === 'vertical') { | ||
| if (options.direction === 'vertical') { | ||
| carouselEl.scrollTo({ | ||
@@ -109,7 +141,10 @@ top: currentChildEl.offsetTop, | ||
| left: currentChildEl.offsetLeft, | ||
| behavior: (options === null || options === void 0 ? void 0 : options.scrollBehavior) || 'smooth' | ||
| behavior: options.scrollBehavior || 'smooth' | ||
| }); | ||
| } | ||
| } | ||
| // Update the inView items | ||
| updateInView(); | ||
| // Not a user scroll so set to false | ||
| setIsUserScroll(false); | ||
| // Update the carousel position | ||
@@ -116,0 +151,0 @@ setPosition({ |
@@ -6,2 +6,3 @@ import React from 'react'; | ||
| direction?: 'horizontal' | 'vertical'; | ||
| initialRef?: React.RefObject<HTMLElement>; | ||
| } | ||
@@ -13,6 +14,6 @@ interface CarouselPosition { | ||
| interface CarouselHook { | ||
| ref: React.MutableRefObject<HTMLElement>; | ||
| previous: () => void; | ||
| next: () => void; | ||
| setCurrent: (current: number) => void; | ||
| ref: React.RefObject<HTMLElement>; | ||
| previous: (amount?: number, interaction?: Interaction) => void; | ||
| next: (amount?: number, interaction?: Interaction) => void; | ||
| setCurrent: (current: number, interaction?: Interaction) => void; | ||
| reset: () => void; | ||
@@ -23,3 +24,3 @@ position: CarouselPosition; | ||
| } | ||
| declare const useCarousel: (options?: Options | undefined) => CarouselHook; | ||
| declare const useCarousel: (options?: Options) => CarouselHook; | ||
| export default useCarousel; |
+44
-9
| import { useState, useRef, useEffect, useReducer, useCallback } from 'react'; | ||
| import useDidUpdateEffect from '@use-effect/did-update'; | ||
| import reducer from './reducer'; | ||
| const useCarousel = (options) => { | ||
| const ref = useRef(); | ||
| const useCarousel = (options = {}) => { | ||
| const ref = options.initialRef | ||
| ? options.initialRef | ||
| : useRef(); | ||
| const [{ current, interaction }, dispatch] = useReducer(reducer, { | ||
@@ -11,8 +13,11 @@ current: 0, | ||
| const [maxIndex, setMaxIndex] = useState(-1); | ||
| const [isUserScroll, setIsUserScroll] = useState(null); | ||
| const [position, setPosition] = useState({ isAtStart: true, isAtEnd: false }); | ||
| const [inView, setInView] = useState([0]); | ||
| const updateMaxIndex = useCallback(() => { | ||
| if (!ref.current) | ||
| return; | ||
| const el = ref.current; | ||
| const children = [...el.children]; | ||
| const lastScrollableChild = children.findIndex(child => (options === null || options === void 0 ? void 0 : options.direction) === 'vertical' | ||
| const lastScrollableChild = children.findIndex(child => options.direction === 'vertical' | ||
| ? child.offsetTop >= el.scrollHeight - el.offsetHeight | ||
@@ -23,2 +28,4 @@ : child.offsetLeft >= el.scrollWidth - el.offsetWidth); | ||
| const updateInView = () => { | ||
| if (!ref.current) | ||
| return; | ||
| const carouselEl = ref.current; | ||
@@ -32,3 +39,3 @@ const childEls = [...carouselEl.children]; | ||
| for (let index = current + 1; index < childEls.length; index++) { | ||
| if ((options === null || options === void 0 ? void 0 : options.direction) === 'vertical') { | ||
| if (options.direction === 'vertical') { | ||
| if (childEls[index].offsetTop + childEls[index].offsetHeight <= carouselBottom) { | ||
@@ -53,2 +60,4 @@ inView.push(index); | ||
| const updateCurrentOnScroll = (current) => { | ||
| if (!ref.current) | ||
| return; | ||
| const carouselEl = ref.current; | ||
@@ -65,3 +74,18 @@ for (let index = 0; index < carouselEl.children.length; index++) { | ||
| }; | ||
| // Add events to handle setting of isUserScroll | ||
| useEffect(() => { | ||
| if (!ref.current) | ||
| return; | ||
| const mouseDownHandler = () => setIsUserScroll(true); | ||
| const mouseUpHandler = () => setIsUserScroll(false); | ||
| ref.current.addEventListener('mousedown', mouseDownHandler); | ||
| ref.current.addEventListener('mouseup', mouseUpHandler); | ||
| return () => { | ||
| if (!ref.current) | ||
| return; | ||
| ref.current.removeEventListener('mousedown', mouseDownHandler); | ||
| ref.current.removeEventListener('mouseup', mouseUpHandler); | ||
| }; | ||
| }, []); | ||
| useEffect(() => { | ||
| window.addEventListener('resize', updateMaxIndex); | ||
@@ -75,8 +99,14 @@ window.addEventListener('resize', updateInView); | ||
| useEffect(() => { | ||
| if (!ref.current) | ||
| return; | ||
| // When current updates, attach a new event handler with the updated | ||
| // current value to call updateCurrentOnScroll | ||
| const scrollHandler = () => updateCurrentOnScroll(current); | ||
| const scrollHandler = () => isUserScroll && updateCurrentOnScroll(current); | ||
| ref.current.addEventListener('scroll', scrollHandler); | ||
| return () => ref.current.removeEventListener('scroll', scrollHandler); | ||
| }, [current]); | ||
| return () => { | ||
| if (!ref.current) | ||
| return; | ||
| ref.current.removeEventListener('scroll', scrollHandler); | ||
| }; | ||
| }, [current, isUserScroll]); | ||
| useEffect(() => { | ||
@@ -87,2 +117,4 @@ updateMaxIndex(); | ||
| useDidUpdateEffect(() => { | ||
| if (!ref.current) | ||
| return; | ||
| // If interaction is scroll, the carousel doesn't need to handle the scrolling | ||
@@ -93,3 +125,3 @@ if (interaction !== 'scroll') { | ||
| // Scroll to the element | ||
| if ((options === null || options === void 0 ? void 0 : options.direction) === 'vertical') { | ||
| if (options.direction === 'vertical') { | ||
| carouselEl.scrollTo({ | ||
@@ -103,7 +135,10 @@ top: currentChildEl.offsetTop, | ||
| left: currentChildEl.offsetLeft, | ||
| behavior: (options === null || options === void 0 ? void 0 : options.scrollBehavior) || 'smooth' | ||
| behavior: options.scrollBehavior || 'smooth' | ||
| }); | ||
| } | ||
| } | ||
| // Update the inView items | ||
| updateInView(); | ||
| // Not a user scroll so set to false | ||
| setIsUserScroll(false); | ||
| // Update the carousel position | ||
@@ -110,0 +145,0 @@ setPosition({ |
+9
-4
| { | ||
| "name": "use-carousel-hook", | ||
| "version": "0.0.11", | ||
| "version": "0.0.13", | ||
| "description": "Adds functionality for carousels using React hooks", | ||
@@ -9,3 +9,4 @@ "main": "dist/index.js", | ||
| "build": "cross-env ./node_modules/typescript/bin/tsc", | ||
| "build:cjs": "cross-env ./node_modules/typescript/bin/tsc --p tsconfig.cjs.json" | ||
| "build:cjs": "cross-env ./node_modules/typescript/bin/tsc --p tsconfig.cjs.json", | ||
| "test": "cross-env ./node_modules/jest/bin/jest.js" | ||
| }, | ||
@@ -17,2 +18,4 @@ "peerDependencies": { | ||
| "devDependencies": { | ||
| "@testing-library/react-hooks": "^7.0.1", | ||
| "@types/jest": "^26.0.24", | ||
| "@types/node": "^16.3.1", | ||
@@ -28,5 +31,7 @@ "@types/react": "^17.0.14", | ||
| "eslint-plugin-react": "^7.24.0", | ||
| "jest": "^27.0.6", | ||
| "prettier": "^2.3.2", | ||
| "react": "^16.14.0", | ||
| "react-dom": "^16.14.0", | ||
| "react": "17", | ||
| "react-dom": "^17.0.2", | ||
| "ts-jest": "^27.0.4", | ||
| "typescript": "^4.3.5" | ||
@@ -33,0 +38,0 @@ }, |
| stages: | ||
| - deploy | ||
| cache: | ||
| paths: | ||
| - node_modules/ | ||
| publish: | ||
| image: node:16-alpine | ||
| stage: deploy | ||
| rules: | ||
| - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | ||
| changes: | ||
| - package.json | ||
| script: | ||
| - npm ci | ||
| - npm run build | ||
| - npm run build:cjs | ||
| - npm config set -- '//registry.npmjs.org/:_authToken' "${NPM_ACCESS_TOKEN}" | ||
| - npm publish |
| { | ||
| "compilerOptions": { | ||
| "target": "es2017", | ||
| "module": "commonjs", | ||
| "lib": ["dom", "dom.iterable", "esnext"], | ||
| "jsx": "preserve", | ||
| "declaration": true, | ||
| "outDir": "./cjs", | ||
| "noEmit": false, | ||
| "isolatedModules": true, | ||
| "strict": true, | ||
| "strictNullChecks": true, | ||
| "moduleResolution": "node", | ||
| "baseUrl": "./", | ||
| "allowSyntheticDefaultImports": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true | ||
| }, | ||
| "include": ["src/**/*.ts", "src/**/*.tsx"], | ||
| "exclude": ["node_modules"] | ||
| } |
28738
24.94%18
5.88%615
37.89%18
28.57%