├── .eslintrc.cjs ├── .github ├── FUNDING.yml └── workflows │ └── playwright.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── index.es.js ├── index.es.js.map ├── index.umd.js ├── index.umd.js.map ├── src │ ├── App.d.ts │ ├── customScroll.d.ts │ ├── example │ │ ├── demoComp.d.ts │ │ └── demoText.d.ts │ ├── main.d.ts │ └── utils.d.ts └── vite.svg ├── example.html ├── exampleDist ├── assets │ ├── giraffe-icon-4kF3UqUO.png │ ├── index-B1KVCYZb.css │ └── index-yOPKkUYY.js ├── index.html └── vite.svg ├── giraffe-icon.png ├── index.html ├── index.ts ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── customScroll.tsx ├── example │ ├── demoComp.css │ ├── demoComp.tsx │ ├── demoText.ts │ └── giraffe-icon.png ├── index.css ├── main.tsx ├── utils.ts └── vite-env.d.ts ├── startServer.ts ├── stopServer.ts ├── tailwind.config.js ├── tests ├── customScroll.spec.ts └── customScrollDriver.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── vite.config.ts.timestamp-1706665317733-460586ad0276b.mjs └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: rommguy 4 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 18 16 | - name: Install dependencies 17 | run: npm install -g yarn && yarn 18 | - name: Install Playwright Browsers 19 | run: yarn playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: yarn playwright test 22 | - uses: actions/upload-artifact@v4 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 6 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | #dist 12 | #exampleDist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | /test-results/ 27 | /playwright-report/ 28 | /blob-report/ 29 | /playwright/.cache/ 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Guy Romm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-image]][npm-url] 2 | ![](https://github.com/rommguy/react-custom-scroll/workflows/build/badge.svg) 3 | ![](https://img.shields.io/npm/dw/react-custom-scroll) 4 | 5 | 6 | # React-Custom-Scroll 7 | An easily designable, cross browser (!!), custom scroll with ReactJS. 8 | 9 | The actual scroll is still the native one - Meaning the scroll animations and scroll rate work as always 10 | The only thin that is different is the visible design and scrollbar layout. 11 | 12 | ##### See a [working demo](http://rommguy.github.io/react-custom-scroll/exampleDist/index.html) ### 13 | ## Installation 14 | ```sh 15 | npm i react-custom-scroll --save 16 | ``` 17 | 18 | ### Why do I need this ? 19 | - Same design on all browsers 20 | - Scrollbar is above the content instead of floating to the side - same layout on scrolled content as not scrolled content 21 | 22 | ### How to use ? 23 | Custom scroll component is available in module or commonJS format. 24 | It is located in /dist directory 25 | **From unpkg cdn:** 26 | * [Js file](https://unpkg.com/react-custom-scroll@7.0.0/dist/index.umd.js) 27 | 28 | Wrap your content with the custom scroll component 29 | Remove any overflow style properties from your content root component - The custom scroll will take care of it 30 | 31 | 32 | ```js 33 | import { CustomScroll } from "react-custom-scroll"; 34 | ``` 35 | 36 | ```jsx 37 | 38 | your content 39 | 40 | ``` 41 | 42 | ### How to change the design ? 43 | Your own custom design can be applied by styling these 2 classes in your css: 44 | 45 | - rcs-custom-scrollbar - this class styles the container of the scroll handle, you can use it if your handle width is greater than the default. 46 | - rcs-inner-handle - this class styles the handle itself, you can use it to change the color, background, border and such of the handle 47 | 48 | You can see a usage example in the demo page. 49 | 50 | ### Options (react props) 51 | 52 | - **allowOuterScroll** : boolean, default false. Blocks outer scroll while scrolling the content 53 | - **heightRelativeToParent** : string, default undefined. Content height limit is relative to parent - the value should be the height limit. 54 | - **flex** : number, default undefined. If present will apply to the content wrapped by the custom scroll. 55 | This prop represents flex size. It is only relevant if the parent of customScroll has display: flex. See example below. 56 | This prop will override any value given to heightRelativeToParent when setting the height of customScroll. 57 | - **onScroll** - function, default undefined. Listener that will be called on each scroll. 58 | - **addScrolledClass** : boolean, default false. If true, will add a css class 'content-scrolled' while being scrolled. 59 | - **freezePosition** : boolean, default false. When true, will prevent scrolling. 60 | - **minScrollHandleHeight** : number, sets the mimimum height of the scroll handle. Default is 38, as in Chrome on OSX. 61 | - **rtl** : boolean, default false. Right to left document, will place the custom scrollbar on the left side of the content, and assume the native one is also there. 62 | - **scrollTo**: number, default undefined. Will scroll content to the given value. 63 | - **keepAtBottom**: boolean, default false. For dynamic content, will keep the scroll position at the bottom of the content, when the content changes, if the position was at the bottom before the change. [See example here](http://rommguy.github.io/react-custom-scroll/exampleDist/index.html#dynamic-content-example) 64 | - **className**: string, default undefined. Allows adding your own class name to the root element. 65 | - **handleClass**: string. Can be used to replace the className given to the scroll handle, which is 'rcs-inner-handle' by default. 66 | 67 | ##### Example for heightRelativeToParent 68 | 69 | ```jsx 70 | 71 | your content 72 | 73 | ``` 74 | 75 | ### It doesn't work, please help me 76 | 77 | - Check if you forgot to remove 'overflow' properties from the content root element. 78 | - If you're using JSX, make sure you use Pascal case and not camelCase \ and not \. 79 | starting with lower case causes JSX to treat the tag as a native dom element 80 | - Make sure you have a height limit on the content root element (max-height) 81 | - Check if your height limit is relative to parent, and you didn't use heightRelativeToParent prop. 82 | 83 | ### Typescript 84 | - You can use CustomScroll types by installing @types/react-custom-scroll from npm 85 | 86 | ### Usage with flexbox 87 | ##### See a [demo with Flex](http://rommguy.github.io/react-custom-scroll/exampleDist/index.html#flex-example) ### 88 | There are some details that apply when using customScroll on elements with size set by css flex. 89 | Here is an example for an HTML structure before using customScroll: 90 | 91 | ```jsx 92 | 93 | 94 | 95 | your content (with enough height to cause a scroll) 96 | 97 | 98 | ``` 99 | 100 | In this example, a scroll is active on the flexibleHeightElement, where the flex size sets it's height to 400px, after the fixedHeight element took 100px. 101 | 102 | #### Solutions 103 | There are 2 options to use customScroll with this structure: 104 | 105 | - Wrapping the content: 106 | For this solution, the overflow property should be removed from the flex size element, since the customScroll will take care of that. 107 | Instead, min-height and min-width should be set to 0. 108 | 109 | ```jsx 110 | 111 | 112 | 113 | 114 | your content (with enough height to cause a scroll) 115 | 116 | 117 | 118 | ``` 119 | 120 | min-height and min-width are required since flex won't shrink below it's minimum content size ([flex box spec](https://www.w3.org/TR/css-flexbox/#flex-common)). 121 | 122 | - Replacing the flex-size element with customScroll 123 | 124 | ```jsx 125 | 126 | 127 | 128 | your content (with enough height to cause a scroll) 129 | 130 | 131 | ``` 132 | 133 | ### Contributing 134 | This project is built using Vite. 135 | To run in dev mode - "yarn dev" or "npm run dev". 136 | To build the project - "yarn build" or "npm run build". 137 | 138 | ### Tests 139 | ```sh 140 | npm install 141 | npm test 142 | # Or for continuous run 143 | npx karma start 144 | ``` 145 | 146 | [npm-image]: https://img.shields.io/npm/v/react-custom-scroll.svg?style=flat-square 147 | [npm-url]: https://npmjs.org/package/react-custom-scroll 148 | [travis-image]: https://img.shields.io/travis/wix/react-custom-scroll/main.svg?style=flat-square 149 | [travis-url]: https://travis-ci.org/wix/react-custom-scroll 150 | [coveralls-image]: https://img.shields.io/coveralls/wix/react-custom-scroll/main.svg?style=flat-square 151 | [coveralls-url]: https://coveralls.io/r/wix/react-custom-scroll?branch=gh-pages 152 | [downloads-image]: http://img.shields.io/npm/dm/react-custom-scroll.svg?style=flat-square 153 | [downloads-url]: https://npmjs.org/package/react-custom-scroll 154 | -------------------------------------------------------------------------------- /dist/index.umd.js: -------------------------------------------------------------------------------- 1 | (function(V,T){typeof exports=="object"&&typeof module<"u"?T(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],T):(V=typeof globalThis<"u"?globalThis:V||self,T(V["react-custom-scroll"]={},V.React))})(this,function(V,T){"use strict";var Ro=Object.defineProperty;var Co=(V,T,ee)=>T in V?Ro(V,T,{enumerable:!0,configurable:!0,writable:!0,value:ee}):V[T]=ee;var b=(V,T,ee)=>(Co(V,typeof T!="symbol"?T+"":T,ee),ee);var ee={exports:{}},je={};/** 2 | * @license React 3 | * react-jsx-runtime.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */var Ht;function Lr(){if(Ht)return je;Ht=1;var e=T,t=Symbol.for("react.element"),r=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=e.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};function c(f,l,h){var p,g={},S=null,R=null;h!==void 0&&(S=""+h),l.key!==void 0&&(S=""+l.key),l.ref!==void 0&&(R=l.ref);for(p in l)o.call(l,p)&&!s.hasOwnProperty(p)&&(g[p]=l[p]);if(f&&f.defaultProps)for(p in l=f.defaultProps,l)g[p]===void 0&&(g[p]=l[p]);return{$$typeof:t,type:f,key:S,ref:R,props:g,_owner:i.current}}return je.Fragment=r,je.jsx=c,je.jsxs=c,je}var He={};/** 10 | * @license React 11 | * react-jsx-runtime.development.js 12 | * 13 | * Copyright (c) Facebook, Inc. and its affiliates. 14 | * 15 | * This source code is licensed under the MIT license found in the 16 | * LICENSE file in the root directory of this source tree. 17 | */var It;function Wr(){return It||(It=1,process.env.NODE_ENV!=="production"&&function(){var e=T,t=Symbol.for("react.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),s=Symbol.for("react.profiler"),c=Symbol.for("react.provider"),f=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),p=Symbol.for("react.suspense_list"),g=Symbol.for("react.memo"),S=Symbol.for("react.lazy"),R=Symbol.for("react.offscreen"),A=Symbol.iterator,H="@@iterator";function L(n){if(n===null||typeof n!="object")return null;var a=A&&n[A]||n[H];return typeof a=="function"?a:null}var _=e.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;function y(n){{for(var a=arguments.length,u=new Array(a>1?a-1:0),d=1;d=1&&$>=0&&w[I]!==Y[$];)$--;for(;I>=1&&$>=0;I--,$--)if(w[I]!==Y[$]){if(I!==1||$!==1)do if(I--,$--,$<0||w[I]!==Y[$]){var q=` 21 | `+w[I].replace(" at new "," at ");return n.displayName&&q.includes("")&&(q=q.replace("",n.displayName)),typeof n=="function"&&de.set(n,q),q}while(I>=1&&$>=0);break}}}finally{J=!1,ie.current=O,Oe(),Error.prepareStackTrace=P}var Ne=n?n.displayName||n.name:"",Fr=Ne?K(Ne):"";return typeof n=="function"&&de.set(n,Fr),Fr}function eo(n,a,u){return Z(n,!1)}function to(n){var a=n.prototype;return!!(a&&a.isReactComponent)}function at(n,a,u){if(n==null)return"";if(typeof n=="function")return Z(n,to(n));if(typeof n=="string")return K(n);switch(n){case h:return K("Suspense");case p:return K("SuspenseList")}if(typeof n=="object")switch(n.$$typeof){case l:return eo(n.render);case g:return at(n.type,a,u);case S:{var d=n,P=d._payload,O=d._init;try{return at(O(P),a,u)}catch{}}}return""}var ct=Object.prototype.hasOwnProperty,xr={},_r=_.ReactDebugCurrentFrame;function lt(n){if(n){var a=n._owner,u=at(n.type,n._source,a?a.type:null);_r.setExtraStackFrame(u)}else _r.setExtraStackFrame(null)}function ro(n,a,u,d,P){{var O=Function.call.bind(ct);for(var C in n)if(O(n,C)){var w=void 0;try{if(typeof n[C]!="function"){var Y=Error((d||"React class")+": "+u+" type `"+C+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof n[C]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw Y.name="Invariant Violation",Y}w=n[C](a,C,d,u,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(I){w=I}w&&!(w instanceof Error)&&(lt(P),y("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",d||"React class",u,C,typeof w),lt(null)),w instanceof Error&&!(w.message in xr)&&(xr[w.message]=!0,lt(P),y("Failed %s type: %s",u,w.message),lt(null))}}}var no=Array.isArray;function Ot(n){return no(n)}function oo(n){{var a=typeof Symbol=="function"&&Symbol.toStringTag,u=a&&n[Symbol.toStringTag]||n.constructor.name||"Object";return u}}function io(n){try{return kr(n),!1}catch{return!0}}function kr(n){return""+n}function Or(n){if(io(n))return y("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",oo(n)),kr(n)}var Le=_.ReactCurrentOwner,so={key:!0,ref:!0,__self:!0,__source:!0},Ar,Dr,At;At={};function ao(n){if(ct.call(n,"ref")){var a=Object.getOwnPropertyDescriptor(n,"ref").get;if(a&&a.isReactWarning)return!1}return n.ref!==void 0}function co(n){if(ct.call(n,"key")){var a=Object.getOwnPropertyDescriptor(n,"key").get;if(a&&a.isReactWarning)return!1}return n.key!==void 0}function lo(n,a){if(typeof n.ref=="string"&&Le.current&&a&&Le.current.stateNode!==a){var u=U(Le.current.type);At[u]||(y('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref',U(Le.current.type),n.ref),At[u]=!0)}}function uo(n,a){{var u=function(){Ar||(Ar=!0,y("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",a))};u.isReactWarning=!0,Object.defineProperty(n,"key",{get:u,configurable:!0})}}function fo(n,a){{var u=function(){Dr||(Dr=!0,y("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",a))};u.isReactWarning=!0,Object.defineProperty(n,"ref",{get:u,configurable:!0})}}var ho=function(n,a,u,d,P,O,C){var w={$$typeof:t,type:n,key:a,ref:u,props:C,_owner:O};return w._store={},Object.defineProperty(w._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(w,"_self",{configurable:!1,enumerable:!1,writable:!1,value:d}),Object.defineProperty(w,"_source",{configurable:!1,enumerable:!1,writable:!1,value:P}),Object.freeze&&(Object.freeze(w.props),Object.freeze(w)),w};function po(n,a,u,d,P){{var O,C={},w=null,Y=null;u!==void 0&&(Or(u),w=""+u),co(a)&&(Or(a.key),w=""+a.key),ao(a)&&(Y=a.ref,lo(a,P));for(O in a)ct.call(a,O)&&!so.hasOwnProperty(O)&&(C[O]=a[O]);if(n&&n.defaultProps){var I=n.defaultProps;for(O in I)C[O]===void 0&&(C[O]=I[O])}if(w||Y){var $=typeof n=="function"?n.displayName||n.name||"Unknown":n;w&&uo(C,$),Y&&fo(C,$)}return ho(n,w,Y,P,d,Le.current,C)}}var Dt=_.ReactCurrentOwner,Nr=_.ReactDebugCurrentFrame;function De(n){if(n){var a=n._owner,u=at(n.type,n._source,a?a.type:null);Nr.setExtraStackFrame(u)}else Nr.setExtraStackFrame(null)}var Nt;Nt=!1;function jt(n){return typeof n=="object"&&n!==null&&n.$$typeof===t}function jr(){{if(Dt.current){var n=U(Dt.current.type);if(n)return` 22 | 23 | Check the render method of \``+n+"`."}return""}}function go(n){{if(n!==void 0){var a=n.fileName.replace(/^.*[\\\/]/,""),u=n.lineNumber;return` 24 | 25 | Check your code at `+a+":"+u+"."}return""}}var Hr={};function mo(n){{var a=jr();if(!a){var u=typeof n=="string"?n:n.displayName||n.name;u&&(a=` 26 | 27 | Check the top-level render call using <`+u+">.")}return a}}function Ir(n,a){{if(!n._store||n._store.validated||n.key!=null)return;n._store.validated=!0;var u=mo(a);if(Hr[u])return;Hr[u]=!0;var d="";n&&n._owner&&n._owner!==Dt.current&&(d=" It was passed a child from "+U(n._owner.type)+"."),De(n),y('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',u,d),De(null)}}function Mr(n,a){{if(typeof n!="object")return;if(Ot(n))for(var u=0;u",w=" Did you accidentally export a JSX literal instead of a component?"):I=typeof n,y("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",I,w)}var $=po(n,a,u,P,O);if($==null)return $;if(C){var q=a.children;if(q!==void 0)if(d)if(Ot(q)){for(var Ne=0;Ne0?F(Ee,--B):0,we--,M===10&&(we=1,ze--),M}function X(){return M=B2||ht(M)>3?"":" "}function en(e,t){for(;--t&&X()&&!(M<48||M>102||M>57&&M<65||M>70&&M<97););return Ue(e,Be()+(t<6&&he()==32&&X()==32))}function gt(e){for(;X();)switch(M){case e:return B;case 34:case 39:e!==34&&e!==39&>(M);break;case 40:e===41&>(e);break;case 92:X();break}return B}function tn(e,t){for(;X()&&e+M!==57;)if(e+M===84&&he()===47)break;return"/*"+Ue(t,B-1)+"*"+ft(e===47?e:X())}function rn(e){for(;!ht(he());)X();return Ue(e,B)}function nn(e){return Zr(Ge("",null,null,null,[""],e=Jr(e),0,[0],e))}function Ge(e,t,r,o,i,s,c,f,l){for(var h=0,p=0,g=c,S=0,R=0,A=0,H=1,L=1,_=1,y=0,k="",D=i,j=s,E=o,m=k;L;)switch(A=y,y=X()){case 40:if(A!=108&&F(m,g-1)==58){Ye(m+=v(pt(y),"&","&\f"),"&\f",Ft(h?f[h-1]:0))!=-1&&(_=-1);break}case 34:case 39:case 91:m+=pt(y);break;case 9:case 10:case 13:case 32:m+=Qr(A);break;case 92:m+=en(Be()-1,7);continue;case 47:switch(he()){case 42:case 47:Me(on(tn(X(),Be()),t,r,l),l);break;default:m+="/"}break;case 123*H:f[h++]=Q(m)*_;case 125*H:case 59:case 0:switch(y){case 0:case 125:L=0;case 59+p:_==-1&&(m=v(m,/\f/g,"")),R>0&&Q(m)-g&&Me(R>32?Bt(m+";",o,r,g-1,l):Bt(v(m," ","")+";",o,r,g-2,l),l);break;case 59:m+=";";default:if(Me(E=Vt(m,t,r,h,p,i,f,k,D=[],j=[],g,s),s),y===123)if(p===0)Ge(m,t,E,E,D,s,g,f,j);else switch(S===99&&F(m,3)===110?100:S){case 100:case 108:case 109:case 115:Ge(e,E,E,o&&Me(Vt(e,E,E,0,0,i,f,k,i,D=[],g,j),j),i,j,g,f,o?D:j);break;default:Ge(m,E,E,E,[""],j,0,f,j)}}h=p=R=0,H=_=1,k=m="",g=c;break;case 58:g=1+Q(m),R=A;default:if(H<1){if(y==123)--H;else if(y==125&&H++==0&&Kr()==125)continue}switch(m+=ft(y),y*H){case 38:_=p>0?1:(m+="\f",-1);break;case 44:f[h++]=(Q(m)-1)*_,_=1;break;case 64:he()===45&&(m+=pt(X())),S=he(),p=g=Q(k=m+=rn(Be())),y++;break;case 45:A===45&&Q(m)==2&&(H=0)}}return s}function Vt(e,t,r,o,i,s,c,f,l,h,p,g){for(var S=i-1,R=i===0?s:[""],A=Wt(R),H=0,L=0,_=0;H0?R[y]+" "+k:v(k,/&\f/g,R[y])))&&(l[_++]=D);return Ve(e,t,r,i===0?We:f,l,h,p,g)}function on(e,t,r,o){return Ve(e,t,r,Mt,ft(Xr()),Se(e,2,-2),0,o)}function Bt(e,t,r,o,i){return Ve(e,t,r,ut,Se(e,0,o),Se(e,o+1,-1),o,i)}function Ut(e,t,r){switch(Gr(e,t)){case 5103:return x+"print-"+e+e;case 5737:case 4201:case 3177:case 3433:case 1641:case 4457:case 2921:case 5572:case 6356:case 5844:case 3191:case 6645:case 3005:case 6391:case 5879:case 5623:case 6135:case 4599:case 4855:case 4215:case 6389:case 5109:case 5365:case 5621:case 3829:return x+e+e;case 4789:return Ie+e+e;case 5349:case 4246:case 4810:case 6968:case 2756:return x+e+Ie+e+N+e+e;case 5936:switch(F(e,t+11)){case 114:return x+e+N+v(e,/[svh]\w+-[tblr]{2}/,"tb")+e;case 108:return x+e+N+v(e,/[svh]\w+-[tblr]{2}/,"tb-rl")+e;case 45:return x+e+N+v(e,/[svh]\w+-[tblr]{2}/,"lr")+e}case 6828:case 4268:case 2903:return x+e+N+e+e;case 6165:return x+e+N+"flex-"+e+e;case 5187:return x+e+v(e,/(\w+).+(:[^]+)/,x+"box-$1$2"+N+"flex-$1$2")+e;case 5443:return x+e+N+"flex-item-"+v(e,/flex-|-self/g,"")+(te(e,/flex-|baseline/)?"":N+"grid-row-"+v(e,/flex-|-self/g,""))+e;case 4675:return x+e+N+"flex-line-pack"+v(e,/align-content|flex-|-self/g,"")+e;case 5548:return x+e+N+v(e,"shrink","negative")+e;case 5292:return x+e+N+v(e,"basis","preferred-size")+e;case 6060:return x+"box-"+v(e,"-grow","")+x+e+N+v(e,"grow","positive")+e;case 4554:return x+v(e,/([^-])(transform)/g,"$1"+x+"$2")+e;case 6187:return v(v(v(e,/(zoom-|grab)/,x+"$1"),/(image-set)/,x+"$1"),e,"")+e;case 5495:case 3959:return v(e,/(image-set\([^]*)/,x+"$1$`$1");case 4968:return v(v(e,/(.+:)(flex-)?(.*)/,x+"box-pack:$3"+N+"flex-pack:$3"),/s.+-b[^;]+/,"justify")+x+e+e;case 4200:if(!te(e,/flex-|baseline/))return N+"grid-column-align"+Se(e,t)+e;break;case 2592:case 3360:return N+v(e,"template-","")+e;case 4384:case 3616:return r&&r.some(function(o,i){return t=i,te(o.props,/grid-\w+-end/)})?~Ye(e+(r=r[t].value),"span",0)?e:N+v(e,"-start","")+e+N+"grid-row-span:"+(~Ye(r,"span",0)?te(r,/\d+/):+te(r,/\d+/)-+te(e,/\d+/))+";":N+v(e,"-start","")+e;case 4896:case 4128:return r&&r.some(function(o){return te(o.props,/grid-\w+-start/)})?e:N+v(v(e,"-end","-span"),"span ","")+e;case 4095:case 3583:case 4068:case 2532:return v(e,/(.+)-inline(.+)/,x+"$1$2")+e;case 8116:case 7059:case 5753:case 5535:case 5445:case 5701:case 4933:case 4677:case 5533:case 5789:case 5021:case 4765:if(Q(e)-1-t>6)switch(F(e,t+1)){case 109:if(F(e,t+4)!==45)break;case 102:return v(e,/(.+:)(.+)-([^]+)/,"$1"+x+"$2-$3$1"+Ie+(F(e,t+3)==108?"$3":"$2-$3"))+e;case 115:return~Ye(e,"stretch",0)?Ut(v(e,"stretch","fill-available"),t,r)+e:e}break;case 5152:case 5920:return v(e,/(.+?):(\d+)(\s*\/\s*(span)?\s*(\d+))?(.*)/,function(o,i,s,c,f,l,h){return N+i+":"+s+h+(c?N+i+"-span:"+(f?l:+l-+s)+h:"")+e});case 4949:if(F(e,t+6)===121)return v(e,":",":"+x)+e;break;case 6444:switch(F(e,F(e,14)===45?18:11)){case 120:return v(e,/(.+:)([^;\s!]+)(;|(\s+)?!.+)?/,"$1"+x+(F(e,14)===45?"inline-":"")+"box$3$1"+x+"$2$3$1"+N+"$2box$3")+e;case 100:return v(e,":",":"+N)+e}break;case 5719:case 2647:case 2135:case 3927:case 2391:return v(e,"scroll-","scroll-snap-")+e}return e}function qe(e,t){for(var r="",o=0;o-1&&!e.return)switch(e.type){case ut:e.return=Ut(e.value,e.length,r);return;case $t:return qe([ce(e,{value:v(e.value,"@","@"+x)})],o);case We:if(e.length)return qr(r=e.props,function(i){switch(te(i,o=/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":Re(ce(e,{props:[v(i,/:(read-\w+)/,":"+Ie+"$1")]})),Re(ce(e,{props:[i]})),dt(e,{props:Yt(r,o)});break;case"::placeholder":Re(ce(e,{props:[v(i,/:(plac\w+)/,":"+x+"input-$1")]})),Re(ce(e,{props:[v(i,/:(plac\w+)/,":"+Ie+"$1")]})),Re(ce(e,{props:[v(i,/:(plac\w+)/,N+"input-$1")]})),Re(ce(e,{props:[i]})),dt(e,{props:Yt(r,o)});break}return""})}}var un={animationIterationCount:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},pe=typeof process<"u"&&process.env!==void 0&&(process.env.REACT_APP_SC_ATTR||process.env.SC_ATTR)||"data-styled",Gt="active",qt="data-styled-version",Xe="6.1.8",mt=`/*!sc*/ 28 | `,vt=typeof window<"u"&&"HTMLElement"in window,fn=!!(typeof SC_DISABLE_SPEEDY=="boolean"?SC_DISABLE_SPEEDY:typeof process<"u"&&process.env!==void 0&&process.env.REACT_APP_SC_DISABLE_SPEEDY!==void 0&&process.env.REACT_APP_SC_DISABLE_SPEEDY!==""?process.env.REACT_APP_SC_DISABLE_SPEEDY!=="false"&&process.env.REACT_APP_SC_DISABLE_SPEEDY:typeof process<"u"&&process.env!==void 0&&process.env.SC_DISABLE_SPEEDY!==void 0&&process.env.SC_DISABLE_SPEEDY!==""?process.env.SC_DISABLE_SPEEDY!=="false"&&process.env.SC_DISABLE_SPEEDY:process.env.NODE_ENV!=="production"),Xt=/invalid hook call/i,Ke=new Set,dn=function(e,t){if(process.env.NODE_ENV!=="production"){var r=t?' with the id of "'.concat(t,'"'):"",o="The component ".concat(e).concat(r,` has been created dynamically. 29 | `)+`You may see this warning because you've called styled inside another component. 30 | To resolve this only create new StyledComponents outside of any render method and function component.`,i=console.error;try{var s=!0;console.error=function(c){for(var f=[],l=1;l?@[\\\]^`{|}~-]+/g,gn=/(^-|-$)/g;function Kt(e){return e.replace(pn,"-").replace(gn,"")}var mn=/(a)(d)/gi,Ze=52,Jt=function(e){return String.fromCharCode(e+(e>25?39:97))};function bt(e){var t,r="";for(t=Math.abs(e);t>Ze;t=t/Ze|0)r=Jt(t%Ze)+r;return(Jt(t%Ze)+r).replace(mn,"$1-$2")}var St,Zt=5381,ge=function(e,t){for(var r=t.length;r;)e=33*e^t.charCodeAt(--r);return e},Qt=function(e){return ge(Zt,e)};function vn(e){return bt(Qt(e)>>>0)}function er(e){return process.env.NODE_ENV!=="production"&&typeof e=="string"&&e||e.displayName||e.name||"Component"}function wt(e){return typeof e=="string"&&(process.env.NODE_ENV==="production"||e.charAt(0)===e.charAt(0).toLowerCase())}var tr=typeof Symbol=="function"&&Symbol.for,rr=tr?Symbol.for("react.memo"):60115,yn=tr?Symbol.for("react.forward_ref"):60112,bn={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},Sn={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},nr={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},wn=((St={})[yn]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},St[rr]=nr,St);function or(e){return("type"in(t=e)&&t.type.$$typeof)===rr?nr:"$$typeof"in e?wn[e.$$typeof]:bn;var t}var En=Object.defineProperty,Rn=Object.getOwnPropertyNames,ir=Object.getOwnPropertySymbols,Cn=Object.getOwnPropertyDescriptor,Pn=Object.getPrototypeOf,sr=Object.prototype;function ar(e,t,r){if(typeof t!="string"){if(sr){var o=Pn(t);o&&o!==sr&&ar(e,o,r)}var i=Rn(t);ir&&(i=i.concat(ir(t)));for(var s=or(e),c=or(t),f=0;f ({})}\n```\n\n',8:`ThemeProvider: Please make your "theme" prop an object. 56 | 57 | `,9:"Missing document ``\n\n",10:`Cannot find a StyleSheet instance. Usually this happens if there are multiple copies of styled-components loaded at once. Check out this issue for how to troubleshoot and fix the common cases where this situation can happen: https://github.com/styled-components/styled-components/issues/1941#issuecomment-417862021 58 | 59 | `,11:`_This error was replaced with a dev-time warning, it will be deleted for v4 final._ [createGlobalStyle] received children which will not be rendered. Please use the component without passing children elements. 60 | 61 | `,12:"It seems you are interpolating a keyframe declaration (%s) into an untagged string. This was supported in styled-components v3, but is not longer supported in v4 as keyframes are now injected on-demand. Please wrap your string in the css\\`\\` helper which ensures the styles are injected correctly. See https://www.styled-components.com/docs/api#css\n\n",13:`%s is not a styled component and cannot be referred to via component selector. See https://www.styled-components.com/docs/advanced#referring-to-other-components for more details. 62 | 63 | `,14:`ThemeProvider: "theme" prop is required. 64 | 65 | `,15:"A stylis plugin has been supplied that is not named. We need a name for each plugin to be able to prevent styling collisions between different stylis configurations within the same app. Before you pass your plugin to ``, please make sure each plugin is uniquely-named, e.g.\n\n```js\nObject.defineProperty(importedPlugin, 'name', { value: 'some-unique-name' });\n```\n\n",16:`Reached the limit of how many styled components may be created at group %s. 66 | You may only create up to 1,073,741,824 components. If you're creating components dynamically, 67 | as for instance in your render method then you may be running into this limitation. 68 | 69 | `,17:`CSSStyleSheet could not be found on HTMLStyleElement. 70 | Has styled-components' style tag been unmounted or altered by another script? 71 | `,18:"ThemeProvider: Please make sure your useTheme hook is within a ``"}:{};function xn(){for(var e=[],t=0;t0?" Args: ".concat(t.join(", ")):"")):new Error(xn.apply(void 0,be([Tn[e]],t,!1)).trim())}var _n=function(){function e(t){this.groupSizes=new Uint32Array(512),this.length=512,this.tag=t}return e.prototype.indexOfGroup=function(t){for(var r=0,o=0;o=this.groupSizes.length){for(var o=this.groupSizes,i=o.length,s=i;t>=s;)if((s<<=1)<0)throw xe(16,"".concat(t));this.groupSizes=new Uint32Array(s),this.groupSizes.set(o),this.length=s;for(var c=i;c=this.length||this.groupSizes[t]===0)return r;for(var o=this.groupSizes[t],i=this.indexOfGroup(t),s=i+o,c=i;c1073741824))throw xe(16,"".concat(t));return Qe.set(e,t),et.set(t,e),t},kn=function(e,t){tt=t+1,Qe.set(e,t),et.set(t,e)},On="style[".concat(pe,"][").concat(qt,'="').concat(Xe,'"]'),An=new RegExp("^".concat(pe,'\\.g(\\d+)\\[id="([\\w\\d-]+)"\\].*?"([^"]*)')),Dn=function(e,t,r){for(var o,i=r.split(","),s=0,c=i.length;s=0){var o=document.createTextNode(r);return this.element.insertBefore(o,this.nodes[t]||null),this.length++,!0}return!1},e.prototype.deleteRule=function(t){this.element.removeChild(this.nodes[t]),this.length--},e.prototype.getRule=function(t){return t0&&(L+="".concat(_,","))}),l+="".concat(A).concat(H,'{content:"').concat(L,'"}').concat(mt)},p=0;p0?".".concat(t):S},p=l.slice();p.push(function(S){S.type===We&&S.value.includes("&")&&(S.props[0]=S.props[0].replace(Fn,r).replace(o,h))}),c.prefix&&p.push(ln),p.push(sn);var g=function(S,R,A,H){R===void 0&&(R=""),A===void 0&&(A=""),H===void 0&&(H="&"),t=H,r=R,o=new RegExp("\\".concat(r,"\\b"),"g");var L=S.replace(Ln,""),_=nn(A||R?"".concat(A," ").concat(R," { ").concat(L," }"):L);c.namespace&&(_=dr(_,c.namespace));var y=[];return qe(_,an(p.concat(cn(function(k){return y.push(k)})))),y};return g.hash=l.length?l.reduce(function(S,R){return R.name||xe(15),ge(S,R.name)},Zt).toString():"",g}var Yn=new fr,Pt=Wn(),hr=T.createContext({shouldForwardProp:void 0,styleSheet:Yn,stylis:Pt});hr.Consumer,T.createContext(void 0);function pr(){return T.useContext(hr)}var gr=function(){function e(t,r){var o=this;this.inject=function(i,s){s===void 0&&(s=Pt);var c=o.name+s.hash;i.hasNameForId(o.id,c)||i.insertRules(o.id,c,s(o.rules,c,"@keyframes"))},this.name=t,this.id="sc-keyframes-".concat(t),this.rules=r,Ct(this,function(){throw xe(12,String(o.name))})}return e.prototype.getName=function(t){return t===void 0&&(t=Pt),this.name+t.hash},e}(),zn=function(e){return e>="A"&&e<="Z"};function mr(e){for(var t="",r=0;r>>0);if(!r.hasNameForId(this.componentId,c)){var f=o(s,".".concat(c),void 0,this.componentId);r.insertRules(this.componentId,c,f)}i=me(i,c),this.staticRulesId=c}else{for(var l=ge(this.baseHash,o.hash),h="",p=0;p>>0);r.hasNameForId(this.componentId,R)||r.insertRules(this.componentId,R,o(h,".".concat(R),void 0,this.componentId)),i=me(i,R)}}return i},e}(),br=T.createContext(void 0);br.Consumer;var Tt={},Sr=new Set;function Gn(e,t,r){var o=Et(e),i=e,s=!wt(e),c=t.attrs,f=c===void 0?Je:c,l=t.componentId,h=l===void 0?function(D,j){var E=typeof D!="string"?"sc":Kt(D);Tt[E]=(Tt[E]||0)+1;var m="".concat(E,"-").concat(vn(Xe+E+Tt[E]));return j?"".concat(j,"-").concat(m):m}(t.displayName,t.parentComponentId):l,p=t.displayName,g=p===void 0?function(D){return wt(D)?"styled.".concat(D):"Styled(".concat(er(D),")")}(e):p,S=t.displayName&&t.componentId?"".concat(Kt(t.displayName),"-").concat(t.componentId):t.componentId||h,R=o&&i.attrs?i.attrs.concat(f).filter(Boolean):f,A=t.shouldForwardProp;if(o&&i.shouldForwardProp){var H=i.shouldForwardProp;if(t.shouldForwardProp){var L=t.shouldForwardProp;A=function(D,j){return H(D,j)&&L(D,j)}}else A=H}var _=new Un(r,S,o?i.componentStyle:void 0);function y(D,j){return function(E,m,re){var ne=E.attrs,_t=E.componentStyle,kt=E.defaultProps,it=E.foldedComponentIds,U=E.styledComponentId,oe=E.target,ye=T.useContext(br),st=pr(),_e=E.shouldForwardProp||st.shouldForwardProp;process.env.NODE_ENV!=="production"&&T.useDebugValue(U);var $e=hn(m,ye,kt)||Ce,G=function(Oe,ie,fe){for(var K,J=z(z({},ie),{className:void 0,theme:fe}),de=0;de` (connect an API like `@emotion/is-prop-valid`) or consider using transient props (`$` prefix for automatic filtering.)')))));var ke=function(Oe,ie){var fe=pr(),K=Oe.generateAndInjectStyles(ie,fe.styleSheet,fe.stylis);return process.env.NODE_ENV!=="production"&&T.useDebugValue(K),K}(_t,G);process.env.NODE_ENV!=="production"&&E.warnTooManyClasses&&E.warnTooManyClasses(ke);var Fe=me(it,U);return ke&&(Fe+=" "+ke),G.className&&(Fe+=" "+G.className),ue[wt(le)&&!yt.has(le)?"class":"className"]=Fe,ue.ref=re,T.createElement(le,ue)}(k,D,j)}y.displayName=g;var k=T.forwardRef(y);return k.attrs=R,k.componentStyle=_,k.displayName=g,k.shouldForwardProp=A,k.foldedComponentIds=o?me(i.foldedComponentIds,i.styledComponentId):"",k.styledComponentId=S,k.target=o?i.target:e,Object.defineProperty(k,"defaultProps",{get:function(){return this._foldedDefaultProps},set:function(D){this._foldedDefaultProps=o?function(j){for(var E=[],m=1;m=200)){var ne=j?' with the id of "'.concat(j,'"'):"";console.warn("Over ".concat(200," classes were generated for component ").concat(D).concat(ne,`. 72 | `)+`Consider using the attrs method, together with a style object for frequently changed styles. 73 | Example: 74 | const Component = styled.div.attrs(props => ({ 75 | style: { 76 | background: props.background, 77 | }, 78 | }))\`width: 100%;\` 79 | 80 | `),m=!0,E={}}}}(g,S)),Ct(k,function(){return".".concat(k.styledComponentId)}),s&&ar(k,e,{attrs:!0,componentStyle:!0,displayName:!0,foldedComponentIds:!0,shouldForwardProp:!0,styledComponentId:!0,target:!0}),k}function wr(e,t){for(var r=[e[0]],o=0,i=t.length;o{let r;function o(){clearTimeout(r)}function i(){o(),r=setTimeout(()=>{e()},t)}return i.cancel=o,i},Cr=(e,t,r)=>(t=!t&&t!==0?e:t,r=!r&&r!==0?e:r,t>r?(console.error("min limit is greater than max limit"),e):er?r:e),Pr=(e,t)=>e.clientX>t.left&&e.clientXt.top&&e.clientY{const r=t.getBoundingClientRect();return Pr(e,r)},Tr=nt.div` 85 | position: absolute; 86 | height: 100%; 87 | width: 6px; 88 | right: 3px; 89 | opacity: 0; 90 | z-index: 1; 91 | transition: opacity 0.4s ease-out; 92 | padding: 6px 0; 93 | box-sizing: border-box; 94 | will-change: opacity; 95 | pointer-events: none; 96 | 97 | &.rcs-custom-scrollbar-rtl { 98 | right: auto; 99 | left: 3px; 100 | } 101 | 102 | &.scroll-visible { 103 | opacity: 1; 104 | transition-duration: 0.2s; 105 | } 106 | `,Jn=nt.div` 107 | height: calc(100% - 12px); 108 | margin-top: 6px; 109 | background-color: rgba(78, 183, 245, 0.7); 110 | border-radius: 3px; 111 | `,Zn=nt.div` 112 | min-height: 0; 113 | min-width: 0; 114 | 115 | & .rcs-outer-container { 116 | overflow: hidden; 117 | 118 | & .rcs-positioning { 119 | position: relative; 120 | } 121 | } 122 | 123 | & .rcs-inner-container { 124 | overflow-x: hidden; 125 | overflow-y: scroll; 126 | -webkit-overflow-scrolling: touch; 127 | 128 | &:after { 129 | content: ""; 130 | position: absolute; 131 | top: 0; 132 | right: 0; 133 | left: 0; 134 | height: 0; 135 | background-image: linear-gradient( 136 | to bottom, 137 | rgba(0, 0, 0, 0.2) 0%, 138 | rgba(0, 0, 0, 0.05) 60%, 139 | rgba(0, 0, 0, 0) 100% 140 | ); 141 | pointer-events: none; 142 | transition: height 0.1s ease-in; 143 | will-change: height; 144 | } 145 | 146 | &.rcs-content-scrolled:after { 147 | height: 5px; 148 | transition: height 0.15s ease-out; 149 | } 150 | } 151 | 152 | &.rcs-scroll-handle-dragged .rcs-inner-container { 153 | user-select: none; 154 | } 155 | 156 | &.rcs-scroll-handle-dragged ${Tr} { 157 | opacity: 1; 158 | } 159 | 160 | & .rcs-custom-scroll-handle { 161 | position: absolute; 162 | width: 100%; 163 | top: 0; 164 | } 165 | `;class Qn extends T.Component{constructor(r){super(r);b(this,"scrollbarYWidth");b(this,"hideScrollThumb");b(this,"contentHeight",0);b(this,"visibleHeight",0);b(this,"scrollHandleHeight",0);b(this,"scrollRatio",1);b(this,"hasScroll",!1);b(this,"startDragHandlePos",0);b(this,"startDragMousePos",0);b(this,"customScrollRef",T.createRef());b(this,"innerContainerRef",T.createRef());b(this,"customScrollbarRef",T.createRef());b(this,"scrollHandleRef",T.createRef());b(this,"contentWrapperRef",T.createRef());b(this,"adjustFreezePosition",r=>{if(!this.contentWrapperRef.current)return;const o=this.getScrolledElement(),i=this.contentWrapperRef.current;this.props.freezePosition&&(i.scrollTop=this.state.scrollPos),r.freezePosition&&(o.scrollTop=this.state.scrollPos)});b(this,"toggleScrollIfNeeded",()=>{const r=this.contentHeight-this.visibleHeight>1;this.hasScroll!==r&&(this.hasScroll=r,this.forceUpdate())});b(this,"updateScrollPosition",r=>{const o=this.getScrolledElement(),i=Cr(r,0,this.contentHeight-this.visibleHeight);o.scrollTop=i,this.setState({scrollPos:i})});b(this,"onClick",r=>{if(!this.hasScroll||!this.isMouseEventOnCustomScrollbar(r)||this.isMouseEventOnScrollHandle(r))return;const o=this.calculateNewScrollHandleTop(r),i=this.getScrollValueFromHandlePosition(o);this.updateScrollPosition(i)});b(this,"isMouseEventOnCustomScrollbar",r=>{if(!this.customScrollbarRef.current)return!1;const i=this.customScrollRef.current.getBoundingClientRect(),s=this.customScrollbarRef.current.getBoundingClientRect(),c=this.props.rtl?{left:i.left,right:s.right}:{left:s.left,width:i.right},f={right:i.right,top:i.top,height:i.height,...c};return Pr(r,f)});b(this,"isMouseEventOnScrollHandle",r=>{if(!this.scrollHandleRef.current)return!1;const o=this.scrollHandleRef.current;return Kn(r,o)});b(this,"calculateNewScrollHandleTop",r=>{const s=this.customScrollRef.current.getBoundingClientRect().top+window.pageYOffset,c=r.pageY-s,f=this.getScrollHandleStyle().top;let l;return c>f+this.scrollHandleHeight?l=f+Math.min(this.scrollHandleHeight,this.visibleHeight-this.scrollHandleHeight):l=f-Math.max(this.scrollHandleHeight,0),l});b(this,"getScrollValueFromHandlePosition",r=>r/this.scrollRatio);b(this,"getScrollHandleStyle",()=>{const r=this.state.scrollPos*this.scrollRatio;return this.scrollHandleHeight=this.visibleHeight*this.scrollRatio,{height:this.scrollHandleHeight,top:r}});b(this,"adjustCustomScrollPosToContentPos",r=>{this.setState({scrollPos:r})});b(this,"onScroll",r=>{this.props.freezePosition||(this.hideScrollThumb(),this.adjustCustomScrollPosToContentPos(r.currentTarget.scrollTop),this.props.onScroll&&this.props.onScroll(r))});b(this,"getScrolledElement",()=>this.innerContainerRef.current);b(this,"onMouseDown",r=>{!this.hasScroll||!this.isMouseEventOnScrollHandle(r)||(this.startDragHandlePos=this.getScrollHandleStyle().top,this.startDragMousePos=r.pageY,this.setState({onDrag:!0}),document.addEventListener("mousemove",this.onHandleDrag,{passive:!1}),document.addEventListener("mouseup",this.onHandleDragEnd,{passive:!1}))});b(this,"onTouchStart",()=>{this.setState({onDrag:!0})});b(this,"onHandleDrag",r=>{r.preventDefault();const o=r.pageY-this.startDragMousePos,i=Cr(this.startDragHandlePos+o,0,this.visibleHeight-this.scrollHandleHeight),s=this.getScrollValueFromHandlePosition(i);this.updateScrollPosition(s)});b(this,"onHandleDragEnd",r=>{this.setState({onDrag:!1}),r.preventDefault(),document.removeEventListener("mousemove",this.onHandleDrag),document.removeEventListener("mouseup",this.onHandleDragEnd)});b(this,"getInnerContainerClasses",()=>this.state.scrollPos&&this.props.addScrolledClass?"rcs-inner-container rcs-content-scrolled":"rcs-inner-container");b(this,"getScrollStyles",()=>{const r=this.scrollbarYWidth||20,o=this.props.rtl?"marginLeft":"marginRight",i={height:this.props.heightRelativeToParent||this.props.flex?"100%":"",overscrollBehavior:this.props.allowOuterScroll?"auto":"none"};i[o]=-1*r;const s={height:this.props.heightRelativeToParent||this.props.flex?"100%":"",overflowY:this.props.freezePosition?"hidden":"visible"};return s[o]=this.scrollbarYWidth?0:r,{innerContainer:i,contentWrapper:s}});b(this,"getOuterContainerStyle",()=>({height:this.props.heightRelativeToParent||this.props.flex?"100%":""}));b(this,"getRootStyles",()=>{const r={};return this.props.heightRelativeToParent?r.height=this.props.heightRelativeToParent:this.props.flex&&(r.flex=this.props.flex),r});b(this,"enforceMinHandleHeight",r=>{const o=this.props.minScrollHandleHeight||38;if(r.height>=o)return r;const i=o-r.height,s=this.state.scrollPos/(this.contentHeight-this.visibleHeight),c=i*s,f=r.top-c;return{height:o,top:f}});b(this,"onMouseEnter",()=>{this.setState({visible:!0})});b(this,"onMouseLeave",()=>{this.setState({visible:!1})});this.scrollbarYWidth=0,this.state={scrollPos:0,onDrag:!1,visible:!1},this.hideScrollThumb=Xn(()=>{this.setState({onDrag:!1})},500)}componentDidMount(){typeof this.props.scrollTo<"u"?this.updateScrollPosition(this.props.scrollTo):this.forceUpdate()}componentDidUpdate(r,o){const i=this.contentHeight,s=this.visibleHeight,c=this.getScrolledElement(),f=o.scrollPos>=i-s;this.contentHeight=c.scrollHeight,this.scrollbarYWidth=c.offsetWidth-c.clientWidth,this.visibleHeight=c.clientHeight,this.scrollRatio=this.contentHeight?this.visibleHeight/this.contentHeight:1,this.toggleScrollIfNeeded();const l=this.state===o;(this.props.freezePosition||r.freezePosition)&&this.adjustFreezePosition(r),typeof this.props.scrollTo<"u"&&this.props.scrollTo!==r.scrollTo?this.updateScrollPosition(this.props.scrollTo):this.props.keepAtBottom&&l&&f&&this.updateScrollPosition(this.contentHeight-this.visibleHeight)}componentWillUnmount(){this.hideScrollThumb.cancel(),document.removeEventListener("mousemove",this.onHandleDrag),document.removeEventListener("mouseup",this.onHandleDragEnd)}render(){const r=this.getScrollStyles(),o=this.getRootStyles(),i=this.enforceMinHandleHeight(this.getScrollHandleStyle()),s=[this.props.className||"","rcs-custom-scroll",this.state.onDrag?"rcs-scroll-handle-dragged":""].join(" ");return ae.jsx(Zn,{className:s,style:o,ref:this.customScrollRef,children:ae.jsxs("div",{"data-testid":"outer-container",className:"rcs-outer-container",style:this.getOuterContainerStyle(),onMouseDown:this.onMouseDown,onTouchStart:this.onTouchStart,onClick:this.onClick,onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave,children:[this.hasScroll?ae.jsx("div",{className:"rcs-positioning",children:ae.jsx(Tr,{"data-testid":"custom-scrollbar",ref:this.customScrollbarRef,className:`rcs-custom-scrollbar ${this.props.rtl?"rcs-custom-scrollbar-rtl":""} ${this.state.visible?"scroll-visible":""}`,children:ae.jsx("div",{"data-testid":"custom-scroll-handle",ref:this.scrollHandleRef,className:"rcs-custom-scroll-handle",style:i,children:ae.jsx(Jn,{className:this.props.handleClass||"rcs-inner-handle"})})},"scrollbar")}):null,ae.jsx("div",{"data-testid":"inner-container",ref:this.innerContainerRef,className:this.getInnerContainerClasses(),style:r.innerContainer,onScroll:this.onScroll,children:ae.jsx("div",{ref:this.contentWrapperRef,style:r.contentWrapper,children:this.props.children})})]})})}}V.CustomScroll=Qn,Object.defineProperty(V,Symbol.toStringTag,{value:"Module"})}); 166 | //# sourceMappingURL=index.umd.js.map 167 | -------------------------------------------------------------------------------- /dist/src/App.d.ts: -------------------------------------------------------------------------------- 1 | export declare const App: () => import("react/jsx-runtime").JSX.Element; 2 | -------------------------------------------------------------------------------- /dist/src/customScroll.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, CSSProperties, UIEvent, MouseEvent, PropsWithChildren } from "react"; 2 | import { simpleDebounce } from "./utils.ts"; 3 | interface CustomScrollProps extends PropsWithChildren { 4 | allowOuterScroll?: boolean; 5 | heightRelativeToParent?: string; 6 | onScroll?: (event: UIEvent) => void; 7 | addScrolledClass?: boolean; 8 | freezePosition?: boolean; 9 | handleClass?: string; 10 | minScrollHandleHeight?: number; 11 | flex?: string; 12 | rtl?: boolean; 13 | scrollTo?: number; 14 | keepAtBottom?: boolean; 15 | className?: string; 16 | } 17 | interface CustomScrollState { 18 | scrollPos: number; 19 | onDrag: boolean; 20 | visible: boolean; 21 | } 22 | export declare class CustomScroll extends Component { 23 | scrollbarYWidth: number; 24 | hideScrollThumb: ReturnType; 25 | contentHeight: number; 26 | visibleHeight: number; 27 | scrollHandleHeight: number; 28 | scrollRatio: number; 29 | hasScroll: boolean; 30 | startDragHandlePos: number; 31 | startDragMousePos: number; 32 | constructor(props: CustomScrollProps); 33 | componentDidMount(): void; 34 | componentDidUpdate(prevProps: CustomScrollProps, prevState: CustomScrollState): void; 35 | componentWillUnmount(): void; 36 | customScrollRef: import("react").RefObject; 37 | innerContainerRef: import("react").RefObject; 38 | customScrollbarRef: import("react").RefObject; 39 | scrollHandleRef: import("react").RefObject; 40 | contentWrapperRef: import("react").RefObject; 41 | adjustFreezePosition: (prevProps: CustomScrollProps) => void; 42 | toggleScrollIfNeeded: () => void; 43 | updateScrollPosition: (scrollValue: number) => void; 44 | onClick: (event: MouseEvent) => void; 45 | isMouseEventOnCustomScrollbar: (event: MouseEvent) => boolean; 46 | isMouseEventOnScrollHandle: (event: MouseEvent) => boolean; 47 | calculateNewScrollHandleTop: (clickEvent: MouseEvent) => number; 48 | getScrollValueFromHandlePosition: (handlePosition: number) => number; 49 | getScrollHandleStyle: () => { 50 | height: number; 51 | top: number; 52 | }; 53 | adjustCustomScrollPosToContentPos: (scrollPosition: number) => void; 54 | onScroll: (event: UIEvent) => void; 55 | getScrolledElement: () => HTMLElement; 56 | onMouseDown: (event: MouseEvent) => void; 57 | onTouchStart: () => void; 58 | onHandleDrag: (event: MouseEvent) => void; 59 | onHandleDragEnd: (e: MouseEvent) => void; 60 | getInnerContainerClasses: () => "rcs-inner-container rcs-content-scrolled" | "rcs-inner-container"; 61 | getScrollStyles: () => { 62 | innerContainer: CSSProperties; 63 | contentWrapper: CSSProperties; 64 | }; 65 | getOuterContainerStyle: () => { 66 | height: string; 67 | }; 68 | getRootStyles: () => CSSProperties; 69 | enforceMinHandleHeight: (calculatedStyle: { 70 | height: number; 71 | top: number; 72 | }) => { 73 | height: number; 74 | top: number; 75 | }; 76 | onMouseEnter: () => void; 77 | onMouseLeave: () => void; 78 | render(): import("react/jsx-runtime").JSX.Element; 79 | } 80 | export {}; 81 | -------------------------------------------------------------------------------- /dist/src/example/demoComp.d.ts: -------------------------------------------------------------------------------- 1 | interface DemoCompProps { 2 | demoType: "compare-with-native" | "crazy-designer" | "flex" | "dynamic-content" | "allow-outer-scroll"; 3 | descriptionSide: "left" | "right"; 4 | testId?: string; 5 | } 6 | export declare const DemoComp: ({ demoType, descriptionSide, testId, }: DemoCompProps) => import("react/jsx-runtime").JSX.Element; 7 | export {}; 8 | -------------------------------------------------------------------------------- /dist/src/example/demoText.d.ts: -------------------------------------------------------------------------------- 1 | export declare const demoText: { 2 | shortText: string; 3 | text: string; 4 | }; 5 | -------------------------------------------------------------------------------- /dist/src/main.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rommguy/react-custom-scroll/f68ecfc470524dc4124a0f7954e9bbd930c02e69/dist/src/main.d.ts -------------------------------------------------------------------------------- /dist/src/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent } from "react"; 2 | export declare const simpleDebounce: (func: () => void, delay: number) => { 3 | (): void; 4 | cancel: () => void; 5 | }; 6 | export declare const ensureWithinLimits: (value: number, min: number, max: number) => number; 7 | export interface ElementLayout { 8 | top: number; 9 | right: number; 10 | height: number; 11 | left: number; 12 | width?: number; 13 | } 14 | export declare const isEventPosOnLayout: (event: MouseEvent, layout: ElementLayout) => boolean; 15 | export declare const isEventPosOnDomNode: (event: MouseEvent, domNode: HTMLElement) => boolean; 16 | -------------------------------------------------------------------------------- /dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo page - react-custom-scroll 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /exampleDist/assets/giraffe-icon-4kF3UqUO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rommguy/react-custom-scroll/f68ecfc470524dc4124a0f7954e9bbd930c02e69/exampleDist/assets/giraffe-icon-4kF3UqUO.png -------------------------------------------------------------------------------- /exampleDist/assets/index-B1KVCYZb.css: -------------------------------------------------------------------------------- 1 | #root{margin:0 auto;text-align:center;font-family:Open Sans,sans-serif;background-color:#fff;color:#000}.app-root{max-height:calc(100vh - 70px);max-width:900px;margin:0 auto;padding-top:90px}.demo-title{font-size:32px;margin:24px 0;padding:0 24px}.demo-subtitle{max-width:606px;margin:0 auto 50px;text-align:center;padding:0 24px}.example-wrapper{display:flex;flex-wrap:wrap;justify-content:space-around;align-items:center;margin:70px auto}.example-wrapper:last-child{padding-bottom:70px}.container{text-align:center}.container .side-title{display:block;margin:0 0 20px;font-size:18px}.scroll-creator{background:linear-gradient(to bottom,#bdeafc,#fcbdc9)}.panel{display:inline-block;width:288px;box-shadow:0 0 1px #ddd;border-radius:8px}.panel-header{height:40px;line-height:40px;padding-left:20px;background-color:#333;text-align:left;color:#fff;border-top-left-radius:8px;border-top-right-radius:8px}.panel-content,.outer-container{border-bottom-left-radius:8px;border-bottom-right-radius:8px}.panel-content-native{max-height:525px;overflow:auto}.panel-content-custom{max-height:525px}.content-fill{background:#d6eff5;line-height:20px;font-size:12px;text-align:left;padding:10px 20px}.crazy-scroll .rcs-custom-scrollbar{width:45px}.crazy-scroll .scroll-handle-override{background-color:inherit;background-image:url(http://rommguy.github.io/react-custom-scroll/giraffe-icon.png);background-repeat:no-repeat no-repeat;background-size:cover}.flex-scroll{display:flex;flex-direction:column;height:400px}.dynamic-content{padding:30px 15px;margin-bottom:10px;font-size:14px;background-color:#fff}.example-dynamic-wrapper .panel-content-custom{max-height:250px}.example-dynamic-wrapper .dynamic-content-button{display:block;border:10px;font-family:Roboto,sans-serif;cursor:pointer;outline:none;height:36px;line-height:36px;border-radius:2px;transition:all .45s cubic-bezier(.23,1,.32,1) 0ms;background-color:#00bcd4;text-align:center;font-size:14px;letter-spacing:0px;text-transform:uppercase;margin:10px auto;min-width:200px;-webkit-user-select:none;-moz-user-select:none;user-select:none;padding:0 16px;color:#fff}.example-dynamic-wrapper .dynamic-content-button:hover{background-color:#00bcd499}.example-description{margin:20px 0;font-size:18px;width:248px;padding:0 20px;text-align:start}.example-description img{height:130px;display:block;margin:20px auto}@media only screen and (max-width: 580px){.native-scroll{margin-bottom:35px}.example-wrapper{margin:35px auto}}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scroll-behavior:smooth}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}} 2 | -------------------------------------------------------------------------------- /exampleDist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React custom scroll demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /exampleDist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /giraffe-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rommguy/react-custom-scroll/f68ecfc470524dc4124a0f7954e9bbd930c02e69/giraffe-icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React custom scroll demo 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { CustomScroll } from "./src/customScroll"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-custom-scroll", 3 | "version": "7.0.0", 4 | "private": false, 5 | "type": "module", 6 | "main": "dist/index.umd.js", 7 | "module": "dist/index.es.js", 8 | "types": "dist/src/customScroll.d.ts", 9 | "files": [ 10 | "/dist" 11 | ], 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "dev": "vite", 17 | "build": "tsc && vite build", 18 | "build:example": "tsc && vite build --mode example", 19 | "build:public": "npm run build && npm run build:example", 20 | "prepare": "npm run build", 21 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 22 | "preview": "vite preview", 23 | "test": "playwright test", 24 | "test:ui": "playwright test --ui --project firefox" 25 | }, 26 | "dependencies": { 27 | "@types/lodash": "^4.14.202", 28 | "lodash": "^4.17.21", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "styled-components": "^6.1.8" 32 | }, 33 | "devDependencies": { 34 | "@playwright/test": "^1.50.1", 35 | "@types/node": "^20.11.10", 36 | "@types/react": "^18.2.43", 37 | "@types/react-dom": "^18.2.17", 38 | "@typescript-eslint/eslint-plugin": "^6.14.0", 39 | "@typescript-eslint/parser": "^6.14.0", 40 | "@vitejs/plugin-react-swc": "^3.5.0", 41 | "autoprefixer": "^10.4.17", 42 | "eslint": "^8.55.0", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-react-refresh": "^0.4.5", 45 | "postcss": "^8.4.33", 46 | "prettier": "^3.2.4", 47 | "tailwindcss": "^3.4.1", 48 | "typescript": "^5.2.2", 49 | "vite": "^5.0.8", 50 | "vite-plugin-dts": "^3.7.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | /** 8 | * Read environment variables from file. 9 | * https://github.com/motdotla/dotenv 10 | */ 11 | // require('dotenv').config(); 12 | 13 | /** 14 | * See https://playwright.dev/docs/test-configuration. 15 | */ 16 | export default defineConfig({ 17 | testDir: "./tests", 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: "html", 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | // baseURL: 'http://127.0.0.1:3000', 32 | 33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 34 | trace: "on-first-retry", 35 | }, 36 | 37 | /* Configure projects for major browsers */ 38 | projects: [ 39 | { 40 | name: "chromium", 41 | use: { ...devices["Desktop Chrome"] }, 42 | }, 43 | 44 | { 45 | name: "firefox", 46 | use: { ...devices["Desktop Firefox"] }, 47 | }, 48 | 49 | { 50 | name: "webkit", 51 | use: { ...devices["Desktop Safari"] }, 52 | }, 53 | 54 | /* Test against mobile viewports. */ 55 | // { 56 | // name: 'Mobile Chrome', 57 | // use: { ...devices['Pixel 5'] }, 58 | // }, 59 | // { 60 | // name: 'Mobile Safari', 61 | // use: { ...devices['iPhone 12'] }, 62 | // }, 63 | 64 | /* Test against branded browsers. */ 65 | // { 66 | // name: 'Microsoft Edge', 67 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 68 | // }, 69 | // { 70 | // name: 'Google Chrome', 71 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 72 | // }, 73 | ], 74 | 75 | globalSetup: path.resolve(__dirname, "startServer.ts"), 76 | globalTeardown: path.resolve(__dirname, "stopServer.ts"), 77 | 78 | /* Run your local dev server before starting the tests */ 79 | // webServer: { 80 | // command: 'npm run start', 81 | // url: 'http://127.0.0.1:3000', 82 | // reuseExistingServer: !process.env.CI, 83 | // }, 84 | }); 85 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | margin: 0 auto; 3 | text-align: center; 4 | font-family: "Open Sans", sans-serif; 5 | background-color: white; 6 | color: black; 7 | } 8 | 9 | .app-root { 10 | max-height: calc(100vh - 70px); 11 | max-width: 900px; 12 | margin: 0 auto; 13 | padding-top: 90px; 14 | } 15 | 16 | .demo-title { 17 | font-size: 32px; 18 | margin: 24px 0; 19 | padding: 0 24px; 20 | } 21 | 22 | .demo-subtitle { 23 | max-width: 606px; 24 | margin: 0 auto 50px; 25 | text-align: center; 26 | padding: 0 24px; 27 | } 28 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { DemoComp } from "./example/demoComp.tsx"; 3 | import { CustomScroll } from "./customScroll.tsx"; 4 | 5 | export const App = () => { 6 | return ( 7 | 8 |
9 |
React-Custom-Scroll Demo page
10 |
11 | react-custom-scroll lets you design unique scrollbars without 12 | compromising on performance. It preserves the browser's native 13 | scrolling mechanism, ensuring a smooth, familiar user experience. Its 14 | hover design means no content is obscured, offering a consistent look 15 | across browsers and operating systems. 16 |
17 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/customScroll.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | CSSProperties, 4 | createRef, 5 | UIEvent, 6 | MouseEvent, 7 | PropsWithChildren, 8 | } from "react"; 9 | import styled from "styled-components"; 10 | import { 11 | ElementLayout, 12 | ensureWithinLimits, 13 | isEventPosOnDomNode, 14 | isEventPosOnLayout, 15 | simpleDebounce, 16 | } from "./utils.ts"; 17 | 18 | const CustomScrollbar = styled.div` 19 | position: absolute; 20 | height: 100%; 21 | width: 6px; 22 | right: 3px; 23 | opacity: 0; 24 | z-index: 1; 25 | transition: opacity 0.4s ease-out; 26 | padding: 6px 0; 27 | box-sizing: border-box; 28 | will-change: opacity; 29 | pointer-events: none; 30 | 31 | &.rcs-custom-scrollbar-rtl { 32 | right: auto; 33 | left: 3px; 34 | } 35 | 36 | &.scroll-visible { 37 | opacity: 1; 38 | transition-duration: 0.2s; 39 | } 40 | `; 41 | 42 | const ScrollHandle = styled.div` 43 | height: calc(100% - 12px); 44 | margin-top: 6px; 45 | background-color: rgba(78, 183, 245, 0.7); 46 | border-radius: 3px; 47 | `; 48 | 49 | const CustomScrollRoot = styled.div` 50 | min-height: 0; 51 | min-width: 0; 52 | 53 | & .rcs-outer-container { 54 | overflow: hidden; 55 | 56 | & .rcs-positioning { 57 | position: relative; 58 | } 59 | } 60 | 61 | & .rcs-inner-container { 62 | overflow-x: hidden; 63 | overflow-y: scroll; 64 | -webkit-overflow-scrolling: touch; 65 | 66 | &:after { 67 | content: ""; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | left: 0; 72 | height: 0; 73 | background-image: linear-gradient( 74 | to bottom, 75 | rgba(0, 0, 0, 0.2) 0%, 76 | rgba(0, 0, 0, 0.05) 60%, 77 | rgba(0, 0, 0, 0) 100% 78 | ); 79 | pointer-events: none; 80 | transition: height 0.1s ease-in; 81 | will-change: height; 82 | } 83 | 84 | &.rcs-content-scrolled:after { 85 | height: 5px; 86 | transition: height 0.15s ease-out; 87 | } 88 | } 89 | 90 | &.rcs-scroll-handle-dragged .rcs-inner-container { 91 | user-select: none; 92 | } 93 | 94 | &.rcs-scroll-handle-dragged ${CustomScrollbar} { 95 | opacity: 1; 96 | } 97 | 98 | & .rcs-custom-scroll-handle { 99 | position: absolute; 100 | width: 100%; 101 | top: 0; 102 | } 103 | `; 104 | 105 | interface CustomScrollProps extends PropsWithChildren { 106 | allowOuterScroll?: boolean; 107 | heightRelativeToParent?: string; 108 | onScroll?: (event: UIEvent) => void; 109 | addScrolledClass?: boolean; 110 | freezePosition?: boolean; 111 | handleClass?: string; 112 | minScrollHandleHeight?: number; 113 | flex?: string; 114 | rtl?: boolean; 115 | scrollTo?: number; 116 | keepAtBottom?: boolean; 117 | className?: string; 118 | } 119 | 120 | interface CustomScrollState { 121 | scrollPos: number; 122 | onDrag: boolean; 123 | visible: boolean; 124 | } 125 | 126 | export class CustomScroll extends Component< 127 | CustomScrollProps, 128 | CustomScrollState 129 | > { 130 | scrollbarYWidth: number; 131 | hideScrollThumb: ReturnType; 132 | contentHeight: number = 0; 133 | visibleHeight: number = 0; 134 | scrollHandleHeight: number = 0; 135 | scrollRatio: number = 1; 136 | hasScroll: boolean = false; 137 | startDragHandlePos: number = 0; 138 | startDragMousePos: number = 0; 139 | 140 | constructor(props: CustomScrollProps) { 141 | super(props); 142 | 143 | this.scrollbarYWidth = 0; 144 | this.state = { 145 | scrollPos: 0, 146 | onDrag: false, 147 | visible: false, 148 | }; 149 | 150 | this.hideScrollThumb = simpleDebounce(() => { 151 | this.setState({ 152 | onDrag: false, 153 | }); 154 | }, 500); 155 | } 156 | 157 | componentDidMount() { 158 | if (typeof this.props.scrollTo !== "undefined") { 159 | this.updateScrollPosition(this.props.scrollTo); 160 | } else { 161 | this.forceUpdate(); 162 | } 163 | } 164 | 165 | componentDidUpdate( 166 | prevProps: CustomScrollProps, 167 | prevState: CustomScrollState, 168 | ) { 169 | const prevContentHeight = this.contentHeight; 170 | const prevVisibleHeight = this.visibleHeight; 171 | const innerContainer = this.getScrolledElement(); 172 | const reachedBottomOnPrevRender = 173 | prevState.scrollPos >= prevContentHeight - prevVisibleHeight; 174 | 175 | this.contentHeight = innerContainer.scrollHeight; 176 | this.scrollbarYWidth = 177 | innerContainer.offsetWidth - innerContainer.clientWidth; 178 | this.visibleHeight = innerContainer.clientHeight; 179 | this.scrollRatio = this.contentHeight 180 | ? this.visibleHeight / this.contentHeight 181 | : 1; 182 | 183 | this.toggleScrollIfNeeded(); 184 | const isExternalRender = this.state === prevState; 185 | if (this.props.freezePosition || prevProps.freezePosition) { 186 | this.adjustFreezePosition(prevProps); 187 | } 188 | if ( 189 | typeof this.props.scrollTo !== "undefined" && 190 | this.props.scrollTo !== prevProps.scrollTo 191 | ) { 192 | this.updateScrollPosition(this.props.scrollTo); 193 | } else if ( 194 | this.props.keepAtBottom && 195 | isExternalRender && 196 | reachedBottomOnPrevRender 197 | ) { 198 | this.updateScrollPosition(this.contentHeight - this.visibleHeight); 199 | } 200 | } 201 | 202 | componentWillUnmount() { 203 | this.hideScrollThumb.cancel(); 204 | // @ts-expect-error problem typing event handlers 205 | document.removeEventListener("mousemove", this.onHandleDrag); 206 | // @ts-expect-error problem typing event handlers 207 | document.removeEventListener("mouseup", this.onHandleDragEnd); 208 | } 209 | 210 | customScrollRef = createRef(); 211 | innerContainerRef = createRef(); 212 | customScrollbarRef = createRef(); 213 | scrollHandleRef = createRef(); 214 | contentWrapperRef = createRef(); 215 | 216 | adjustFreezePosition = (prevProps: CustomScrollProps) => { 217 | if (!this.contentWrapperRef.current) { 218 | return; 219 | } 220 | const innerContainer = this.getScrolledElement(); 221 | const contentWrapper = this.contentWrapperRef.current; 222 | 223 | if (this.props.freezePosition) { 224 | contentWrapper.scrollTop = this.state.scrollPos; 225 | } 226 | 227 | if (prevProps.freezePosition) { 228 | innerContainer.scrollTop = this.state.scrollPos; 229 | } 230 | }; 231 | 232 | toggleScrollIfNeeded = () => { 233 | const shouldHaveScroll = this.contentHeight - this.visibleHeight > 1; 234 | if (this.hasScroll !== shouldHaveScroll) { 235 | this.hasScroll = shouldHaveScroll; 236 | this.forceUpdate(); 237 | } 238 | }; 239 | 240 | updateScrollPosition = (scrollValue: number) => { 241 | const innerContainer = this.getScrolledElement(); 242 | const updatedScrollTop = ensureWithinLimits( 243 | scrollValue, 244 | 0, 245 | this.contentHeight - this.visibleHeight, 246 | ); 247 | innerContainer.scrollTop = updatedScrollTop; 248 | this.setState({ 249 | scrollPos: updatedScrollTop, 250 | }); 251 | }; 252 | 253 | onClick = (event: MouseEvent) => { 254 | if ( 255 | !this.hasScroll || 256 | !this.isMouseEventOnCustomScrollbar(event) || 257 | this.isMouseEventOnScrollHandle(event) 258 | ) { 259 | return; 260 | } 261 | const newScrollHandleTop = this.calculateNewScrollHandleTop(event); 262 | const newScrollValue = 263 | this.getScrollValueFromHandlePosition(newScrollHandleTop); 264 | 265 | this.updateScrollPosition(newScrollValue); 266 | }; 267 | 268 | isMouseEventOnCustomScrollbar = (event: MouseEvent) => { 269 | if (!this.customScrollbarRef.current) { 270 | return false; 271 | } 272 | const customScrollElm = this.customScrollRef.current as HTMLElement; 273 | const boundingRect = customScrollElm.getBoundingClientRect(); 274 | const customScrollbarBoundingRect = 275 | this.customScrollbarRef.current.getBoundingClientRect(); 276 | const horizontalClickArea = this.props.rtl 277 | ? { 278 | left: boundingRect.left, 279 | right: customScrollbarBoundingRect.right, 280 | } 281 | : { 282 | left: customScrollbarBoundingRect.left, 283 | width: boundingRect.right, 284 | }; 285 | const customScrollbarLayout: ElementLayout = { 286 | right: boundingRect.right, 287 | top: boundingRect.top, 288 | height: boundingRect.height, 289 | ...horizontalClickArea, 290 | }; 291 | 292 | return isEventPosOnLayout(event, customScrollbarLayout); 293 | }; 294 | 295 | isMouseEventOnScrollHandle = (event: MouseEvent) => { 296 | if (!this.scrollHandleRef.current) { 297 | return false; 298 | } 299 | const scrollHandle = this.scrollHandleRef.current; 300 | return isEventPosOnDomNode(event, scrollHandle); 301 | }; 302 | 303 | calculateNewScrollHandleTop = (clickEvent: MouseEvent) => { 304 | const domNode = this.customScrollRef.current as HTMLElement; 305 | const boundingRect = domNode.getBoundingClientRect(); 306 | const currentTop = boundingRect.top + window.pageYOffset; 307 | const clickYRelativeToScrollbar = clickEvent.pageY - currentTop; 308 | const scrollHandleTop = this.getScrollHandleStyle().top; 309 | let newScrollHandleTop; 310 | const isBelowHandle = 311 | clickYRelativeToScrollbar > scrollHandleTop + this.scrollHandleHeight; 312 | if (isBelowHandle) { 313 | newScrollHandleTop = 314 | scrollHandleTop + 315 | Math.min( 316 | this.scrollHandleHeight, 317 | this.visibleHeight - this.scrollHandleHeight, 318 | ); 319 | } else { 320 | newScrollHandleTop = 321 | scrollHandleTop - Math.max(this.scrollHandleHeight, 0); 322 | } 323 | return newScrollHandleTop; 324 | }; 325 | 326 | getScrollValueFromHandlePosition = (handlePosition: number) => 327 | handlePosition / this.scrollRatio; 328 | 329 | getScrollHandleStyle = (): { height: number; top: number } => { 330 | const handlePosition = this.state.scrollPos * this.scrollRatio; 331 | this.scrollHandleHeight = this.visibleHeight * this.scrollRatio; 332 | return { 333 | height: this.scrollHandleHeight, 334 | top: handlePosition, 335 | }; 336 | }; 337 | 338 | adjustCustomScrollPosToContentPos = (scrollPosition: number) => { 339 | this.setState({ 340 | scrollPos: scrollPosition, 341 | }); 342 | }; 343 | 344 | onScroll = (event: UIEvent) => { 345 | if (this.props.freezePosition) { 346 | return; 347 | } 348 | this.hideScrollThumb(); 349 | this.adjustCustomScrollPosToContentPos(event.currentTarget.scrollTop); 350 | if (this.props.onScroll) { 351 | this.props.onScroll(event); 352 | } 353 | }; 354 | 355 | getScrolledElement = () => this.innerContainerRef.current as HTMLElement; 356 | 357 | onMouseDown = (event: MouseEvent) => { 358 | if (!this.hasScroll || !this.isMouseEventOnScrollHandle(event)) { 359 | return; 360 | } 361 | 362 | this.startDragHandlePos = this.getScrollHandleStyle().top; 363 | this.startDragMousePos = event.pageY; 364 | this.setState({ 365 | onDrag: true, 366 | }); 367 | 368 | // @ts-expect-error problem typing event handlers 369 | document.addEventListener("mousemove", this.onHandleDrag, { 370 | passive: false, 371 | }); 372 | // @ts-expect-error problem typing event handlers 373 | document.addEventListener("mouseup", this.onHandleDragEnd, { 374 | passive: false, 375 | }); 376 | }; 377 | 378 | onTouchStart = () => { 379 | this.setState({ 380 | onDrag: true, 381 | }); 382 | }; 383 | 384 | onHandleDrag = (event: MouseEvent) => { 385 | event.preventDefault(); 386 | const mouseDeltaY = event.pageY - this.startDragMousePos; 387 | const handleTopPosition = ensureWithinLimits( 388 | this.startDragHandlePos + mouseDeltaY, 389 | 0, 390 | this.visibleHeight - this.scrollHandleHeight, 391 | ); 392 | const newScrollValue = 393 | this.getScrollValueFromHandlePosition(handleTopPosition); 394 | this.updateScrollPosition(newScrollValue); 395 | }; 396 | 397 | onHandleDragEnd = (e: MouseEvent) => { 398 | this.setState({ 399 | onDrag: false, 400 | }); 401 | e.preventDefault(); 402 | // @ts-expect-error problem typing event handlers 403 | document.removeEventListener("mousemove", this.onHandleDrag); 404 | // @ts-expect-error problem typing event handlers 405 | document.removeEventListener("mouseup", this.onHandleDragEnd); 406 | }; 407 | 408 | getInnerContainerClasses = () => { 409 | if (this.state.scrollPos && this.props.addScrolledClass) { 410 | return "rcs-inner-container rcs-content-scrolled"; 411 | } 412 | return "rcs-inner-container"; 413 | }; 414 | 415 | getScrollStyles = () => { 416 | const scrollSize = this.scrollbarYWidth || 20; 417 | const marginKey = this.props.rtl ? "marginLeft" : "marginRight"; 418 | const innerContainerStyle: CSSProperties = { 419 | height: 420 | this.props.heightRelativeToParent || this.props.flex ? "100%" : "", 421 | overscrollBehavior: this.props.allowOuterScroll ? "auto" : "none", 422 | }; 423 | innerContainerStyle[marginKey] = -1 * scrollSize; 424 | const contentWrapperStyle: CSSProperties = { 425 | height: 426 | this.props.heightRelativeToParent || this.props.flex ? "100%" : "", 427 | overflowY: this.props.freezePosition ? "hidden" : "visible", 428 | }; 429 | contentWrapperStyle[marginKey] = this.scrollbarYWidth ? 0 : scrollSize; 430 | 431 | return { 432 | innerContainer: innerContainerStyle, 433 | contentWrapper: contentWrapperStyle, 434 | }; 435 | }; 436 | 437 | getOuterContainerStyle = () => ({ 438 | height: this.props.heightRelativeToParent || this.props.flex ? "100%" : "", 439 | }); 440 | 441 | getRootStyles = () => { 442 | const result: CSSProperties = {}; 443 | 444 | if (this.props.heightRelativeToParent) { 445 | result.height = this.props.heightRelativeToParent; 446 | } else if (this.props.flex) { 447 | result.flex = this.props.flex; 448 | } 449 | 450 | return result; 451 | }; 452 | 453 | enforceMinHandleHeight = (calculatedStyle: { 454 | height: number; 455 | top: number; 456 | }) => { 457 | const minHeight = this.props.minScrollHandleHeight || 38; 458 | if (calculatedStyle.height >= minHeight) { 459 | return calculatedStyle; 460 | } 461 | 462 | const diffHeightBetweenMinAndCalculated = 463 | minHeight - calculatedStyle.height; 464 | const scrollPositionToAvailableScrollRatio = 465 | this.state.scrollPos / (this.contentHeight - this.visibleHeight); 466 | const scrollHandlePosAdjustmentForMinHeight = 467 | diffHeightBetweenMinAndCalculated * scrollPositionToAvailableScrollRatio; 468 | const handlePosition = 469 | calculatedStyle.top - scrollHandlePosAdjustmentForMinHeight; 470 | 471 | return { 472 | height: minHeight, 473 | top: handlePosition, 474 | }; 475 | }; 476 | 477 | onMouseEnter = () => { 478 | this.setState({ visible: true }); 479 | }; 480 | 481 | onMouseLeave = () => { 482 | this.setState({ visible: false }); 483 | }; 484 | 485 | render() { 486 | const scrollStyles = this.getScrollStyles(); 487 | const rootStyle = this.getRootStyles(); 488 | const scrollHandleStyle = this.enforceMinHandleHeight( 489 | this.getScrollHandleStyle(), 490 | ); 491 | const className = [ 492 | this.props.className || "", 493 | "rcs-custom-scroll", 494 | this.state.onDrag ? "rcs-scroll-handle-dragged" : "", 495 | ].join(" "); 496 | 497 | return ( 498 | 503 |
513 | {this.hasScroll ? ( 514 |
515 | 521 |
527 | 530 |
531 |
532 |
533 | ) : null} 534 |
541 |
545 | {this.props.children} 546 |
547 |
548 |
549 |
550 | ); 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /src/example/demoComp.css: -------------------------------------------------------------------------------- 1 | .example-wrapper { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-around; 5 | align-items: center; 6 | margin: 70px auto; 7 | } 8 | 9 | .example-wrapper:last-child { 10 | padding-bottom: 70px; 11 | } 12 | 13 | .container { 14 | text-align: center; 15 | } 16 | 17 | .container .side-title { 18 | display: block; 19 | margin: 0 0 20px; 20 | font-size: 18px; 21 | } 22 | 23 | .scroll-creator { 24 | background: linear-gradient(to bottom, #bdeafc, #fcbdc9); 25 | } 26 | 27 | .panel { 28 | display: inline-block; 29 | width: 288px; 30 | box-shadow: 0 0 1px #ddd; 31 | border-radius: 8px; 32 | } 33 | 34 | .panel-header { 35 | height: 40px; 36 | line-height: 40px; 37 | padding-left: 20px; 38 | background-color: #333; 39 | text-align: left; 40 | color: white; 41 | border-top-left-radius: 8px; 42 | border-top-right-radius: 8px; 43 | } 44 | 45 | .panel-content, 46 | .outer-container { 47 | border-bottom-left-radius: 8px; 48 | border-bottom-right-radius: 8px; 49 | } 50 | 51 | .panel-content-native { 52 | max-height: 525px; 53 | overflow: auto; 54 | } 55 | 56 | .panel-content-custom { 57 | max-height: 525px; 58 | } 59 | 60 | .content-fill { 61 | background: #d6eff5; 62 | line-height: 20px; 63 | font-size: 12px; 64 | text-align: left; 65 | padding: 10px 20px; 66 | } 67 | 68 | .crazy-scroll .rcs-custom-scrollbar { 69 | width: 45px; 70 | } 71 | 72 | .crazy-scroll .scroll-handle-override { 73 | background-color: inherit; 74 | background-image: url("http://rommguy.github.io/react-custom-scroll/giraffe-icon.png"); 75 | background-repeat: no-repeat no-repeat; 76 | background-size: cover; 77 | } 78 | 79 | .flex-scroll { 80 | display: flex; 81 | flex-direction: column; 82 | height: 400px; 83 | } 84 | 85 | .dynamic-content { 86 | padding: 30px 15px; 87 | margin-bottom: 10px; 88 | font-size: 14px; 89 | background-color: white; 90 | } 91 | 92 | .example-dynamic-wrapper .panel-content-custom { 93 | max-height: 250px; 94 | } 95 | 96 | .example-dynamic-wrapper .dynamic-content-button { 97 | display: block; 98 | border: 10px; 99 | font-family: Roboto, sans-serif; 100 | cursor: pointer; 101 | padding: 0px; 102 | outline: none; 103 | height: 36px; 104 | line-height: 36px; 105 | border-radius: 2px; 106 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 107 | background-color: rgb(0, 188, 212); 108 | text-align: center; 109 | font-size: 14px; 110 | letter-spacing: 0px; 111 | text-transform: uppercase; 112 | margin: 10px auto; 113 | min-width: 200px; 114 | user-select: none; 115 | padding-left: 16px; 116 | padding-right: 16px; 117 | color: rgb(255, 255, 255); 118 | } 119 | 120 | .example-dynamic-wrapper .dynamic-content-button:hover { 121 | background-color: rgba(0, 188, 212, 0.6); 122 | } 123 | 124 | .example-description { 125 | margin: 20px 0; 126 | font-size: 18px; 127 | width: 248px; 128 | padding: 0 20px; 129 | text-align: start; 130 | } 131 | 132 | .example-description img { 133 | height: 130px; 134 | display: block; 135 | margin: 20px auto; 136 | } 137 | 138 | @media only screen and (max-width: 580px) { 139 | .native-scroll { 140 | margin-bottom: 35px; 141 | } 142 | 143 | .example-wrapper { 144 | margin: 35px auto; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/example/demoComp.tsx: -------------------------------------------------------------------------------- 1 | import { CustomScroll } from "../customScroll.tsx"; 2 | import { demoText } from "./demoText.ts"; 3 | import { times, map } from "lodash/fp"; 4 | import "./demoComp.css"; 5 | import { CSSProperties, useState } from "react"; 6 | import Giraffe from "./giraffe-icon.png"; 7 | 8 | interface DemoCompProps { 9 | demoType: 10 | | "compare-with-native" 11 | | "crazy-designer" 12 | | "flex" 13 | | "dynamic-content" 14 | | "allow-outer-scroll"; 15 | descriptionSide: "left" | "right"; 16 | testId?: string; 17 | } 18 | 19 | export const DemoComp = ({ 20 | demoType, 21 | descriptionSide, 22 | testId, 23 | }: DemoCompProps) => { 24 | const [dynamicContentCounter, setDynamicContentCounter] = useState(4); 25 | const addContent = () => { 26 | setDynamicContentCounter((prev) => prev + 1); 27 | }; 28 | const removeContent = () => { 29 | setDynamicContentCounter((prev) => Math.max(prev - 1, 4)); 30 | }; 31 | const descriptionStyle: CSSProperties = { 32 | flexDirection: descriptionSide === "left" ? "row" : "row-reverse", 33 | }; 34 | switch (demoType) { 35 | case "compare-with-native": 36 | return ( 37 |
42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 |
{demoText.text}
50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 |
60 | 61 |
62 |
{demoText.text}
63 |
64 |
65 |
66 |
67 |
68 | ); 69 | case "crazy-designer": 70 | return ( 71 |
76 |
77 | 78 | There are no limits for your design. 79 |
80 |
81 |
82 | 83 |
84 | 88 |
89 |
{demoText.text}
90 |
91 |
92 |
93 |
94 | ); 95 | case "flex": 96 | return ( 97 |
104 |
105 | Custom scroll supports flexible layouts. You can use it on elements 106 | styled with flex, by passing the flex prop to CustomScroll 107 |
108 |
109 |
110 | 111 |
112 | 113 |
114 |
{demoText.text}
115 |
116 |
117 |
118 |
119 | ); 120 | case "dynamic-content": 121 | return ( 122 |
128 |
129 | When your content is dynamic, you can use the keepAtBottom{" "} 130 | prop, to make sure new content doesn't change the scroll position, 131 | supporting the user experience. 132 |
133 |
134 |
135 |
136 | 137 |
138 | 139 |
140 |
141 | {map( 142 | (content: string) => ( 143 |
144 | {content} 145 |
146 | ), 147 | times( 148 | (index) => `Content #${index}`, 149 | dynamicContentCounter, 150 | ), 151 | )} 152 |
153 |
154 |
155 |
156 | 163 | 170 |
171 |
172 | ); 173 | case "allow-outer-scroll": 174 | return ( 175 |
181 |
182 | In this example, scrolling the wrapping element is enabled with 183 | {`allowOuterScroll`} 184 |
185 |
186 |
187 |
188 | 189 |
190 | 191 |
195 |
{demoText.shortText}
196 |
197 |
198 |
199 |
200 |
201 | ); 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /src/example/demoText.ts: -------------------------------------------------------------------------------- 1 | export const demoText = { 2 | shortText: 3 | "Cras elementum lacus eu dictum vestibulum. Donec eros dui, cursus ut finibus vel, interdum et sem. Sed sed diam dui. Suspendisse at eros non felis faucibus consectetur. Nullam non eleifend sapien. In porttitor est in arcu auctor interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque eu sem euismod, dignissim orci sit amet, facilisis leo. Nulla at tempus sapien. Nunc pharetra eros at ex aliquam rutrum. Nunc quis iaculis nulla. Ut semper nisi in felis aliquam, vitae tincidunt erat tristique. Sed lobortis vulputate enim nec feugiat. Suspendisse maximus purus vitae elementum ullamcorper. Praesent fermentum, odio interdum gravida tempus, orci diam volutpat nisl, in sodales erat felis eget lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean nec egestas lorem. In hac habitasse platea dictumst. Donec laoreet felis id enim tempus, id finibus mauris faucibus. Maecenas sed risus sed quam finibus sollicitudin. Donec dictum id elit in faucibus. Sed pretium cursus tempus. Duis pulvinar, felis sit amet aliquam placerat, dolor risus finibus erat, et convallis velit lacus eget lorem. Etiam bibendum ex ac finibus tincidunt. Fusce elementum semper nunc sodales egestas. Maecenas eu facilisis metus. Suspendisse at eleifend lorem, feugiat tempor ligula. Vivamus dictum metus tortor, et dictum nibh sodales eu. Nulla ut iaculis tellus, eu convallis nulla. Proin mollis dui nec quam accumsan, sed pharetra velit elementum. Suspendisse vitae purus sollicitudin, posuere justo in, mattis nisl. Cras elementum lacus eu dictum vestibulum. " + 4 | "Donec eros dui, cursus ut finibus vel, interdum et sem. Sed sed diam dui. Suspendisse at eros non felis faucibus consectetur. Nullam non eleifend sapien. In porttitor est in arcu auctor interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque eu sem euismod, dignissim orci sit amet, facilisis leo. Nulla at tempus sapien. Nunc pharetra eros at ex aliquam rutrum. Nunc quis iaculis nulla. Ut semper nisi in felis aliquam, vitae tincidunt erat tristique. Sed lobortis vulputate enim nec feugiat. Suspendisse maximus purus vitae elementum ullamcorper. Praesent fermentum, odio interdum gravida tempus, orci diam volutpat nisl, in sodales erat felis eget lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean nec egestas lorem. In hac habitasse platea dictumst. Donec laoreet felis id enim tempus, id finibus mauris faucibus. Maecenas sed risus sed quam finibus sollicitudin. Donec dictum id elit in faucibus. Sed pretium cursus tempus. Duis pulvinar, felis sit amet aliquam placerat, dolor risus finibus erat, et convallis velit lacus eget lorem. Etiam bibendum ex ac finibus tincidunt. Fusce elementum semper nunc sodales egestas. Maecenas eu facilisis metus. Suspendisse at eleifend lorem, feugiat tempor ligula. Vivamus dictum metus tortor, et dictum nibh sodales eu. Nulla ut iaculis tellus, eu convallis nulla. Proin mollis dui nec quam accumsan, sed pharetra velit elementum. Suspendisse vitae purus sollicitudin, posuere justo in, mattis nisl. Cras elementum lacus eu dictum vestibulum. ", 5 | text: "Cras elementum lacus eu dictum vestibulum. Donec eros dui, cursus ut finibus vel, interdum et sem. Sed sed diam dui. Suspendisse at eros non felis faucibus consectetur. Nullam non eleifend sapien. In porttitor est in arcu auctor interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque eu sem euismod, dignissim orci sit amet, facilisis leo. Nulla at tempus sapien. Nunc pharetra eros at ex aliquam rutrum. Nunc quis iaculis nulla. Ut semper nisi in felis aliquam, vitae tincidunt erat tristique. Sed lobortis vulputate enim nec feugiat. Suspendisse maximus purus vitae elementum ullamcorper. Praesent fermentum, odio interdum gravida tempus, orci diam volutpat nisl, in sodales erat felis eget lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean nec egestas lorem. In hac habitasse platea dictumst. Donec laoreet felis id enim tempus, id finibus mauris faucibus. Maecenas sed risus sed quam finibus sollicitudin. Donec dictum id elit in faucibus. Sed pretium cursus tempus. Duis pulvinar, felis sit amet aliquam placerat, dolor risus finibus erat, et convallis velit lacus eget lorem. Etiam bibendum ex ac finibus tincidunt. Fusce elementum semper nunc sodales egestas. Maecenas eu facilisis metus. Suspendisse at eleifend lorem, feugiat tempor ligula. Vivamus dictum metus tortor, et dictum nibh sodales eu. Nulla ut iaculis tellus, eu convallis nulla. Proin mollis dui nec quam accumsan, sed pharetra velit elementum. Suspendisse vitae purus sollicitudin, posuere justo in, mattis nisl. Cras elementum lacus eu dictum vestibulum. Donec eros dui, cursus ut finibus vel, interdum et sem. Sed sed diam dui. Suspendisse at eros non felis faucibus consectetur. Nullam non eleifend sapien. In porttitor est in arcu auctor interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque eu sem euismod, dignissim orci sit amet, facilisis leo. Nulla at tempus sapien. Nunc pharetra eros at ex aliquam rutrum. Nunc quis iaculis nulla. Ut semper nisi in felis aliquam, vitae tincidunt erat tristique. Sed lobortis vulputate enim nec feugiat. Suspendisse maximus purus vitae elementum ullamcorper. Praesent fermentum, odio interdum gravida tempus, orci diam volutpat nisl, in sodales erat felis eget lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean nec egestas lorem. In hac habitasse platea dictumst. Donec laoreet felis id enim tempus, id finibus mauris faucibus. Maecenas sed risus sed quam finibus sollicitudin. Donec dictum id elit in faucibus. Sed pretium cursus tempus. Duis pulvinar, felis sit amet aliquam placerat, dolor risus finibus erat, et convallis velit lacus eget lorem. Etiam bibendum ex ac finibus tincidunt. Fusce elementum semper nunc sodales egestas. Maecenas eu facilisis metus. Suspendisse at eleifend lorem, feugiat tempor ligula. Vivamus dictum metus tortor, et dictum nibh sodales eu. Nulla ut iaculis tellus, eu convallis nulla. Proin mollis dui nec quam accumsan, sed pharetra velit elementum. Suspendisse vitae purus sollicitudin, posuere justo in, mattis nisl. Cras elementum lacus eu dictum vestibulum. Donec eros dui, cursus ut finibus vel, interdum et sem. Sed sed diam dui. Suspendisse at eros non felis faucibus consectetur. Nullam non eleifend sapien. In porttitor est in arcu auctor interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque eu sem euismod, dignissim orci sit amet, facilisis leo. Nulla at tempus sapien. Nunc pharetra eros at ex aliquam rutrum. Nunc quis iaculis nulla. Ut semper nisi in felis aliquam, vitae tincidunt erat tristique. Sed lobortis vulputate enim nec feugiat. Suspendisse maximus purus vitae elementum ullamcorper. Praesent fermentum, odio interdum gravida tempus, orci diam volutpat nisl, in sodales erat felis eget lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean nec egestas lorem. In hac habitasse platea dictumst. Donec laoreet felis id eni.", 6 | }; 7 | -------------------------------------------------------------------------------- /src/example/giraffe-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rommguy/react-custom-scroll/f68ecfc470524dc4124a0f7954e9bbd930c02e69/src/example/giraffe-icon.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | 15 | scroll-behavior: smooth; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent } from "react"; 2 | 3 | export const simpleDebounce = (func: () => void, delay: number) => { 4 | let timer: ReturnType; 5 | 6 | function cancel() { 7 | clearTimeout(timer); 8 | } 9 | 10 | function debounced() { 11 | cancel(); 12 | timer = setTimeout(() => { 13 | func(); 14 | }, delay); 15 | } 16 | 17 | debounced.cancel = cancel; 18 | return debounced; 19 | }; 20 | 21 | export const ensureWithinLimits = (value: number, min: number, max: number) => { 22 | min = !min && min !== 0 ? value : min; 23 | max = !max && max !== 0 ? value : max; 24 | if (min > max) { 25 | console.error("min limit is greater than max limit"); 26 | return value; 27 | } 28 | if (value < min) { 29 | return min; 30 | } 31 | if (value > max) { 32 | return max; 33 | } 34 | return value; 35 | }; 36 | 37 | export interface ElementLayout { 38 | top: number; 39 | right: number; 40 | height: number; 41 | left: number; 42 | width?: number; 43 | } 44 | 45 | export const isEventPosOnLayout = (event: MouseEvent, layout: ElementLayout) => 46 | event.clientX > layout.left && 47 | event.clientX < layout.right && 48 | event.clientY > layout.top && 49 | event.clientY < layout.top + layout.height; 50 | 51 | export const isEventPosOnDomNode = ( 52 | event: MouseEvent, 53 | domNode: HTMLElement, 54 | ) => { 55 | const nodeClientRect = domNode.getBoundingClientRect(); 56 | return isEventPosOnLayout(event, nodeClientRect); 57 | }; 58 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /startServer.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | 3 | const startServer = async () => { 4 | // @ts-expect-error not needed 5 | global.__VITE_SERVER__ = exec("npm run dev"); // Replace 'npm run dev' with your Vite start script 6 | await new Promise((resolve) => setTimeout(resolve, 4000)); // Wait for the server to start 7 | }; 8 | 9 | export default startServer; 10 | -------------------------------------------------------------------------------- /stopServer.ts: -------------------------------------------------------------------------------- 1 | const stopServer = async () => { 2 | // @ts-expect-error not needed 3 | global.__VITE_SERVER__.kill(); 4 | }; 5 | 6 | export default stopServer; 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/customScroll.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { 3 | assertCustomScrollBarVisible, 4 | assertDomElementProperty, 5 | getExamplePanel, 6 | getInnerContainer, 7 | getScrollHandle, 8 | } from "./customScrollDriver"; 9 | 10 | const APP_URL = "http://localhost:5174/"; 11 | 12 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 13 | 14 | test.describe("basic functionality", () => { 15 | test.beforeEach(async ({ page }) => { 16 | await page.goto(APP_URL); 17 | }); 18 | 19 | test("Custom scrollbar appears when hovering the container", async ({ 20 | page, 21 | }) => { 22 | const examplePanel = getExamplePanel(page); 23 | await examplePanel.getByTestId("outer-container").hover(); 24 | 25 | await assertCustomScrollBarVisible(examplePanel); 26 | }); 27 | 28 | test("Updates the position of the scroll handle when scrolling", async ({ 29 | page, 30 | }) => { 31 | const examplePanel = getExamplePanel(page); 32 | await examplePanel.getByTestId("outer-container").hover(); 33 | 34 | await assertDomElementProperty( 35 | getScrollHandle(examplePanel), 36 | "offsetTop", 37 | 0, 38 | ); 39 | 40 | await page.mouse.wheel(0, 100); 41 | await sleep(500); 42 | 43 | await assertDomElementProperty( 44 | getInnerContainer(examplePanel), 45 | "scrollTop", 46 | 100, 47 | ); 48 | 49 | await assertDomElementProperty( 50 | getScrollHandle(examplePanel), 51 | "offsetTop", 52 | 28, 53 | ); 54 | }); 55 | }); 56 | 57 | // test.describe("mouse interactions with custom scrollbar", () => { 58 | // test.beforeEach(async ({ page }) => { 59 | // await page.goto(APP_URL); 60 | // }); 61 | // 62 | // test("Should scroll when clicking on the scrollbar area", async ({ 63 | // page, 64 | // }) => { 65 | // const examplePanel = getExamplePanel(page); 66 | // 67 | // const customHandle = getScrollHandle(examplePanel); 68 | // await customHandle.hover(); 69 | // // click below the handle 70 | // page.mouse.click(0, 50); 71 | // 72 | // // check the scroll moved downwards 73 | // }); 74 | // }); 75 | 76 | // test.describe("Blocking outer scroll", () => { 77 | // test.beforeEach(async ({ page }) => { 78 | // await page.goto(APP_URL); 79 | // }); 80 | // test("should block outer scroll when reaching the end of the scroll range", async ({ 81 | // page, 82 | // }) => { 83 | // const documentElement = await getDocumentElement(page); 84 | // 85 | // const examplePanel = page.getByTestId("first-example"); 86 | // await examplePanel.getByTestId("outer-container").click(); 87 | // 88 | // await assertDomElementProperty(documentElement, "scrollTop", 114); 89 | // 90 | // await page.mouse.wheel(0, 4000); 91 | // await sleep(500); 92 | // await page.mouse.wheel(0, -4000); 93 | // 94 | // await assertDomElementProperty(documentElement, "scrollTop", 200); 95 | // }); 96 | // }); 97 | -------------------------------------------------------------------------------- /tests/customScrollDriver.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator, Page } from "@playwright/test"; 2 | 3 | export const getCustomScrollbar = (container: Locator) => 4 | container.getByTestId("custom-scrollbar"); 5 | 6 | export const assertCustomScrollBarVisible = async (container: Locator) => { 7 | await expect(getCustomScrollbar(container)).toBeVisible(); 8 | await expect(getCustomScrollbar(container)).toHaveCSS("opacity", "1"); 9 | }; 10 | 11 | export const getAppBody = (page: Page) => page.content(); 12 | 13 | export const getInnerContainer = (container: Locator) => 14 | container.getByTestId("inner-container"); 15 | 16 | export const getScrollHandle = (container: Locator) => 17 | container.getByTestId("custom-scroll-handle"); 18 | 19 | export const getExamplePanel = (page: Page) => 20 | page.getByTestId("first-example"); 21 | 22 | export const assertDomElementProperty = async ( 23 | element: Locator, 24 | elmProperty: "scrollTop" | "offsetTop", 25 | expectedValue: number, 26 | ) => { 27 | expect( 28 | await element.evaluate( 29 | // @ts-expect-error missing type 30 | (node, elmProperty) => node[elmProperty], 31 | elmProperty, 32 | ), 33 | ).toBe(expectedValue); 34 | }; 35 | 36 | export const getDocumentElement = (page: Page) => 37 | // @ts-expect-error missing type 38 | page.evaluateHandle(() => document.documentElement); 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "declaration": true, 16 | "typeRoots": ["./dist/index.d.ts"], 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import path from "path"; 4 | import dts from "vite-plugin-dts"; 5 | 6 | export default defineConfig(({ mode }) => { 7 | const isProdBuild = mode !== "example"; 8 | 9 | if (!isProdBuild) { 10 | return { 11 | plugins: [react()], 12 | build: { outDir: "exampleDist" }, 13 | base: "react-custom-scroll/exampleDist", 14 | }; 15 | } 16 | 17 | return { 18 | build: { 19 | lib: { 20 | entry: path.resolve(__dirname, "index.ts"), 21 | name: "react-custom-scroll", 22 | fileName: (format) => `index.${format}.js`, 23 | }, 24 | rollupOptions: { 25 | external: ["react", "react-dom"], 26 | output: { 27 | globals: { react: "React", "react-dom": "ReactDOM" }, 28 | }, 29 | }, 30 | sourcemap: true, 31 | emptyOutDir: true, 32 | }, 33 | plugins: [react(), dts({ insertTypesEntry: true })], 34 | server: { 35 | port: 5174, 36 | }, 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /vite.config.ts.timestamp-1706665317733-460586ad0276b.mjs: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from "file:///Users/guyromm/myprojects/react-custom-scroll-revamp/node_modules/vite/dist/node/index.js"; 3 | import react from "file:///Users/guyromm/myprojects/react-custom-scroll-revamp/node_modules/@vitejs/plugin-react-swc/index.mjs"; 4 | import path from "path"; 5 | import dts from "file:///Users/guyromm/myprojects/react-custom-scroll-revamp/node_modules/vite-plugin-dts/dist/index.mjs"; 6 | var __vite_injected_original_dirname = "/Users/guyromm/myprojects/react-custom-scroll-revamp"; 7 | var vite_config_default = defineConfig({ 8 | build: { 9 | lib: { 10 | entry: path.resolve(__vite_injected_original_dirname, "index.ts"), 11 | name: "react-custom-scroll", 12 | fileName: (format) => `index.${format}.js` 13 | }, 14 | rollupOptions: { 15 | external: ["react", "react-dom"], 16 | output: { 17 | globals: { 18 | react: "React", 19 | "react-dom": "ReactDOM" 20 | } 21 | } 22 | }, 23 | sourcemap: true, 24 | emptyOutDir: true 25 | }, 26 | plugins: [react(), dts()] 27 | }); 28 | export { 29 | vite_config_default as default 30 | }; 31 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvZ3V5cm9tbS9teXByb2plY3RzL3JlYWN0LWN1c3RvbS1zY3JvbGwtcmV2YW1wXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvZ3V5cm9tbS9teXByb2plY3RzL3JlYWN0LWN1c3RvbS1zY3JvbGwtcmV2YW1wL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ndXlyb21tL215cHJvamVjdHMvcmVhY3QtY3VzdG9tLXNjcm9sbC1yZXZhbXAvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IHJlYWN0IGZyb20gXCJAdml0ZWpzL3BsdWdpbi1yZWFjdC1zd2NcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5pbXBvcnQgZHRzIGZyb20gXCJ2aXRlLXBsdWdpbi1kdHNcIjtcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIGJ1aWxkOiB7XG4gICAgbGliOiB7XG4gICAgICBlbnRyeTogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgXCJpbmRleC50c1wiKSxcbiAgICAgIG5hbWU6IFwicmVhY3QtY3VzdG9tLXNjcm9sbFwiLFxuICAgICAgZmlsZU5hbWU6IChmb3JtYXQpID0+IGBpbmRleC4ke2Zvcm1hdH0uanNgLFxuICAgIH0sXG4gICAgcm9sbHVwT3B0aW9uczoge1xuICAgICAgZXh0ZXJuYWw6IFtcInJlYWN0XCIsIFwicmVhY3QtZG9tXCJdLFxuICAgICAgb3V0cHV0OiB7XG4gICAgICAgIGdsb2JhbHM6IHtcbiAgICAgICAgICByZWFjdDogXCJSZWFjdFwiLFxuICAgICAgICAgIFwicmVhY3QtZG9tXCI6IFwiUmVhY3RET01cIixcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgZW1wdHlPdXREaXI6IHRydWUsXG4gIH0sXG4gIHBsdWdpbnM6IFtyZWFjdCgpLCBkdHMoKV0sXG59KTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOFUsU0FBUyxvQkFBb0I7QUFDM1csT0FBTyxXQUFXO0FBQ2xCLE9BQU8sVUFBVTtBQUNqQixPQUFPLFNBQVM7QUFIaEIsSUFBTSxtQ0FBbUM7QUFNekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsT0FBTztBQUFBLElBQ0wsS0FBSztBQUFBLE1BQ0gsT0FBTyxLQUFLLFFBQVEsa0NBQVcsVUFBVTtBQUFBLE1BQ3pDLE1BQU07QUFBQSxNQUNOLFVBQVUsQ0FBQyxXQUFXLFNBQVMsTUFBTTtBQUFBLElBQ3ZDO0FBQUEsSUFDQSxlQUFlO0FBQUEsTUFDYixVQUFVLENBQUMsU0FBUyxXQUFXO0FBQUEsTUFDL0IsUUFBUTtBQUFBLFFBQ04sU0FBUztBQUFBLFVBQ1AsT0FBTztBQUFBLFVBQ1AsYUFBYTtBQUFBLFFBQ2Y7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBLElBQ0EsV0FBVztBQUFBLElBQ1gsYUFBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFNBQVMsQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDO0FBQzFCLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== 32 | --------------------------------------------------------------------------------