@yoursurprise/slider 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ .eslintrc.js
package/.eslintrc.js ADDED
@@ -0,0 +1,45 @@
1
+ module.exports = {
2
+ extends: [
3
+ 'eslint:recommended',
4
+ 'airbnb-typescript',
5
+ 'airbnb/hooks',
6
+ 'plugin:import/recommended',
7
+ ],
8
+ rules: {
9
+ 'react/jsx-indent': ['error', 4],
10
+ 'react/jsx-indent-props': ['error', 4],
11
+ 'react/jsx-props-no-spreading': ['error', { custom: 'ignore' }],
12
+ 'react/prop-types': 'off',
13
+ 'react/require-default-props': 'off',
14
+ 'indent': ['error', 4, {SwitchCase: 1}],
15
+ '@typescript-eslint/indent': ['error', 4],
16
+ 'import/no-extraneous-dependencies': [
17
+ 'error',
18
+ {
19
+ devDependencies: [
20
+ '**/*.test.*',
21
+ '**/rollup.config.js*',
22
+ '**/babel.config.js*',
23
+ '**/jest.config.js*',
24
+ '**/[mM]ock[s?]/**'
25
+ ]
26
+ }
27
+ ],
28
+
29
+ },
30
+ plugins: ['react', "testing-library"],
31
+ env: {
32
+ browser: true,
33
+ es6: true,
34
+ node: true,
35
+ },
36
+ parser: '@typescript-eslint/parser',
37
+ parserOptions: {
38
+ project: ['./tsconfig.json'],
39
+ ecmaFeatures: {
40
+ jsx: true,
41
+ },
42
+ ecmaVersion: 'latest',
43
+ sourceType: 'module',
44
+ },
45
+ };
@@ -0,0 +1,18 @@
1
+ name: Lint
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ jobs:
8
+ linting:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Check out repository code
12
+ uses: actions/checkout@v3
13
+ - name: Install
14
+ run: npm ci
15
+ - name: Lint
16
+ run: npm run lint
17
+ - name: Test
18
+ run: npm run test -- --coverage
@@ -0,0 +1,23 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [created]
5
+ jobs:
6
+ publish:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - name: Check out repository code
10
+ uses: actions/checkout@v3
11
+ - name: Setup node
12
+ uses: actions/setup-node@v3
13
+ with:
14
+ node-version: '16.x'
15
+ registry-url: 'https://registry.npmjs.org'
16
+ - name: Install
17
+ run: npm ci
18
+ - name: Build
19
+ run: npm run build
20
+ - name: Publish
21
+ run: npm publish
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ presets: [
3
+ '@babel/preset-env',
4
+ ['@babel/preset-react', { runtime: 'automatic' }],
5
+ '@babel/preset-typescript',
6
+ ],
7
+ };
@@ -0,0 +1,2 @@
1
+ .slider{position:relative}.slider__button{bottom:0;display:none;height:48px;margin:auto 0;position:absolute;top:0;width:48px}@media (hover:hover){.slider__button{display:flex}}.slider__button--next{right:-8px}@media screen and (min-width:768px){.slider__button--next{right:0}}.slider__button--prev{left:-8px}@media screen and (min-width:768px){.slider__button--prev{left:0}}.slider__wrapper{-ms-overflow-style:none;display:flex;flex-direction:row;margin:16px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;scrollbar-width:none}.slider__wrapper::-webkit-scrollbar{display:none}@media screen and (min-width:768px){.slider__wrapper{margin:8px}}@media (hover:hover){.slider__wrapper{scroll-snap-type:none}}.slider__wrapper.is-scrollable{cursor:move}.slider__wrapper.is-dragging{cursor:grabbing;user-select:none}.slider__wrapper:not(.is-dragging){scroll-behavior:smooth}.slider__wrapper__slide{margin-right:8px;scroll-snap-align:start}@media screen and (min-width:768px){.slider__wrapper__slide{margin-left:0;margin-right:16px}}.slider__wrapper__slide:last-child{margin-right:0}
2
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["Slider.scss"],"names":[],"mappings":"AAAA,QACE,iBACF,CACA,gBAEE,QAAS,CAKT,YAAa,CAHb,WAAY,CADZ,aAAc,CAGd,iBAAkB,CALlB,KAAM,CAIN,UAGF,CACA,qBACE,gBACE,YACF,CACF,CACA,sBACE,UACF,CACA,oCACE,sBACE,OACF,CACF,CACA,sBACE,SACF,CACA,oCACE,sBACE,MACF,CACF,CACA,iBAEE,uBAAwB,CAExB,YAAa,CACb,kBAAmB,CAEnB,WAAY,CADZ,eAAgB,CALhB,6BAA8B,CAO9B,4BAA6B,CAL7B,oBAMF,CACA,oCACE,YACF,CACA,oCACE,iBACE,UACF,CACF,CACA,qBACE,iBACE,qBACF,CACF,CACA,+BACE,WACF,CACA,6BACE,eAAgB,CAChB,gBACF,CACA,mCACE,sBACF,CACA,wBAEE,gBAAiB,CADjB,uBAEF,CACA,oCACE,wBACE,aAAc,CACd,iBACF,CACF,CACA,mCACE,cACF","file":"index.css","sourcesContent":[".slider {\n position: relative;\n}\n.slider__button {\n top: 0;\n bottom: 0;\n margin: auto 0;\n height: 48px;\n width: 48px;\n position: absolute;\n display: none;\n}\n@media (hover: hover) {\n .slider__button {\n display: flex;\n }\n}\n.slider__button--next {\n right: -8px;\n}\n@media screen and (min-width: 768px) {\n .slider__button--next {\n right: 0;\n }\n}\n.slider__button--prev {\n left: -8px;\n}\n@media screen and (min-width: 768px) {\n .slider__button--prev {\n left: 0;\n }\n}\n.slider__wrapper {\n overscroll-behavior-x: contain;\n -ms-overflow-style: none;\n scrollbar-width: none;\n display: flex;\n flex-direction: row;\n overflow-x: auto;\n margin: 16px;\n scroll-snap-type: x proximity;\n}\n.slider__wrapper::-webkit-scrollbar {\n display: none;\n}\n@media screen and (min-width: 768px) {\n .slider__wrapper {\n margin: 8px;\n }\n}\n@media (hover: hover) {\n .slider__wrapper {\n scroll-snap-type: initial;\n }\n}\n.slider__wrapper.is-scrollable {\n cursor: move;\n}\n.slider__wrapper.is-dragging {\n cursor: grabbing;\n user-select: none;\n}\n.slider__wrapper:not(.is-dragging) {\n scroll-behavior: smooth;\n}\n.slider__wrapper__slide {\n scroll-snap-align: start;\n margin-right: 8px;\n}\n@media screen and (min-width: 768px) {\n .slider__wrapper__slide {\n margin-left: 0;\n margin-right: 16px;\n }\n}\n.slider__wrapper__slide:last-child {\n margin-right: 0;\n}"]}
@@ -0,0 +1,2 @@
1
+ "use strict";var e,t,r=require("react/jsx-runtime"),n=require("react");!function(e){e[e.FULL=0]="FULL",e[e.PARTIAL=1]="PARTIAL",e[e.NONE=2]="NONE"}(e||(e={})),function(e){e[e.PREV=0]="PREV",e[e.NEXT=1]="NEXT"}(t||(t={}));module.exports=({children:s})=>{const c=n.useRef([]),i=n.useRef(null),l=n.useRef([]),o=n.useRef([]),[u,a]=n.useState(!1),[d,f]=n.useState(!1),[h,b]=n.useState(!1),[L,g]=n.useState({clientX:0,scrollX:0}),m=n.useRef(null),p=n.useRef(null);n.useEffect((()=>{const e=i.current;if(!e)return()=>{};const t=()=>a(e.classList.toggle("is-scrollable",e.scrollWidth>e.clientWidth));return window?.addEventListener("resize",t),t(),()=>{window?.removeEventListener("resize",t)}}),[i]),n.useEffect((()=>{i.current?.classList.toggle("is-scrollable",u)}),[u]),n.useEffect((()=>{i.current?.classList.toggle("is-dragging",d);const e=e=>{e.stopPropagation(),e.preventDefault(),b(!1),f(!1)};return d&&document?.addEventListener("mouseup",e),()=>{document?.removeEventListener("mouseup",e)}}),[d]);const E=()=>l.current[0]??o.current[0]??-1,v=()=>l.current[l.current.length-1]??o.current[o.current.length-1]??-1,N=n.useCallback((()=>{const e=v()+1===c.current.length,t=u&&!1===e,r=E()>0&&u;p.current&&m.current&&(p.current.classList.toggle("hidden",!1===t),p.current.ariaHidden=String(!1===t),m.current.classList.toggle("hidden",!1===r),m.current.ariaHidden=String(!1===r))}),[u]),R=t=>t>=.9?e.FULL:t>=.5?e.PARTIAL:e.NONE;n.useEffect((()=>{if(!i.current)return()=>{};N();const t=new IntersectionObserver((t=>{t.forEach((t=>{const r=t.target,n=Number(r.dataset.slideIndex);R(t.intersectionRatio)===e.FULL?l.current.push(n):l.current=l.current.filter((e=>e!==n)),R(t.intersectionRatio)===e.PARTIAL?o.current.push(n):o.current=o.current.filter((e=>e!==n))})),l.current=[...new Set(l.current)].sort(((e,t)=>e-t)),o.current=[...new Set(o.current)].sort(((e,t)=>e-t)),N()}),{root:i.current,threshold:[0,.5,.9]});return c.current.forEach((({element:e})=>t.observe(e))),()=>t.disconnect()}),[i,N]);const _=e=>{if(!i.current)return;const r=e===t.PREV?E()-1:v()+1,n=c.current[r];let s=0;n&&(s=e===t.PREV?n.element.offsetLeft-i.current.offsetLeft-i.current.clientWidth+n.element.clientWidth:n.element.offsetLeft-i.current.offsetLeft,i.current.scrollTo({behavior:"smooth",left:s,top:0}))};return r.jsxs("div",{className:"slider",children:[r.jsx("div",{className:"slider__wrapper",role:"list",ref:i,onMouseDown:e=>{g({...L,clientX:e.clientX,scrollX:i.current?.scrollLeft??0}),f(!0)},onMouseMove:e=>{i.current&&d&&(Math.abs(L.clientX-e.clientX)>5&&b(!0),i.current.scrollLeft=L.scrollX+L.clientX-e.clientX)},onMouseUp:()=>{f(!1)},onClickCapture:e=>{h&&(e.stopPropagation(),e.preventDefault()),b(!1)},children:n.Children.map(s,((t,n)=>r.jsx("div",{className:"slider__wrapper__slide",role:"listitem","data-slide-index":n,ref:t=>{t&&((t,r)=>{c.current[r]={element:t,visibility:e.NONE}})(t,n)},children:t},n)))}),r.jsx("button",{"aria-label":"Previous slide",type:"button",onClick:()=>_(t.PREV),ref:m,className:"slider__button slider__button--prev button button--ghost button--clean button--has-icon hidden",children:r.jsx("i",{className:"icon-chevron-left"})}),r.jsx("button",{"aria-label":"Next slide",type:"button",onClick:()=>_(t.NEXT),ref:p,className:"slider__button slider__button--next button button--ghost button--clean button--has-icon hidden",children:r.jsx("i",{className:"icon-chevron-right"})})]})};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/src/src/Slider.tsx"],"sourcesContent":[null],"names":["Visbility","NavigationDirection","children","slides","useRef","wrapper","visibleSlideIndices","partiallyVisibleSlideIndices","isScrollable","setIsScrollable","useState","isDragging","setIsDragging","isBlockingClicks","setIsBlockingClicks","mousePosition","setMousePosition","clientX","scrollX","arrowPrevRef","arrowNextRef","useEffect","currentWrapper","current","checkScrollable","classList","toggle","scrollWidth","clientWidth","window","addEventListener","removeEventListener","onDocumentMouseUp","event","stopPropagation","preventDefault","document","getFirstVisibleSlideIndex","getLastVisibleSlideIndex","length","setControlsVisibility","useCallback","lastSlideFullyVisible","moreContentAvailable","previousContentAvailable","ariaHidden","String","getVisibilityByIntersectionRatio","intersectionRatio","FULL","PARTIAL","NONE","intersectionObserver","IntersectionObserver","entries","forEach","entry","target","index","Number","dataset","slideIndex","push","filter","Set","sort","a","b","root","threshold","element","observe","disconnect","navigate","direction","targetSlideIndex","PREV","targetSlide","scrollLeft","offsetLeft","scrollTo","behavior","left","top","_jsxs","jsxs","className","_jsx","role","ref","onMouseDown","onMouseMove","Math","abs","onMouseUp","onClickCapture","Children","map","child","node","visibility","addSlide","type","onClick","NEXT"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Config } from '@jest/types';
2
+ declare const config: Partial<Config.ConfigGlobals>;
3
+ export default config;
@@ -0,0 +1,4 @@
1
+ import type React from 'react';
2
+ import type { PropsWithChildren } from 'react';
3
+ import './Slider.scss';
4
+ export declare const Slider: React.FC<PropsWithChildren>;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,2 @@
1
+ import { Slider } from './Slider';
2
+ export default Slider;
@@ -0,0 +1,2 @@
1
+ .slider{position:relative}.slider__button{bottom:0;display:none;height:48px;margin:auto 0;position:absolute;top:0;width:48px}@media (hover:hover){.slider__button{display:flex}}.slider__button--next{right:-8px}@media screen and (min-width:768px){.slider__button--next{right:0}}.slider__button--prev{left:-8px}@media screen and (min-width:768px){.slider__button--prev{left:0}}.slider__wrapper{-ms-overflow-style:none;display:flex;flex-direction:row;margin:16px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;scrollbar-width:none}.slider__wrapper::-webkit-scrollbar{display:none}@media screen and (min-width:768px){.slider__wrapper{margin:8px}}@media (hover:hover){.slider__wrapper{scroll-snap-type:none}}.slider__wrapper.is-scrollable{cursor:move}.slider__wrapper.is-dragging{cursor:grabbing;user-select:none}.slider__wrapper:not(.is-dragging){scroll-behavior:smooth}.slider__wrapper__slide{margin-right:8px;scroll-snap-align:start}@media screen and (min-width:768px){.slider__wrapper__slide{margin-left:0;margin-right:16px}}.slider__wrapper__slide:last-child{margin-right:0}
2
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["Slider.scss"],"names":[],"mappings":"AAAA,QACE,iBACF,CACA,gBAEE,QAAS,CAKT,YAAa,CAHb,WAAY,CADZ,aAAc,CAGd,iBAAkB,CALlB,KAAM,CAIN,UAGF,CACA,qBACE,gBACE,YACF,CACF,CACA,sBACE,UACF,CACA,oCACE,sBACE,OACF,CACF,CACA,sBACE,SACF,CACA,oCACE,sBACE,MACF,CACF,CACA,iBAEE,uBAAwB,CAExB,YAAa,CACb,kBAAmB,CAEnB,WAAY,CADZ,eAAgB,CALhB,6BAA8B,CAO9B,4BAA6B,CAL7B,oBAMF,CACA,oCACE,YACF,CACA,oCACE,iBACE,UACF,CACF,CACA,qBACE,iBACE,qBACF,CACF,CACA,+BACE,WACF,CACA,6BACE,eAAgB,CAChB,gBACF,CACA,mCACE,sBACF,CACA,wBAEE,gBAAiB,CADjB,uBAEF,CACA,oCACE,wBACE,aAAc,CACd,iBACF,CACF,CACA,mCACE,cACF","file":"index.css","sourcesContent":[".slider {\n position: relative;\n}\n.slider__button {\n top: 0;\n bottom: 0;\n margin: auto 0;\n height: 48px;\n width: 48px;\n position: absolute;\n display: none;\n}\n@media (hover: hover) {\n .slider__button {\n display: flex;\n }\n}\n.slider__button--next {\n right: -8px;\n}\n@media screen and (min-width: 768px) {\n .slider__button--next {\n right: 0;\n }\n}\n.slider__button--prev {\n left: -8px;\n}\n@media screen and (min-width: 768px) {\n .slider__button--prev {\n left: 0;\n }\n}\n.slider__wrapper {\n overscroll-behavior-x: contain;\n -ms-overflow-style: none;\n scrollbar-width: none;\n display: flex;\n flex-direction: row;\n overflow-x: auto;\n margin: 16px;\n scroll-snap-type: x proximity;\n}\n.slider__wrapper::-webkit-scrollbar {\n display: none;\n}\n@media screen and (min-width: 768px) {\n .slider__wrapper {\n margin: 8px;\n }\n}\n@media (hover: hover) {\n .slider__wrapper {\n scroll-snap-type: initial;\n }\n}\n.slider__wrapper.is-scrollable {\n cursor: move;\n}\n.slider__wrapper.is-dragging {\n cursor: grabbing;\n user-select: none;\n}\n.slider__wrapper:not(.is-dragging) {\n scroll-behavior: smooth;\n}\n.slider__wrapper__slide {\n scroll-snap-align: start;\n margin-right: 8px;\n}\n@media screen and (min-width: 768px) {\n .slider__wrapper__slide {\n margin-left: 0;\n margin-right: 16px;\n }\n}\n.slider__wrapper__slide:last-child {\n margin-right: 0;\n}"]}
@@ -0,0 +1,2 @@
1
+ import{jsxs as e,jsx as t}from"react/jsx-runtime";import{useRef as r,useState as n,useEffect as c,useCallback as o,Children as s}from"react";var i,l;!function(e){e[e.FULL=0]="FULL",e[e.PARTIAL=1]="PARTIAL",e[e.NONE=2]="NONE"}(i||(i={})),function(e){e[e.PREV=0]="PREV",e[e.NEXT=1]="NEXT"}(l||(l={}));const u=({children:u})=>{const a=r([]),d=r(null),f=r([]),h=r([]),[m,L]=n(!1),[b,g]=n(!1),[p,v]=n(!1),[N,E]=n({clientX:0,scrollX:0}),_=r(null),X=r(null);c((()=>{const e=d.current;if(!e)return()=>{};const t=()=>L(e.classList.toggle("is-scrollable",e.scrollWidth>e.clientWidth));return window?.addEventListener("resize",t),t(),()=>{window?.removeEventListener("resize",t)}}),[d]),c((()=>{d.current?.classList.toggle("is-scrollable",m)}),[m]),c((()=>{d.current?.classList.toggle("is-dragging",b);const e=e=>{e.stopPropagation(),e.preventDefault(),v(!1),g(!1)};return b&&document?.addEventListener("mouseup",e),()=>{document?.removeEventListener("mouseup",e)}}),[b]);const P=()=>f.current[0]??h.current[0]??-1,R=()=>f.current[f.current.length-1]??h.current[h.current.length-1]??-1,w=o((()=>{const e=R()+1===a.current.length,t=m&&!1===e,r=P()>0&&m;X.current&&_.current&&(X.current.classList.toggle("hidden",!1===t),X.current.ariaHidden=String(!1===t),_.current.classList.toggle("hidden",!1===r),_.current.ariaHidden=String(!1===r))}),[m]),A=e=>e>=.9?i.FULL:e>=.5?i.PARTIAL:i.NONE;c((()=>{if(!d.current)return()=>{};w();const e=new IntersectionObserver((e=>{e.forEach((e=>{const t=e.target,r=Number(t.dataset.slideIndex);A(e.intersectionRatio)===i.FULL?f.current.push(r):f.current=f.current.filter((e=>e!==r)),A(e.intersectionRatio)===i.PARTIAL?h.current.push(r):h.current=h.current.filter((e=>e!==r))})),f.current=[...new Set(f.current)].sort(((e,t)=>e-t)),h.current=[...new Set(h.current)].sort(((e,t)=>e-t)),w()}),{root:d.current,threshold:[0,.5,.9]});return a.current.forEach((({element:t})=>e.observe(t))),()=>e.disconnect()}),[d,w]);const T=e=>{if(!d.current)return;const t=e===l.PREV?P()-1:R()+1,r=a.current[t];let n=0;r&&(n=e===l.PREV?r.element.offsetLeft-d.current.offsetLeft-d.current.clientWidth+r.element.clientWidth:r.element.offsetLeft-d.current.offsetLeft,d.current.scrollTo({behavior:"smooth",left:n,top:0}))};return e("div",{className:"slider",children:[t("div",{className:"slider__wrapper",role:"list",ref:d,onMouseDown:e=>{E({...N,clientX:e.clientX,scrollX:d.current?.scrollLeft??0}),g(!0)},onMouseMove:e=>{d.current&&b&&(Math.abs(N.clientX-e.clientX)>5&&v(!0),d.current.scrollLeft=N.scrollX+N.clientX-e.clientX)},onMouseUp:()=>{g(!1)},onClickCapture:e=>{p&&(e.stopPropagation(),e.preventDefault()),v(!1)},children:s.map(u,((e,r)=>t("div",{className:"slider__wrapper__slide",role:"listitem","data-slide-index":r,ref:e=>{e&&((e,t)=>{a.current[t]={element:e,visibility:i.NONE}})(e,r)},children:e},r)))}),t("button",{"aria-label":"Previous slide",type:"button",onClick:()=>T(l.PREV),ref:_,className:"slider__button slider__button--prev button button--ghost button--clean button--has-icon hidden",children:t("i",{className:"icon-chevron-left"})}),t("button",{"aria-label":"Next slide",type:"button",onClick:()=>T(l.NEXT),ref:X,className:"slider__button slider__button--next button button--ghost button--clean button--has-icon hidden",children:t("i",{className:"icon-chevron-right"})})]})};export{u as default};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/src/src/Slider.tsx"],"sourcesContent":[null],"names":["Visbility","NavigationDirection","Slider","children","slides","useRef","wrapper","visibleSlideIndices","partiallyVisibleSlideIndices","isScrollable","setIsScrollable","useState","isDragging","setIsDragging","isBlockingClicks","setIsBlockingClicks","mousePosition","setMousePosition","clientX","scrollX","arrowPrevRef","arrowNextRef","useEffect","currentWrapper","current","checkScrollable","classList","toggle","scrollWidth","clientWidth","window","addEventListener","removeEventListener","onDocumentMouseUp","event","stopPropagation","preventDefault","document","getFirstVisibleSlideIndex","getLastVisibleSlideIndex","length","setControlsVisibility","useCallback","lastSlideFullyVisible","moreContentAvailable","previousContentAvailable","ariaHidden","String","getVisibilityByIntersectionRatio","intersectionRatio","FULL","PARTIAL","NONE","intersectionObserver","IntersectionObserver","entries","forEach","entry","target","index","Number","dataset","slideIndex","push","filter","Set","sort","a","b","root","threshold","element","observe","disconnect","navigate","direction","targetSlideIndex","PREV","targetSlide","scrollLeft","offsetLeft","scrollTo","behavior","left","top","_jsxs","className","_jsx","role","ref","onMouseDown","onMouseMove","Math","abs","onMouseUp","onClickCapture","Children","map","child","node","visibility","addSlide","type","onClick","NEXT"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Config } from '@jest/types';
2
+ declare const config: Partial<Config.ConfigGlobals>;
3
+ export default config;
@@ -0,0 +1,4 @@
1
+ import type React from 'react';
2
+ import type { PropsWithChildren } from 'react';
3
+ import './Slider.scss';
4
+ export declare const Slider: React.FC<PropsWithChildren>;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,2 @@
1
+ import { Slider } from './Slider';
2
+ export default Slider;
package/jest.config.ts ADDED
@@ -0,0 +1,54 @@
1
+ // For a detailed explanation regarding each configuration property, visit:
2
+ // https://jestjs.io/docs/en/configuration.html
3
+
4
+ import type { Config } from '@jest/types';
5
+
6
+ const config: Partial<Config.ConfigGlobals> = {
7
+ // Indicates whether the coverage information should be collected while executing the test
8
+ collectCoverage: false,
9
+
10
+ testEnvironment: 'jsdom',
11
+
12
+ // An array of glob patterns indicating a set of files for which coverage information should be collected
13
+ collectCoverageFrom: [
14
+ 'src/**/*.{js,ts,tsx}',
15
+ ],
16
+
17
+ // The directory where Jest should output its coverage files
18
+ coverageDirectory: 'coverage',
19
+
20
+ // A list of reporter names that Jest uses when writing coverage reports
21
+ coverageReporters: [
22
+ 'cobertura',
23
+ 'lcov',
24
+ 'text',
25
+ ],
26
+
27
+ coverageThreshold: {
28
+ global: {
29
+ branches: 80,
30
+ functions: 90,
31
+ lines: 90,
32
+ statements: 90,
33
+ },
34
+ },
35
+
36
+ // An array of directory names to be searched recursively up from the requiring module's location
37
+ moduleDirectories: [
38
+ 'src',
39
+ 'node_modules',
40
+ ],
41
+
42
+ // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
43
+ moduleNameMapper: {
44
+ '^.+\\.(css|scss|less|scss)$': '<rootDir>src/__mocks__/fileMock.js',
45
+ '^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>src/__mocks__/fileMock.js',
46
+ } as unknown as Config.ProjectConfig['moduleNameMapper'],
47
+
48
+ // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
49
+ testPathIgnorePatterns: [
50
+ '/node_modules/',
51
+ ],
52
+ };
53
+
54
+ export default config;
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@yoursurprise/slider",
3
+ "version": "0.0.1",
4
+ "description": "Basic React slider using modern Javascript and CSS",
5
+ "main": "dist/cjs/index.js",
6
+ "module": "dist/esm/index.js",
7
+ "peerDependencies": {
8
+ "react": "^18.2.0"
9
+ },
10
+ "devDependencies": {
11
+ "@babel/preset-env": "^7.19.4",
12
+ "@babel/preset-react": "^7.18.6",
13
+ "@babel/preset-typescript": "^7.18.6",
14
+ "@rollup/plugin-commonjs": "^23.0.2",
15
+ "@rollup/plugin-node-resolve": "^15.0.1",
16
+ "@rollup/plugin-typescript": "^9.0.2",
17
+ "@testing-library/jest-dom": "^5.16.5",
18
+ "@testing-library/react": "^13.4.0",
19
+ "@testing-library/user-event": "^14.4.3",
20
+ "@types/jest": "^29.2.0",
21
+ "@types/react": "^18.0.21",
22
+ "@typescript-eslint/eslint-plugin": "^5.39.0",
23
+ "@typescript-eslint/parser": "^5.39.0",
24
+ "babel-jest": "^29.2.2",
25
+ "eslint": "^8.26.0",
26
+ "eslint-config-airbnb": "^19.0.4",
27
+ "eslint-config-airbnb-typescript": "^17.0.0",
28
+ "eslint-plugin-import": "^2.26.0",
29
+ "eslint-plugin-jsx-a11y": "^6.6.1",
30
+ "eslint-plugin-react": "^7.31.10",
31
+ "eslint-plugin-react-hooks": "^4.6.0",
32
+ "eslint-plugin-testing-library": "^5.9.1",
33
+ "jest": "^29.2.2",
34
+ "jest-environment-jsdom": "^29.2.2",
35
+ "npm": "^8.19.3",
36
+ "postcss": "^8.4.18",
37
+ "react": "^18.2.0",
38
+ "react-dom": "*",
39
+ "react-test-renderer": "^18.2.0",
40
+ "rollup": "^3.2.5",
41
+ "rollup-plugin-peer-deps-external": "^2.2.4",
42
+ "rollup-plugin-postcss": "^4.0.2",
43
+ "rollup-plugin-terser": "^7.0.2",
44
+ "sass": "^1.56.0",
45
+ "ts-node": "^10.9.1",
46
+ "typescript": "^4.8.4"
47
+ },
48
+ "scripts": {
49
+ "test": "jest",
50
+ "lint": "eslint src/",
51
+ "build": "rm -rf ./dist && rollup -c --bundleConfigAsCjs",
52
+ "watch": "rollup -c -w --bundleConfigAsCjs"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+ssh://git@github.com/YourSurpriseCom/slider.git"
57
+ },
58
+ "keywords": [
59
+ "slider",
60
+ "scroll",
61
+ "snap",
62
+ "touch"
63
+ ],
64
+ "author": "YourSurprise",
65
+ "license": "ISC",
66
+ "bugs": {
67
+ "url": "https://github.com/YourSurpriseCom/slider/issues"
68
+ },
69
+ "homepage": "https://github.com/YourSurpriseCom/slider#readme",
70
+ "dependencies": {}
71
+ }
@@ -0,0 +1,38 @@
1
+ import resolve from '@rollup/plugin-node-resolve';
2
+ import commonjs from '@rollup/plugin-commonjs';
3
+ import typescript from '@rollup/plugin-typescript';
4
+ import { terser } from 'rollup-plugin-terser';
5
+ import external from 'rollup-plugin-peer-deps-external';
6
+ import postcss from 'rollup-plugin-postcss';
7
+
8
+ const packageJson = require('./package.json');
9
+
10
+ export default {
11
+ input: 'src/index.ts',
12
+ output: [
13
+ {
14
+ file: packageJson.main,
15
+ format: 'cjs',
16
+ sourcemap: true,
17
+ },
18
+ {
19
+ file: packageJson.module,
20
+ format: 'esm',
21
+ sourcemap: true,
22
+ },
23
+ ],
24
+ plugins: [
25
+ external(),
26
+ resolve(),
27
+ commonjs(),
28
+ typescript({
29
+ tsconfig: './tsconfig.json' }),
30
+ postcss({
31
+ extract: 'index.css',
32
+ minimize: true,
33
+ sourceMap: true,
34
+ }),
35
+ terser(),
36
+ ],
37
+ };
38
+
@@ -0,0 +1,86 @@
1
+ .slider {
2
+ position: relative;
3
+
4
+ &__button {
5
+ top: 0;
6
+ bottom: 0;
7
+ margin: auto 0;
8
+ height: 48px;
9
+ width: 48px;
10
+ position: absolute;
11
+ display: none;
12
+
13
+ @media (hover: hover) {
14
+ display: flex;
15
+ }
16
+
17
+ &--next {
18
+ right: -8px;
19
+
20
+ @media screen and (min-width: 768px) {
21
+ right: 0;
22
+ }
23
+ }
24
+
25
+ &--prev {
26
+ left: -8px;
27
+
28
+ @media screen and (min-width: 768px) {
29
+ left: 0;
30
+ }
31
+ }
32
+ }
33
+
34
+ &__wrapper {
35
+ // Prevent the browser from going back in history when there is no scroll area left
36
+ overscroll-behavior-x: contain;
37
+ // Hide the scrollbars
38
+ -ms-overflow-style: none; // for Internet Explorer, Edge
39
+ scrollbar-width: none; // for Firefox
40
+
41
+ &::-webkit-scrollbar {
42
+ display: none; // for Chrome, Safari, and Opera
43
+ }
44
+
45
+ display: flex;
46
+ flex-direction: row;
47
+ overflow-x: auto;
48
+ margin: 16px;
49
+ scroll-snap-type: x proximity;
50
+
51
+ @media screen and (min-width: 768px) {
52
+ margin: 8px;
53
+ }
54
+
55
+ @media (hover: hover) {
56
+ scroll-snap-type: initial;
57
+ }
58
+
59
+ &.is-scrollable {
60
+ cursor: move;
61
+ }
62
+
63
+ &.is-dragging {
64
+ cursor: grabbing;
65
+ user-select: none;
66
+ }
67
+
68
+ &:not(.is-dragging) {
69
+ scroll-behavior: smooth;
70
+ }
71
+
72
+ &__slide {
73
+ scroll-snap-align: start;
74
+ margin-right: 8px;
75
+
76
+ @media screen and (min-width: 768px) {
77
+ margin-left: 0;
78
+ margin-right: 16px;
79
+ }
80
+
81
+ &:last-child {
82
+ margin-right: 0;
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,248 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import '@testing-library/jest-dom';
4
+ import { Slider } from './Slider';
5
+
6
+ const renderSliderWithDimensions = (clientWidth = 1000, scrollWidth = 2000, scrollLeft = 0) => {
7
+ Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: clientWidth });
8
+ Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: scrollWidth });
9
+ Object.defineProperty(HTMLElement.prototype, 'scrollLeft', { configurable: true, value: scrollLeft, writable: true });
10
+
11
+ render(<Slider>
12
+ <span key={1}/>
13
+ <span key={2}/>
14
+ <span key={3}/>
15
+ <span key={4}/>
16
+ </Slider>);
17
+ };
18
+
19
+ describe('UpsellSlider', () => {
20
+ let observeSpy: jest.Mock;
21
+ let disconnectSpy: jest.Mock;
22
+ let scrollToSpy: jest.Mock;
23
+
24
+ beforeEach(() => {
25
+ const mockIntersectionObserver = jest.fn();
26
+ observeSpy = jest.fn();
27
+ disconnectSpy = jest.fn();
28
+ scrollToSpy = jest.fn();
29
+
30
+ mockIntersectionObserver.mockReturnValue({
31
+ disconnect: disconnectSpy,
32
+ observe: observeSpy,
33
+ unobserve: () => jest.fn(),
34
+ });
35
+ global.IntersectionObserver = mockIntersectionObserver;
36
+ Element.prototype.scrollTo = scrollToSpy;
37
+ });
38
+
39
+ afterEach(() => {
40
+ Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 0 });
41
+ Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 0 });
42
+ Object.defineProperty(HTMLElement.prototype, 'scrollLeft', { configurable: true, value: 0, writable: true });
43
+ });
44
+
45
+ it('renders children', () => {
46
+ render(<Slider><span data-testid="child"/></Slider>);
47
+
48
+ expect(screen.getByTestId('child')).toBeInTheDocument();
49
+ });
50
+
51
+ it('keeps track of the visibility of children', () => {
52
+ const children = [
53
+ <span key={ 1 } data-testid="child-1"/>,
54
+ <span key={ 2 } data-testid="child-2"/>,
55
+ <span key={ 3 } data-testid="child-3"/>,
56
+ <span key={ 4 } data-testid="child-4"/>,
57
+ ];
58
+
59
+ render(<Slider>{ children }</Slider>);
60
+ expect(observeSpy).toHaveBeenCalledTimes(children.length);
61
+ });
62
+
63
+ it('sets the container scrollable if the scroll area exceeds the container width', () => {
64
+ renderSliderWithDimensions(500, 1000);
65
+
66
+ expect(screen.getByRole('list')).toHaveClass('is-scrollable');
67
+ });
68
+
69
+ it('does not set the container scrollable if the scroll area does not exceed the container width', () => {
70
+ renderSliderWithDimensions(500, 400);
71
+
72
+ expect(screen.getByRole('list')).not.toHaveClass('is-scrollable');
73
+ });
74
+
75
+ it('disconnects the intersection observer on re-render', () => {
76
+ const element = <Slider><span>sup</span></Slider>;
77
+
78
+ const { rerender } = render(element);
79
+
80
+ rerender(element);
81
+
82
+ expect(observeSpy).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ describe('controls', () => {
86
+ it('sets controls visibility initially', () => {
87
+ renderSliderWithDimensions();
88
+
89
+ const nextButton = screen.getByLabelText('Next slide');
90
+ const prevButton = screen.getByLabelText('Previous slide');
91
+
92
+ expect(prevButton.ariaHidden).toBe('true');
93
+ expect(nextButton.ariaHidden).toBe('false');
94
+ });
95
+
96
+ it('allows scrolling by dragging with the mouse', () => {
97
+ renderSliderWithDimensions();
98
+
99
+ const scrollElement = screen.getByRole('list');
100
+
101
+ // eslint-disable-next-line testing-library/prefer-user-event
102
+ fireEvent.mouseDown(scrollElement);
103
+ // eslint-disable-next-line testing-library/prefer-user-event
104
+ fireEvent.mouseMove(scrollElement, { clientX: 100, clientY: 0 });
105
+ // eslint-disable-next-line testing-library/prefer-user-event
106
+ fireEvent.mouseUp(scrollElement);
107
+
108
+ expect(scrollElement.scrollLeft).toBe(-100);
109
+ });
110
+
111
+ it('registers click when not scrolling', async () => {
112
+ const clickSpy = jest.fn();
113
+
114
+ render(<Slider>
115
+ <span data-testid="1" onClick={clickSpy}/>
116
+ </Slider>);
117
+
118
+ const scrollElement = screen.getByRole('list');
119
+
120
+ // eslint-disable-next-line testing-library/prefer-user-event
121
+ fireEvent.mouseDown(scrollElement);
122
+ // eslint-disable-next-line testing-library/prefer-user-event
123
+ fireEvent.mouseMove(scrollElement, { clientX: 100, clientY: 0 });
124
+ // eslint-disable-next-line testing-library/prefer-user-event
125
+ fireEvent.mouseUp(scrollElement);
126
+
127
+ // This click is normally triggered when releasing the mouse after scrolling
128
+ await userEvent.click(scrollElement);
129
+
130
+ await userEvent.click(screen.getByTestId('1'));
131
+
132
+ expect(clickSpy).toHaveBeenCalled();
133
+ });
134
+ });
135
+
136
+ describe('sliding', () => {
137
+ let intersectionCallback: (entries: IntersectionObserverEntry[]) => void;
138
+
139
+ beforeEach(() => {
140
+ const mockIntersectionObserver = jest.fn();
141
+ intersectionCallback = jest.fn();
142
+
143
+ mockIntersectionObserver.mockImplementation((callback: (entries: IntersectionObserverEntry[]) => void) => {
144
+ intersectionCallback = callback;
145
+
146
+ return ({
147
+ disconnect: jest.fn(),
148
+ observe: jest.fn(),
149
+ unobserve: jest.fn(),
150
+ });
151
+ });
152
+
153
+ global.IntersectionObserver = mockIntersectionObserver;
154
+ });
155
+
156
+ it('scrolls to the next slide', async () => {
157
+ renderSliderWithDimensions();
158
+
159
+ const slides = screen.getAllByRole('listitem');
160
+ const nextButton = screen.getByLabelText('Next slide');
161
+
162
+ slides.forEach((child, i) => {
163
+ Object.defineProperty(child, 'clientWidth', { value: 100 * (i + 1) });
164
+ Object.defineProperty(child, 'offsetLeft', { value: 100 * (i + 1) });
165
+ });
166
+
167
+ intersectionCallback([
168
+ { intersectionRatio: 1, target: slides[0] } as unknown as IntersectionObserverEntry,
169
+ { intersectionRatio: 0.5, target: slides[1] } as unknown as IntersectionObserverEntry,
170
+ { intersectionRatio: 0, target: slides[2] } as unknown as IntersectionObserverEntry,
171
+ { intersectionRatio: 0, target: slides[3] } as unknown as IntersectionObserverEntry,
172
+ ]);
173
+
174
+ await userEvent.click(nextButton);
175
+
176
+ expect(scrollToSpy).toHaveBeenCalledWith({
177
+ behavior: 'smooth',
178
+ left: 200,
179
+ top: 0,
180
+ });
181
+ });
182
+
183
+ it('scrolls to the previous slide', async () => {
184
+ renderSliderWithDimensions();
185
+
186
+ const slides = screen.getAllByRole('listitem');
187
+ const prevButton = screen.getByLabelText('Previous slide');
188
+
189
+ slides.forEach((child, i) => {
190
+ Object.defineProperty(child, 'clientWidth', { value: 100 * (i + 1) });
191
+ Object.defineProperty(child, 'offsetLeft', { value: 100 * (i + 1) });
192
+ });
193
+
194
+ intersectionCallback([
195
+ { intersectionRatio: 0, target: slides[0] } as unknown as IntersectionObserverEntry,
196
+ { intersectionRatio: 0, target: slides[1] } as unknown as IntersectionObserverEntry,
197
+ { intersectionRatio: 1, target: slides[2] } as unknown as IntersectionObserverEntry,
198
+ { intersectionRatio: 0.5, target: slides[3] } as unknown as IntersectionObserverEntry,
199
+ ]);
200
+
201
+ await userEvent.click(prevButton);
202
+
203
+ expect(scrollToSpy).toHaveBeenCalledWith({
204
+ behavior: 'smooth',
205
+ left: -600,
206
+ top: 0,
207
+ });
208
+ });
209
+
210
+ it('updates controls visibility', () => {
211
+ Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 500 });
212
+ Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 1000 });
213
+ Object.defineProperty(HTMLElement.prototype, 'scrollLeft', { configurable: true, value: 0, writable: true });
214
+
215
+ render(<Slider>
216
+ <span key={1}/>
217
+ <span key={2}/>
218
+ <span key={3}/>
219
+ <span key={4}/>
220
+ </Slider>);
221
+
222
+ const slides = screen.getAllByRole('listitem');
223
+ const nextButton = screen.getByLabelText('Next slide');
224
+ const prevButton = screen.getByLabelText('Previous slide');
225
+
226
+ ([
227
+ // All slides are visible
228
+ [[1, 1, 1, 1], true, true],
229
+ // Only the first slide is not visible
230
+ [[0, 1, 1, 1], false, true],
231
+ // Only the last slide is not visible
232
+ [[1, 1, 1, 0], true, false],
233
+ ] as Array<[number[], boolean, boolean]>).forEach((expectations) => {
234
+ const [intersections, prevButtonHidden, nextButtonHidden] = expectations;
235
+
236
+ intersectionCallback([
237
+ { intersectionRatio: intersections[0], target: slides[0] } as unknown as IntersectionObserverEntry,
238
+ { intersectionRatio: intersections[1], target: slides[1] } as unknown as IntersectionObserverEntry,
239
+ { intersectionRatio: intersections[2], target: slides[2] } as unknown as IntersectionObserverEntry,
240
+ { intersectionRatio: intersections[3], target: slides[3] } as unknown as IntersectionObserverEntry,
241
+ ]);
242
+
243
+ expect(prevButton.ariaHidden).toBe(String(prevButtonHidden));
244
+ expect(nextButton.ariaHidden).toBe(String(nextButtonHidden));
245
+ });
246
+ });
247
+ });
248
+ });
package/src/Slider.tsx ADDED
@@ -0,0 +1,253 @@
1
+ import type React from 'react';
2
+ import type { MouseEvent as ReactMouseEvent, PropsWithChildren } from 'react';
3
+ import { useRef, useEffect, Children, useCallback, useState } from 'react';
4
+ import './Slider.scss';
5
+
6
+ enum Visbility {
7
+ FULL,
8
+ PARTIAL,
9
+ NONE,
10
+ }
11
+
12
+ enum NavigationDirection {
13
+ PREV,
14
+ NEXT,
15
+ }
16
+
17
+ interface SlideVisibilityEntry {
18
+ element: HTMLDivElement;
19
+ visibility: Visbility;
20
+ }
21
+
22
+ export const Slider: React.FC<PropsWithChildren> = ({ children }) => {
23
+ const slides = useRef<SlideVisibilityEntry[]>([]);
24
+ const wrapper = useRef<HTMLDivElement>(null);
25
+ const visibleSlideIndices = useRef<number[]>([]);
26
+ const partiallyVisibleSlideIndices = useRef<number[]>([]);
27
+
28
+ const [isScrollable, setIsScrollable] = useState<boolean>(false);
29
+ const [isDragging, setIsDragging] = useState<boolean>(false);
30
+ const [isBlockingClicks, setIsBlockingClicks] = useState<boolean>(false);
31
+ const [mousePosition, setMousePosition] = useState<{ clientX: number; scrollX: number }>({
32
+ clientX: 0,
33
+ scrollX: 0,
34
+ });
35
+
36
+ const arrowPrevRef = useRef<HTMLButtonElement>(null);
37
+ const arrowNextRef = useRef<HTMLButtonElement>(null);
38
+
39
+ useEffect(() => {
40
+ const currentWrapper = wrapper.current;
41
+
42
+ if (!currentWrapper) {
43
+ return () => {};
44
+ }
45
+
46
+ const checkScrollable = () => setIsScrollable(currentWrapper.classList.toggle('is-scrollable', currentWrapper.scrollWidth > currentWrapper.clientWidth));
47
+
48
+ window?.addEventListener('resize', checkScrollable);
49
+
50
+ checkScrollable();
51
+
52
+ return () => {
53
+ window?.removeEventListener('resize', checkScrollable);
54
+ };
55
+ }, [wrapper]);
56
+
57
+ useEffect(() => {
58
+ wrapper.current?.classList.toggle('is-scrollable', isScrollable);
59
+ }, [isScrollable]);
60
+
61
+ useEffect(() => {
62
+ wrapper.current?.classList.toggle('is-dragging', isDragging);
63
+
64
+ const onDocumentMouseUp = (event: MouseEvent) => {
65
+ event.stopPropagation();
66
+ event.preventDefault();
67
+
68
+ setIsBlockingClicks(false);
69
+ setIsDragging(false);
70
+ };
71
+
72
+ if (isDragging) {
73
+ document?.addEventListener('mouseup', onDocumentMouseUp);
74
+ }
75
+
76
+ return () => {
77
+ document?.removeEventListener('mouseup', onDocumentMouseUp);
78
+ };
79
+ }, [isDragging]);
80
+
81
+ const blockChildClickHandler = (event: ReactMouseEvent<HTMLDivElement>) => {
82
+ if (isBlockingClicks) {
83
+ event.stopPropagation();
84
+ event.preventDefault();
85
+ }
86
+
87
+ setIsBlockingClicks(false);
88
+ };
89
+
90
+ const mouseUpHandler = () => {
91
+ setIsDragging(false);
92
+ };
93
+
94
+ const mouseDownHandler = (event: ReactMouseEvent<HTMLDivElement>) => {
95
+ setMousePosition({
96
+ ...mousePosition,
97
+ clientX: event.clientX,
98
+ scrollX: wrapper.current?.scrollLeft ?? 0,
99
+ });
100
+
101
+ setIsDragging(true);
102
+ };
103
+
104
+ const mouseMoveHandler = (event: ReactMouseEvent<HTMLDivElement>) => {
105
+ if (!wrapper.current || !isDragging) {
106
+ return;
107
+ }
108
+
109
+ if (Math.abs(mousePosition.clientX - event.clientX) > 5) {
110
+ setIsBlockingClicks(true);
111
+ }
112
+
113
+ wrapper.current.scrollLeft = mousePosition.scrollX + mousePosition.clientX - event.clientX;
114
+ };
115
+
116
+ const addSlide = (node: HTMLDivElement, index: number) => {
117
+ slides.current[index] = {
118
+ element: node,
119
+ visibility: Visbility.NONE,
120
+ };
121
+ };
122
+
123
+ const getFirstVisibleSlideIndex = (): number => visibleSlideIndices.current[0] ?? partiallyVisibleSlideIndices.current[0] ?? -1;
124
+
125
+ const getLastVisibleSlideIndex = (): number => visibleSlideIndices.current[visibleSlideIndices.current.length - 1]
126
+ ?? partiallyVisibleSlideIndices.current[partiallyVisibleSlideIndices.current.length - 1] ?? -1;
127
+
128
+ const setControlsVisibility = useCallback(() => {
129
+ const lastSlideFullyVisible = getLastVisibleSlideIndex() + 1 === slides.current.length;
130
+ const moreContentAvailable = isScrollable && lastSlideFullyVisible === false;
131
+ const previousContentAvailable = getFirstVisibleSlideIndex() > 0 && isScrollable;
132
+
133
+ if (arrowNextRef.current && arrowPrevRef.current) {
134
+ arrowNextRef.current.classList.toggle('hidden', moreContentAvailable === false);
135
+ arrowNextRef.current.ariaHidden = String(moreContentAvailable === false);
136
+
137
+ arrowPrevRef.current.classList.toggle('hidden', previousContentAvailable === false);
138
+ arrowPrevRef.current.ariaHidden = String(previousContentAvailable === false);
139
+ }
140
+ }, [isScrollable]);
141
+
142
+ const getVisibilityByIntersectionRatio = (intersectionRatio: number) => {
143
+ if (intersectionRatio >= 0.9) {
144
+ return Visbility.FULL;
145
+ }
146
+
147
+ if (intersectionRatio >= 0.5) {
148
+ return Visbility.PARTIAL;
149
+ }
150
+
151
+ return Visbility.NONE;
152
+ };
153
+
154
+ useEffect(() => {
155
+ if (!wrapper.current) {
156
+ return () => {};
157
+ }
158
+
159
+ setControlsVisibility();
160
+
161
+ const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
162
+ entries.forEach((entry: IntersectionObserverEntry) => {
163
+ const target = entry.target as HTMLDivElement;
164
+ const index = Number(target.dataset.slideIndex);
165
+
166
+ if (getVisibilityByIntersectionRatio(entry.intersectionRatio) === Visbility.FULL) {
167
+ visibleSlideIndices.current.push(index);
168
+ } else {
169
+ visibleSlideIndices.current = visibleSlideIndices.current.filter((slideIndex) => slideIndex !== index);
170
+ }
171
+
172
+ if (getVisibilityByIntersectionRatio(entry.intersectionRatio) === Visbility.PARTIAL) {
173
+ partiallyVisibleSlideIndices.current.push(index);
174
+ } else {
175
+ partiallyVisibleSlideIndices.current = partiallyVisibleSlideIndices.current.filter((slideIndex) => slideIndex !== index);
176
+ }
177
+ });
178
+
179
+ // Make sure there are no duplicate visible slides, then sort to retain proper order
180
+ visibleSlideIndices.current = [...new Set(visibleSlideIndices.current)].sort((a, b) => a - b);
181
+ partiallyVisibleSlideIndices.current = [...new Set(partiallyVisibleSlideIndices.current)].sort((a, b) => a - b);
182
+
183
+ setControlsVisibility();
184
+ };
185
+
186
+ const intersectionObserver = new IntersectionObserver(intersectionCallback, {
187
+ root: wrapper.current,
188
+ threshold: [0, 0.5, 0.9],
189
+ });
190
+
191
+ slides.current.forEach(({ element }) => intersectionObserver.observe(element));
192
+
193
+ return () => intersectionObserver.disconnect();
194
+ }, [wrapper, setControlsVisibility]);
195
+
196
+ const navigate = (direction: NavigationDirection) => {
197
+ if (!wrapper.current) {
198
+ return;
199
+ }
200
+
201
+ const targetSlideIndex = direction === NavigationDirection.PREV ? getFirstVisibleSlideIndex() - 1 : getLastVisibleSlideIndex() + 1;
202
+
203
+ const targetSlide = slides.current[targetSlideIndex];
204
+ let scrollLeft = 0;
205
+
206
+ if (!targetSlide) {
207
+ return;
208
+ }
209
+
210
+ if (direction === NavigationDirection.PREV) {
211
+ scrollLeft = targetSlide.element.offsetLeft - wrapper.current.offsetLeft - wrapper.current.clientWidth + targetSlide.element.clientWidth;
212
+ } else {
213
+ scrollLeft = targetSlide.element.offsetLeft - wrapper.current.offsetLeft;
214
+ }
215
+
216
+ wrapper.current.scrollTo({ behavior: 'smooth', left: scrollLeft, top: 0 });
217
+ };
218
+
219
+ return (
220
+ <div className="slider">
221
+ <div className="slider__wrapper" role="list" ref={wrapper}
222
+ onMouseDown={mouseDownHandler}
223
+ onMouseMove={mouseMoveHandler}
224
+ onMouseUp={mouseUpHandler}
225
+ onClickCapture={blockChildClickHandler}
226
+ >
227
+ {Children.map(children, (child, index: number) => (
228
+ <div className="slider__wrapper__slide" role="listitem" key={index} data-slide-index={index} ref={(node) => { if (node) { addSlide(node, index); } }}>
229
+ {child}
230
+ </div>
231
+ ))}
232
+ </div>
233
+ <button
234
+ aria-label="Previous slide"
235
+ type="button"
236
+ onClick={() => navigate(NavigationDirection.PREV)}
237
+ ref={arrowPrevRef}
238
+ className="slider__button slider__button--prev button button--ghost button--clean button--has-icon hidden"
239
+ >
240
+ <i className="icon-chevron-left"/>
241
+ </button>
242
+ <button
243
+ aria-label="Next slide"
244
+ type="button"
245
+ onClick={() => navigate(NavigationDirection.NEXT)}
246
+ ref={arrowNextRef}
247
+ className="slider__button slider__button--next button button--ghost button--clean button--has-icon hidden"
248
+ >
249
+ <i className="icon-chevron-right"/>
250
+ </button>
251
+ </div>
252
+ );
253
+ };
@@ -0,0 +1,11 @@
1
+ const target = {};
2
+
3
+ const handler = {
4
+ get: function (t, prop) {
5
+ return prop;
6
+ },
7
+ };
8
+
9
+ const proxy = new Proxy(target, handler);
10
+
11
+ export default proxy;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Slider } from './Slider';
2
+
3
+ export default Slider;
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "sourceRoot": "./src",
4
+ "outDir": "dist",
5
+ "target": "ESNext",
6
+ "jsx": "react-jsx",
7
+ "moduleResolution": "node",
8
+ "module": "ESNext",
9
+ "noImplicitAny": true,
10
+ "removeComments": true,
11
+ "preserveConstEnums": true,
12
+ "sourceMap": true,
13
+ "allowSyntheticDefaultImports": true,
14
+ "emitDeclarationOnly": true,
15
+ "declaration": true
16
+ }
17
+ }