159 | )
160 |
161 | const ScrollIndicator = () => (
162 |
163 | {({scrollTop, scrollHeight, clientHeight}) => (
164 |
165 |
177 | top {scrollTop} {scrollHeight}
178 |
179 |
191 | bottom
192 |
193 |
194 | )}
195 |
196 | );
197 |
198 | export default class App extends React.Component <{}, AppState> {
199 | state: AppState = {}
200 |
201 | render() {
202 | return (
203 |
204 | max-height
205 |
206 | }>
207 |
208 |
209 |
210 |
211 |
212 | Simple
213 |
214 |
215 |
216 |
220 |
221 |
222 |
223 | {1 &&
224 | Outer
225 |
226 |
227 |
228 |
229 |
230 |
234 |
235 |
236 |
237 |
238 |
239 | Container
240 |
241 |
242 |
243 |
244 |
245 |
246 | Container + GAP
247 |
248 |
249 |
250 |
251 |
252 |
253 | Nyan Container
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 | Draggable
262 |
263 |
264 |
265 |
269 |
270 |
271 |
272 | Custom Bar
273 |
274 |
275 |
276 |
277 | {/*
*/}
278 |
dragging ? 16 : 8}
282 | draggable
283 | />
284 |
285 |
286 |
287 |
288 | In hidden block
289 |
290 |
297 |
298 |
299 | Horizontal
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 | }
312 |
313 |
314 |
315 | )
316 | }
317 | }
--------------------------------------------------------------------------------
/example/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theKashey/React-stroller/697ba0df7ce22c9e6eb5c05efd32cca47c7c72f0/example/assets/.gitkeep
--------------------------------------------------------------------------------
/example/cat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theKashey/React-stroller/697ba0df7ce22c9e6eb5c05efd32cca47c7c72f0/example/cat.gif
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import App from './app';
4 |
5 | ReactDOM.render(, document.getElementById('app'));
6 |
--------------------------------------------------------------------------------
/example/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const AppWrapper = styled.div`
4 |
5 | `;
--------------------------------------------------------------------------------
/example/utils.ts:
--------------------------------------------------------------------------------
1 | import {Component} from 'react';
2 |
3 | export class ToolboxApp extends Component
{
4 | onCheckboxChange = (propName: any) => () => {
5 | const currentValue = (this.state as any)[propName];
6 | this.setState({ [propName]: !currentValue } as any);
7 | }
8 |
9 | onFieldTextChange = (propName: any) => (e: any) => {
10 | const value = e.target.value;
11 |
12 | (this as any).setState({
13 | [propName]: value
14 | });
15 | }
16 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-stroller",
3 | "version": "1.8.1",
4 | "description": "React custom scrollbars, I mean strollbars",
5 | "scripts": {
6 | "test": "ts-react-toolbox test",
7 | "bootstrap": "ts-react-toolbox init",
8 | "dev": "ts-react-toolbox dev",
9 | "test:ci": "ts-react-toolbox test --runInBand --coverage",
10 | "test:size": "size-limit",
11 | "build": "ts-react-toolbox build",
12 | "prepublish": "ts-react-toolbox build",
13 | "release": "ts-react-toolbox release",
14 | "lint": "ts-react-toolbox lint",
15 | "static": "ts-react-toolbox publish",
16 | "format": "ts-react-toolbox format",
17 | "analyze": "ts-react-toolbox analyze"
18 | },
19 | "devDependencies": {
20 | "@size-limit/preset-small-lib": "^4.10.1",
21 | "@testing-library/react": "^11.2.5",
22 | "prettier": "^2.2.1",
23 | "react": "^16.8.6",
24 | "react-dom": "^16.8.6",
25 | "size-limit": "^4.10.1",
26 | "styled-components": "^5.2.1",
27 | "ts-react-toolbox": "^1.1.1"
28 | },
29 | "engines": {
30 | "node": ">=10.24.0"
31 | },
32 | "peerDependencies": {
33 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0 "
34 | },
35 | "files": [
36 | "dist"
37 | ],
38 | "repository": {
39 | "type": "git",
40 | "url": "git+https://github.com/theKashey/react-stroller.git"
41 | },
42 | "author": "theKashey ",
43 | "license": "MIT",
44 | "keywords": [
45 | "react",
46 | "scroll",
47 | "scrollbar",
48 | "custom scrollbar"
49 | ],
50 | "dependencies": {
51 | "detect-passive-events": "^2.0.3",
52 | "faste": "^1.0.3",
53 | "tslib": "^2.1.0"
54 | },
55 | "types": "dist/es5/index.d.ts",
56 | "jsnext:main": "dist/es2015/index.js",
57 | "module": "dist/es2015/index.js",
58 | "main": "dist/es5/index.js",
59 | "size-limit": [
60 | {
61 | "path": "dist/es2015/index.js",
62 | "limit": "5.50 KB"
63 | }
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/src/Bar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {axisToAxis, axisTypes} from "./utils";
3 |
4 | export type BarView = React.ComponentType<{ dragging?: boolean, axis?: axisTypes }> | React.ComponentType;
5 |
6 | export type BarSizeFunction = (height: number, scrollHeight: number, flags: { dragging: boolean, default: number }) => number;
7 |
8 | export type BarLocation = "fixed" | "inside" | "outside";
9 |
10 | export interface IScrollParams {
11 | scrollSpace: number;
12 | scroll: number;
13 | space: number;
14 | targetSpace: number;
15 | }
16 |
17 | export type ISideBar = React.SFC<{ styles: CSSStyleDeclaration }>;
18 |
19 | export interface IStrollerBarProps {
20 | mainScroll: IScrollParams;
21 | targetScroll?: IScrollParams;
22 |
23 | forwardRef: (ref: HTMLElement) => void;
24 | internal?: BarView;
25 | axis?: axisTypes,
26 | targetAxis?: axisTypes,
27 | oppositePosition?: boolean;
28 | draggable?: boolean;
29 | dragging?: boolean;
30 | sizeFunction?: BarSizeFunction;
31 | location: BarLocation;
32 |
33 | className?: string;
34 | SideBar?: ISideBar;
35 | }
36 |
37 | const Bar: BarView = ({axis}: { axis: axisTypes }) => (
38 |
46 | );
47 |
48 | const positions = {
49 | vertical: {
50 | 0: {
51 | top: 0,
52 | right: 0,
53 | },
54 | 1: {
55 | top: 0,
56 | left: 0,
57 | }
58 | },
59 |
60 | horizontal: {
61 | 0: {
62 | bottom: 0,
63 | left: 0,
64 | },
65 | 1: {
66 | top: 0,
67 | left: 0,
68 | },
69 | }
70 | };
71 |
72 | export const defaultSizeFunction = (height: number, scrollHeight: number): number => (
73 | height * (height / scrollHeight)
74 | );
75 |
76 | export const StollerBar: React.SFC = ({
77 | mainScroll,
78 | // targetScroll,
79 | SideBar,
80 |
81 | forwardRef,
82 | internal,
83 | axis = 'vertical',
84 | oppositePosition = false,
85 | draggable = false,
86 | sizeFunction = defaultSizeFunction,
87 | dragging = false,
88 | location,
89 | className
90 | }) => {
91 | if (mainScroll.scrollSpace <= mainScroll.space) {
92 | return null;
93 | }
94 |
95 | const barSize = sizeFunction(mainScroll.space, mainScroll.scrollSpace, {
96 | dragging,
97 | default: defaultSizeFunction(mainScroll.space, mainScroll.scrollSpace)
98 | });
99 |
100 | const Internal: BarView = internal || Bar;
101 |
102 | const usableSpace = (mainScroll.scrollSpace - mainScroll.space);
103 |
104 | const endPosition = location === 'inside'
105 | ? (mainScroll.scrollSpace - barSize)
106 | : (mainScroll.targetSpace - barSize);
107 |
108 | const top = endPosition * mainScroll.scroll / usableSpace;
109 |
110 | const transform = 'translate' + (axisToAxis[axis]) + '(' + (Math.max(0, Math.min(endPosition, top))) + 'px)';
111 |
112 | const styles = {
113 | position: location === 'fixed' ? 'fixed' : 'absolute',
114 | display: 'flex',
115 | cursor: dragging ? 'grabbing' : (draggable ? 'grab' : 'default'),
116 |
117 | [axis === 'vertical' ? 'height' : 'width']: Math.round(barSize) + 'px',
118 |
119 | ...(positions[axis][oppositePosition ? 1 : 0] as any)
120 | };
121 |
122 | return (
123 |
124 | {SideBar && }
125 |
135 |
136 |
137 |
138 | );
139 | }
--------------------------------------------------------------------------------
/src/Container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {axisToOverflow, axisTypes, getScrollBarWidth} from "./utils";
3 |
4 | export interface IContainerProps {
5 | axis?: axisTypes;
6 | className?: string;
7 | overscroll?: boolean;
8 | gap?: number;
9 | minScrollbarWidth?: number;
10 | containerStyles?: CSSStyleDeclaration;
11 | }
12 |
13 | const getStyle = (scrollWidth: number, gap: number, overscroll: boolean, axis: axisTypes = 'vertical'): React.CSSProperties => {
14 | return {
15 | width: axis === 'vertical' ? `calc(100% + ${scrollWidth - gap}px)` : '100%',
16 | height: axis !== 'vertical' ? `calc(100% + ${scrollWidth - gap}px)` : '100%',
17 | // width:'100%',
18 | // height:'100%',
19 | maxWidth: 'inherit',
20 | maxHeight: 'inherit',
21 | position: 'relative',
22 | [axisToOverflow[axis]]: 'scroll',
23 | overscrollBehavior: overscroll ? 'contain' : 'inherit',
24 | [axis === 'vertical' ? 'paddingRight' : 'paddingBottom']: (scrollWidth + 24) + 'px',
25 | boxSizing: "content-box",
26 | }
27 | };
28 |
29 | const containerStyle: React.CSSProperties = {
30 | height: '100%',
31 | width: '100%',
32 | overflow: 'hidden',
33 | maxWidth: 'inherit',
34 | maxHeight: 'inherit',
35 | };
36 |
37 | export const strollerStyle: React.CSSProperties = {
38 | height: '100%',
39 | width: '100%',
40 | maxWidth: 'inherit',
41 | maxHeight: 'inherit',
42 | // display: 'inline-block', // WHY?
43 | };
44 |
45 | export const subcontainerStyle: React.CSSProperties = {
46 | // minHeight: '100%', // an issue for windows
47 | // minWidth: '100%',
48 | width: '100%',
49 | height: '100%',
50 | // maxWidth: 'inherit',
51 | // maxHeight: 'inherit',
52 | position: 'relative',
53 | // display: 'inline-block', // WHY?
54 | };
55 |
56 | export class Strollable extends React.Component {
57 | scrollWidth = getScrollBarWidth();
58 |
59 | render() {
60 | const {children, axis, overscroll = false, className, gap = 0, minScrollbarWidth = 0, containerStyles = {}} = this.props;
61 | return (
62 |
63 |
64 |
65 | {children}
66 |
67 |
68 |
69 | );
70 | }
71 | }
--------------------------------------------------------------------------------
/src/DragEngine.ts:
--------------------------------------------------------------------------------
1 | import {AnyHookCallback, faste, HookCallback, Faste, InternalMachine, MessageHandler} from 'faste';
2 |
3 | export {
4 | Faste,
5 | InternalMachine,
6 | MessageHandler
7 | };
8 |
9 | const nodeHook: AnyHookCallback = {
10 | on: ({attrs, trigger, message}) => {
11 | const hook = (event: any) => {
12 | trigger(message, event);
13 | event.preventDefault();
14 | };
15 | attrs.node.addEventListener(message, hook);
16 | return [attrs.node, hook]
17 | },
18 | off: ({message}, [node, hook]) => {
19 | node.removeEventListener(message, hook)
20 | }
21 | };
22 |
23 | const documentHook: HookCallback = {
24 | on: ({trigger, message}) => {
25 | const hook = (event: any) => {
26 | trigger(message, event);
27 | event.preventDefault();
28 | };
29 | document.addEventListener(message, hook, true);
30 | return hook
31 | },
32 | off: ({message}, hook) => {
33 | document.removeEventListener(message, hook, true)
34 | }
35 | };
36 |
37 | const getCoords = (event: MouseEvent | Touch) => [event.clientX, event.clientY];
38 |
39 | export const DragMachine = faste()
40 | .withPhases(['init', 'disabled', 'idle', 'dragging', 'cancelDrag'])
41 | .withAttrs<{ node?: HTMLElement, enabled?: boolean }>({})
42 | .withMessages(['check', 'down', 'up', 'move', 'mousedown', 'mouseup', 'mousemove', 'touchstart', 'touchmove', 'touchend'])
43 | .withSignals(['up', 'down', 'move'])
44 |
45 | .on('check', ['init', 'disabled'], ({attrs, transitTo}) => attrs.enabled && attrs.node && transitTo('idle'))
46 | .on('check', ['idle', 'dragging'], ({attrs, transitTo}) => (!attrs.enabled || !attrs.node) && transitTo('disabled'))
47 |
48 | // outer reactions
49 | .on('down', ({transitTo, emit}, event) => {
50 | emit('down', event);
51 | transitTo('dragging')
52 | })
53 | .on('up', ({transitTo}) => transitTo('idle'))
54 | .on('move', ({emit}, event) => emit('move', event))
55 | .on('@enter', ['cancelDrag'], ({transitTo}) => transitTo('idle'))
56 |
57 | // mouse events
58 | .on('mousedown', ['idle'], ({trigger}, event: MouseEvent) => trigger('down', getCoords(event)))
59 | .on('mouseup', ['dragging'], ({trigger}) => trigger('up'))
60 | .on('mousemove', ['dragging'], ({transitTo}, event: MouseEvent) => event.buttons !== 1 && transitTo('cancelDrag'))
61 | .on('mousemove', ['dragging'], ({trigger}, event: MouseEvent) => trigger('move', getCoords(event)))
62 |
63 | // touch events
64 | .on('touchstart', ['idle'], ({trigger}, event: TouchEvent) => trigger('down', getCoords(event.touches[0])))
65 | .on('touchend', ['dragging'], ({trigger}) => trigger('up'))
66 | .on('touchmove', ['dragging'], ({trigger}, event: TouchEvent) => trigger('move', getCoords(event.touches[0])))
67 |
68 | .hooks({
69 | mousedown: nodeHook,
70 | mouseup: documentHook,
71 | mousemove: documentHook,
72 |
73 | touchstart: nodeHook,
74 | touchmove: documentHook,
75 | touchend: documentHook,
76 | });
--------------------------------------------------------------------------------
/src/StrollCaptor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {StrollerContext} from "./context";
3 |
4 | export const StrollCaptor = () => (
5 |
6 | {({setScrollContainer}) => (
7 | setScrollContainer(ref)}/>
8 | )}
9 |
10 | );
--------------------------------------------------------------------------------
/src/StrollableContainer.tsx:
--------------------------------------------------------------------------------
1 | import {IContainerProps, Strollable, strollerStyle} from "./Container";
2 | import {Stroller, IStrollerProps} from "./Stroller";
3 | import * as React from "react";
4 | import {StrollCaptor} from "./StrollCaptor";
5 |
6 | export type Props = IContainerProps & IStrollerProps;
7 |
8 | export const StrollableContainer: React.SFC
= (
9 | {
10 | children,
11 | className,
12 |
13 | axis,
14 | bar,
15 | inBetween,
16 |
17 | scrollBar,
18 | oppositePosition,
19 | draggable,
20 |
21 | barSizeFunction,
22 | barClassName,
23 | SideBar,
24 |
25 | overrideLocation,
26 | targetAxis,
27 |
28 | overscroll,
29 | containerStyles,
30 | minScrollbarWidth,
31 |
32 | scrollKey,
33 | gap,
34 |
35 | onScroll
36 | }
37 | ) => (
38 |
39 |
60 |
68 |
69 | {children}
70 |
71 |
72 |
73 | )
--------------------------------------------------------------------------------
/src/Stroller.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {supportsPassiveEvents} from 'detect-passive-events';
3 |
4 | import {axisToProps, axisTypes, extractValues, findScrollableParent} from "./utils";
5 | import {BarLocation, BarSizeFunction, BarView, StollerBar, IStrollerBarProps, ISideBar} from "./Bar";
6 | import {DragMachine} from "./DragEngine";
7 |
8 | import {StrollerProvider, StrollerStateProvider} from './context';
9 | import {strollerStyle} from "./Container";
10 |
11 | export interface IStrollerProps {
12 | axis?: axisTypes;
13 | targetAxis?: axisTypes;
14 |
15 | inBetween?: React.ReactNode;
16 | bar?: BarView,
17 | scrollBar?: React.ComponentType;
18 | barSizeFunction?: BarSizeFunction;
19 | barClassName?: string,
20 | SideBar?: ISideBar;
21 |
22 | oppositePosition?: boolean,
23 | draggable?: boolean
24 |
25 | overrideLocation?: BarLocation;
26 |
27 | scrollKey?: any;
28 |
29 | passive?: boolean;
30 |
31 | onScroll?: (e: Event) => void;
32 | }
33 |
34 | export interface IComponentState {
35 | scrollWidth: number;
36 | scrollHeight: number;
37 |
38 | clientWidth: number;
39 | clientHeight: number;
40 |
41 | targetWidth?: number;
42 | targetHeight?: number;
43 |
44 | scrollLeft: number;
45 | scrollTop: number;
46 |
47 | hasScroll: boolean,
48 |
49 | dragPhase: string;
50 | mousePosition: number[];
51 | scrollPosition: number[];
52 |
53 | barLocation: BarLocation;
54 | }
55 |
56 | export class Stroller extends React.Component {
57 |
58 | state = {
59 | scrollWidth: 0,
60 | scrollHeight: 0,
61 | clientWidth: 0,
62 | clientHeight: 0,
63 | targetWidth: 0,
64 | targetHeight: 0,
65 | scrollLeft: 0,
66 | scrollTop: 0,
67 | dragPhase: 'idle',
68 | mousePosition: [0, 0],
69 | scrollPosition: [0, 0],
70 | barLocation: 'inside' as BarLocation,
71 | hasScroll: false,
72 | };
73 |
74 | private dragMachine = DragMachine.create();
75 |
76 | private topNode: HTMLElement | undefined = undefined;
77 | private scrollableParent: HTMLElement | undefined = undefined;
78 | private scrollContainer: HTMLElement | null = null;
79 | private barRef: HTMLElement | undefined = undefined;
80 | private dettachParentCallback: null | (() => void) = null;
81 |
82 | componentDidMount() {
83 | this.scrollableParent = findScrollableParent(this.scrollContainer || this.topNode!, this.props.axis);
84 | const scrollableParent: any = this.scrollableParent;
85 |
86 | const barLocation = this.props.overrideLocation || this.scrollableParent === document.body
87 | ? 'fixed'
88 | : (
89 | (this.scrollContainer ? !this.topNode!.contains(this.scrollableParent) : true)
90 | ? 'inside'
91 | : 'outside'
92 | );
93 |
94 | this.setState({
95 | barLocation
96 | });
97 |
98 | this.attach(barLocation === 'fixed' ? window : this.scrollableParent);
99 |
100 | this.onContainerScroll();
101 |
102 | (this.dragMachine as any)._id = this;
103 |
104 | this.dragMachine
105 | .attrs({enabled: this.props.draggable})
106 | .observe((dragPhase: string) => this.setState({dragPhase}))
107 | .connect((message: string, coords: number[]) => {
108 | if (message === 'down') {
109 | this.setState({
110 | mousePosition: coords,
111 | scrollPosition:
112 | this.state.barLocation === 'fixed'
113 | ? [window.scrollX, window.scrollY]
114 | : [scrollableParent.scrollLeft, scrollableParent.scrollTop]
115 | })
116 | }
117 | if (message === 'move') {
118 | const {axis = 'vertical', targetAxis: pTargetAxis} = this.props;
119 | const {mousePosition} = this.state;
120 |
121 | const targetAxis = pTargetAxis || axis;
122 | const axScroll = axisToProps[axis];
123 | const axTarget = axisToProps[targetAxis];
124 |
125 | const delta = [mousePosition[0] - coords[0], mousePosition[1] - coords[1]];
126 |
127 | const st: any = this.state;
128 |
129 | const {space: axisSpace, scrollSpace: axisScrollSpace} = extractValues(st, axis);
130 | const {scrollSpace, targetSpace} = extractValues(st, targetAxis);
131 |
132 | const scrollFactor =
133 | axis === targetAxis
134 | ? scrollSpace / targetSpace
135 | : (axisScrollSpace - axisSpace) / targetSpace;
136 |
137 | const barPosition: any = scrollableParent.getBoundingClientRect();
138 | if (this.state.barLocation === 'fixed') {
139 | const X = axis === 'vertical' ? st.scrollPosition[0] : st.scrollPosition[0] - delta[axTarget.coord] * scrollFactor;
140 | const Y = axis !== 'vertical' ? st.scrollPosition[1] : st.scrollPosition[1] - delta[axTarget.coord] * scrollFactor;
141 | window.scrollTo(X, Y);
142 | } else if (barPosition[axTarget.start] < coords[axTarget.coord] && barPosition[axTarget.end] > coords[axTarget.coord]) {
143 | scrollableParent[axScroll.scroll] = st.scrollPosition[axScroll.coord] - delta[axTarget.coord] * scrollFactor;
144 | }
145 |
146 | }
147 | })
148 | .start('init')
149 | }
150 |
151 | componentWillUnmount() {
152 | this.dragMachine.destroy();
153 | this.dettach();
154 | }
155 |
156 | componentDidUpdate(prevProps: IStrollerProps) {
157 | this.dragMachine.attrs({enabled: this.props.draggable});
158 | this.dragMachine.put('check');
159 | if (this.props.scrollKey !== prevProps.scrollKey) {
160 | this.onContainerScroll();
161 | }
162 | }
163 |
164 | private onContainerScroll = (e?: Event) => {
165 | const topNode = this.scrollableParent as any;
166 |
167 | const scrollLeft = topNode.scrollLeft;
168 | const scrollTop = topNode.scrollTop;
169 |
170 | const scrollWidth = topNode.scrollWidth;
171 | const scrollHeight = topNode.scrollHeight;
172 |
173 | const targetWidth = this.topNode?.clientWidth;
174 | const targetHeight = this.topNode?.clientHeight;
175 |
176 | const clientWidth = topNode.clientWidth;
177 | const clientHeight = topNode.clientHeight;
178 |
179 |
180 | const isFixed = this.state.barLocation === 'fixed';
181 |
182 | const st: any = this.state;
183 |
184 | const {axis = 'vertical', onScroll } = this.props;
185 |
186 | const mainScroll = extractValues(st, axis);
187 |
188 | this.setState({
189 | scrollWidth,
190 | scrollHeight,
191 |
192 | targetWidth,
193 | targetHeight,
194 |
195 | clientWidth: isFixed ? window.innerWidth : clientWidth,
196 | clientHeight: isFixed ? window.innerHeight : clientHeight,
197 |
198 | scrollLeft: isFixed ? window.scrollX : scrollLeft,
199 | scrollTop: isFixed ? window.scrollY : scrollTop,
200 |
201 | hasScroll: mainScroll.scrollSpace > mainScroll.space,
202 | });
203 |
204 | if (onScroll && e) {
205 | onScroll(e);
206 | }
207 | };
208 |
209 | private attach(parent: HTMLElement | Window) {
210 | this.dettach();
211 | const {passive} = this.props;
212 | const options: any = passive && supportsPassiveEvents ? {passive: true} : undefined;
213 | parent.addEventListener('scroll', this.onContainerScroll, options);
214 | this.dettachParentCallback = () => {
215 | parent.removeEventListener('scroll', this.onContainerScroll, options);
216 | };
217 | }
218 |
219 | private dettach() {
220 | if (this.dettachParentCallback) {
221 | this.dettachParentCallback();
222 | this.dettachParentCallback = null;
223 | }
224 | }
225 |
226 | private setScrollContainer = (ref: HTMLElement | null) => this.scrollContainer = ref;
227 |
228 | private setTopNode = (topNode: HTMLElement) => this.topNode = topNode;
229 |
230 | private setBarRef = (barRef: HTMLElement) => {
231 | this.barRef = barRef;
232 | this.dragMachine
233 | .attrs({node: this.barRef})
234 | .put('check');
235 | };
236 |
237 | private strollerProviderValue = {
238 | setScrollContainer: this.setScrollContainer
239 | };
240 |
241 | render() {
242 | const {
243 | children,
244 | bar,
245 | inBetween,
246 | axis = 'vertical',
247 | targetAxis,
248 | oppositePosition = false,
249 | draggable = false,
250 | barSizeFunction,
251 | barClassName,
252 | SideBar,
253 | } = this.props;
254 |
255 | const {dragPhase} = this.state;
256 | const st: any = this.state;
257 |
258 | const ax = axisToProps[axis];
259 |
260 | const scrollSpace: number = st[ax.scrollSpace];
261 |
262 | const Bar = this.props.scrollBar || StollerBar;
263 |
264 | return (
265 |
266 |
267 |
268 | {children}
269 |
270 |
271 | {inBetween}
272 |
273 | {scrollSpace
274 | ? (
275 |
295 | )
296 | : null
297 | }
298 |
299 |
300 | );
301 | }
302 | }
--------------------------------------------------------------------------------
/src/context.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {IStrollerState} from "./types";
3 |
4 | export interface IStrollerContext {
5 | setScrollContainer: (ref: HTMLElement | null) => any;
6 | }
7 |
8 | const contextDefault:IStrollerContext = {
9 | setScrollContainer: () => { throw new Error('StrollerCaptor used without Stroller')}
10 | };
11 |
12 | export const context = React.createContext(contextDefault);
13 | export const StrollerProvider = context.Provider;
14 | export const StrollerContext = context.Consumer;
15 |
16 | export const stateContext = React.createContext({} as IStrollerState);
17 | export const StrollerStateProvider = stateContext.Provider;
18 | export const StrollerState = stateContext.Consumer;
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {Strollable} from "./Container";
2 | import {Stroller} from './Stroller';
3 | import {StrollableContainer} from "./StrollableContainer";
4 | import {StrollCaptor} from './StrollCaptor';
5 | import {StrollerState} from "./context";
6 | import {getScrollBarWidth} from "./utils";
7 |
8 | export {
9 | Strollable,
10 | Stroller,
11 | StrollableContainer,
12 | StrollCaptor,
13 |
14 | StrollerState,
15 | getScrollBarWidth
16 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import {BarLocation} from "./Bar";
2 |
3 | export interface IStrollerState {
4 | scrollWidth: number;
5 | scrollHeight: number;
6 |
7 | clientWidth: number;
8 | clientHeight: number;
9 |
10 | scrollLeft: number;
11 | scrollTop: number;
12 |
13 | dragPhase: string;
14 | mousePosition: number[];
15 | scrollPosition: number[];
16 |
17 | barLocation: BarLocation;
18 | }
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const axisToOverflow = {
2 | vertical: 'overflowY',
3 | horizontal: 'overflowX'
4 | };
5 |
6 | export const axisToOverflowReverse = {
7 | vertical: 'overflowX',
8 | horizontal: 'overflowY'
9 | };
10 |
11 | export const axisToAxis = {
12 | vertical: 'Y',
13 | horizontal: 'X'
14 | }
15 |
16 | export type axisTypes = 'vertical' | 'horizontal';
17 |
18 | export const axisToProps = {
19 | 'vertical': {
20 | scroll: 'scrollTop',
21 | space: 'clientHeight',
22 | targetSpace: 'targetHeight',
23 | scrollSpace: 'scrollHeight',
24 | start: 'top',
25 | end: 'bottom',
26 |
27 | coord: 1,
28 | },
29 | 'horizontal': {
30 | scroll: 'scrollLeft',
31 | space: 'clientWidth',
32 | targetSpace: 'targetWidth',
33 | scrollSpace: 'scrollWidth',
34 | start: 'left',
35 | end: 'right',
36 |
37 | coord: 0,
38 | }
39 | };
40 |
41 | export const findScrollableParent = (node: HTMLElement, axis: axisTypes = 'vertical'): HTMLElement => {
42 | if (node === document.body) {
43 | return node;
44 | }
45 | const style = window.getComputedStyle(node);
46 | const flow: string = style[axisToOverflow[axis] as any];
47 | if (flow === 'hidden' || flow === 'scroll') {
48 | return node;
49 | }
50 | return node.parentNode
51 | ? findScrollableParent(node.parentNode as any, axis)
52 | : node;
53 | };
54 |
55 | let scrollbarWidth = -1;
56 |
57 | export const getScrollBarWidth = (): number => {
58 | if(typeof document === 'undefined'){
59 | return 24;
60 | }
61 | if (scrollbarWidth < 0) {
62 | const outer = document.createElement('div');
63 | const inner = document.createElement('div');
64 | outer.style.overflow = 'scroll';
65 | document.body.appendChild(outer);
66 | outer.appendChild(inner);
67 | scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
68 | document.body.removeChild(outer);
69 | }
70 | return scrollbarWidth;
71 | }
72 |
73 | export const extractValues = (set: any, axis: axisTypes) => {
74 | const ax = axisToProps[axis];
75 | const scrollSpace: number = set[ax.scrollSpace];
76 | const space: number = set[ax.space];
77 | const targetSpace: number = set[ax.targetSpace];
78 | const scroll: number = set[ax.scroll];
79 |
80 | return {
81 | scrollSpace,
82 | space,
83 | targetSpace,
84 | scroll
85 | };
86 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "strictNullChecks": true,
5 | "strictFunctionTypes": true,
6 | "skipLibCheck": true,
7 | "noImplicitThis": true,
8 | "alwaysStrict": true,
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "noImplicitAny": true,
14 | "target": "es5",
15 | "lib": [
16 | "dom",
17 | "es5",
18 | "scripthost",
19 | "es2015.collection",
20 | "es2015.symbol",
21 | "es2015.iterable",
22 | "es2015.promise"
23 | ],
24 | "jsx": "react"
25 | }
26 | }
--------------------------------------------------------------------------------