@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 +1 -0
- package/.eslintrc.js +45 -0
- package/.github/workflows/lint.yml +18 -0
- package/.github/workflows/publish.yml +23 -0
- package/babel.config.js +7 -0
- package/dist/cjs/index.css +2 -0
- package/dist/cjs/index.css.map +1 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/jest.config.d.ts +3 -0
- package/dist/cjs/src/Slider.d.ts +4 -0
- package/dist/cjs/src/Slider.test.d.ts +1 -0
- package/dist/cjs/src/index.d.ts +2 -0
- package/dist/esm/index.css +2 -0
- package/dist/esm/index.css.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/jest.config.d.ts +3 -0
- package/dist/esm/src/Slider.d.ts +4 -0
- package/dist/esm/src/Slider.test.d.ts +1 -0
- package/dist/esm/src/index.d.ts +2 -0
- package/jest.config.ts +54 -0
- package/package.json +71 -0
- package/rollup.config.js +38 -0
- package/src/Slider.scss +86 -0
- package/src/Slider.test.tsx +248 -0
- package/src/Slider.tsx +253 -0
- package/src/__mocks__/fileMock.js +11 -0
- package/src/index.ts +3 -0
- package/tsconfig.json +17 -0
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 }}
|
package/babel.config.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -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":"6IAKA,IAAKA,EAMAC,GANL,SAAKD,GACDA,EAAAA,EAAA,KAAA,GAAA,OACAA,EAAAA,EAAA,QAAA,GAAA,UACAA,EAAAA,EAAA,KAAA,GAAA,MACH,CAJD,CAAKA,IAAAA,EAIJ,CAAA,IAED,SAAKC,GACDA,EAAAA,EAAA,KAAA,GAAA,OACAA,EAAAA,EAAA,KAAA,GAAA,MACH,CAHD,CAAKA,IAAAA,EAGJ,CAAA,UAOYC,EAAsC,EAAGC,eAClD,MAAMC,EAASC,EAA+B,IACxCC,EAAUD,EAAuB,MACjCE,EAAsBF,EAAiB,IACvCG,EAA+BH,EAAiB,KAE/CI,EAAcC,GAAmBC,GAAkB,IACnDC,EAAYC,GAAiBF,GAAkB,IAC/CG,EAAkBC,GAAuBJ,GAAkB,IAC3DK,EAAeC,GAAoBN,EAA+C,CACrFO,QAAS,EACTC,QAAS,IAGPC,EAAef,EAA0B,MACzCgB,EAAehB,EAA0B,MAE/CiB,GAAU,KACN,MAAMC,EAAiBjB,EAAQkB,QAE/B,IAAKD,EACD,MAAO,OAGX,MAAME,EAAkB,IAAMf,EAAgBa,EAAeG,UAAUC,OAAO,gBAAiBJ,EAAeK,YAAcL,EAAeM,cAM3I,OAJAC,QAAQC,iBAAiB,SAAUN,GAEnCA,IAEO,KACHK,QAAQE,oBAAoB,SAAUP,EAAgB,CACzD,GACF,CAACnB,IAEJgB,GAAU,KACNhB,EAAQkB,SAASE,UAAUC,OAAO,gBAAiBlB,EAAa,GACjE,CAACA,IAEJa,GAAU,KACNhB,EAAQkB,SAASE,UAAUC,OAAO,cAAef,GAEjD,MAAMqB,EAAqBC,IACvBA,EAAMC,kBACND,EAAME,iBAENrB,GAAoB,GACpBF,GAAc,EAAM,EAOxB,OAJID,GACAyB,UAAUN,iBAAiB,UAAWE,GAGnC,KACHI,UAAUL,oBAAoB,UAAWC,EAAkB,CAC9D,GACF,CAACrB,IAEJ,MA0CM0B,EAA4B,IAAc/B,EAAoBiB,QAAQ,IAAMhB,EAA6BgB,QAAQ,KAAO,EAExHe,EAA2B,IAAchC,EAAoBiB,QAAQjB,EAAoBiB,QAAQgB,OAAS,IACzGhC,EAA6BgB,QAAQhB,EAA6BgB,QAAQgB,OAAS,KAAO,EAE3FC,EAAwBC,GAAY,KACtC,MAAMC,EAAwBJ,IAA6B,IAAMnC,EAAOoB,QAAQgB,OAC1EI,EAAuBnC,IAA0C,IAA1BkC,EACvCE,EAA2BP,IAA8B,GAAK7B,EAEhEY,EAAaG,SAAWJ,EAAaI,UACrCH,EAAaG,QAAQE,UAAUC,OAAO,UAAmC,IAAzBiB,GAChDvB,EAAaG,QAAQsB,WAAaC,QAAgC,IAAzBH,GAEzCxB,EAAaI,QAAQE,UAAUC,OAAO,UAAuC,IAA7BkB,GAChDzB,EAAaI,QAAQsB,WAAaC,QAAoC,IAA7BF,GAC5C,GACF,CAACpC,IAEEuC,EAAoCC,GAClCA,GAAqB,GACdjD,EAAUkD,KAGjBD,GAAqB,GACdjD,EAAUmD,QAGdnD,EAAUoD,KAGrB9B,GAAU,KACN,IAAKhB,EAAQkB,QACT,MAAO,OAGXiB,IAEA,MAyBMY,EAAuB,IAAIC,sBAzBHC,IAC1BA,EAAQC,SAASC,IACb,MAAMC,EAASD,EAAMC,OACfC,EAAQC,OAAOF,EAAOG,QAAQC,YAEhCd,EAAiCS,EAAMR,qBAAuBjD,EAAUkD,KACxE3C,EAAoBiB,QAAQuC,KAAKJ,GAEjCpD,EAAoBiB,QAAUjB,EAAoBiB,QAAQwC,QAAQF,GAAeA,IAAeH,IAGhGX,EAAiCS,EAAMR,qBAAuBjD,EAAUmD,QACxE3C,EAA6BgB,QAAQuC,KAAKJ,GAE1CnD,EAA6BgB,QAAUhB,EAA6BgB,QAAQwC,QAAQF,GAAeA,IAAeH,GACrH,IAILpD,EAAoBiB,QAAU,IAAI,IAAIyC,IAAI1D,EAAoBiB,UAAU0C,MAAK,CAACC,EAAGC,IAAMD,EAAIC,IAC3F5D,EAA6BgB,QAAU,IAAI,IAAIyC,IAAIzD,EAA6BgB,UAAU0C,MAAK,CAACC,EAAGC,IAAMD,EAAIC,IAE7G3B,GAAuB,GAGiD,CACxE4B,KAAM/D,EAAQkB,QACd8C,UAAW,CAAC,EAAG,GAAK,MAKxB,OAFAlE,EAAOoB,QAAQgC,SAAQ,EAAGe,aAAclB,EAAqBmB,QAAQD,KAE9D,IAAMlB,EAAqBoB,YAAY,GAC/C,CAACnE,EAASmC,IAEb,MAAMiC,EAAYC,IACd,IAAKrE,EAAQkB,QACT,OAGJ,MAAMoD,EAAmBD,IAAc1E,EAAoB4E,KAAOvC,IAA8B,EAAIC,IAA6B,EAE3HuC,EAAc1E,EAAOoB,QAAQoD,GACnC,IAAIG,EAAa,EAEZD,IAKDC,EADAJ,IAAc1E,EAAoB4E,KACrBC,EAAYP,QAAQS,WAAa1E,EAAQkB,QAAQwD,WAAa1E,EAAQkB,QAAQK,YAAciD,EAAYP,QAAQ1C,YAEhHiD,EAAYP,QAAQS,WAAa1E,EAAQkB,QAAQwD,WAGlE1E,EAAQkB,QAAQyD,SAAS,CAAEC,SAAU,SAAUC,KAAMJ,EAAYK,IAAK,IAAI,EAG9E,OACIC,EAAA,MAAA,CAAKC,UAAU,SACXnF,SAAA,CAAAoF,EAAA,MAAA,CAAKD,UAAU,kBAAkBE,KAAK,OAAOC,IAAKnF,EAC9CoF,YAhIcxD,IACtBjB,EAAiB,IACVD,EACHE,QAASgB,EAAMhB,QACfC,QAASb,EAAQkB,SAASuD,YAAc,IAG5ClE,GAAc,EAAK,EA0HX8E,YAvHczD,IACjB5B,EAAQkB,SAAYZ,IAIrBgF,KAAKC,IAAI7E,EAAcE,QAAUgB,EAAMhB,SAAW,GAClDH,GAAoB,GAGxBT,EAAQkB,QAAQuD,WAAa/D,EAAcG,QAAUH,EAAcE,QAAUgB,EAAMhB,QAAO,EA+GlF4E,UAtIW,KACnBjF,GAAc,EAAM,EAsIZkF,eAhJoB7D,IACxBpB,IACAoB,EAAMC,kBACND,EAAME,kBAGVrB,GAAoB,EAAM,EA4IjBZ,SAAA6F,EAASC,IAAI9F,GAAU,CAAC+F,EAAOvC,IAC5B4B,SAAKD,UAAU,yBAAyBE,KAAK,WAAU,mBAA+B7B,EAAO8B,IAAMU,IAAeA,GAhHjH,EAACA,EAAsBxC,KACpCvD,EAAOoB,QAAQmC,GAAS,CACpBY,QAAS4B,EACTC,WAAYpG,EAAUoD,KACzB,EA4GqIiD,CAASF,EAAMxC,EAAS,EAC7IxD,SAAA+F,GADwDvC,OAKrE4B,yBACe,iBACXe,KAAK,SACLC,QAAS,IAAM7B,EAASzE,EAAoB4E,MAC5CY,IAAKrE,EACLkE,UAAU,0GAEVC,EAAG,IAAA,CAAAD,UAAU,wBAEjBC,EACe,SAAA,CAAA,aAAA,aACXe,KAAK,SACLC,QAAS,IAAM7B,EAASzE,EAAoBuG,MAC5Cf,IAAKpE,EACLiE,UAAU,iGAEVnF,SAAAoF,EAAA,IAAA,CAAGD,UAAU,2BAGvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
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
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -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
|
+
|
package/src/Slider.scss
ADDED
|
@@ -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
|
+
};
|
package/src/index.ts
ADDED
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
|
+
}
|