diff --git a/README.md b/README.md index f44e19a..7af201d 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ For **React 0.13** you need to wrap `` child into a function. ``` #### Version without boundled css styles #### -If you prefer including scrollbar without css styles boundled inline to js file it's possible to import package without them. It's useful when you want to make custom css changes in scrollbars without using `!important` in each line. +If you prefer including scrollbar without css styles boundled inline to js file it's possible to import package without them. It's useful when you want to make custom css changes in scrollbars without using `!important` in each line. ```js var ScrollArea = require('react-scrollbar/no-css'); @@ -93,10 +93,14 @@ then open [http://localhost:8003](http://localhost:8003). contentClassName={String} contentStyle={Object} horizontal={Boolean} + horizontalContainerClassName={String} horizontalContainerStyle={Object} + horizontalScrollbarClassName={String} horizontalScrollbarStyle={Object} vertical={Boolean} + verticalContainerClassName={String} verticalContainerStyle={Object} + verticalScrollbarClassName={String} verticalScrollbarStyle={Object} onScroll={(value) => {}} contentWindow={Object} @@ -139,9 +143,15 @@ Inline styles applied to element with scroll area content. When set to false, horizontal scrollbar will not be available. **Default: true** +#### horizontalContainerClassName +CSS class names added to horizontal scrollbar's container. + #### horizontalContainerStyle Inline styles applied to horizontal scrollbar's container. +#### horizontalScrollbarClassName +CSS class names added to horizontal scrollbar. + #### horizontalScrollbarStyle Inline styles applied to horizontal scrollbar. @@ -149,9 +159,15 @@ Inline styles applied to horizontal scrollbar. When set to false, vertical scrollbar will not be available, regardless of the content height. **Default: true** +#### verticalContainerClassName +CSS class names added to vertical scrollbar's container. + #### verticalContainerStyle Inline styles applied to vertical scrollbar's container. +#### verticalScrollbarClassName +CSS class names added to vertical scrollbar. + #### verticalScrollbarStyle Inline styles applied to vertical scrollbar. @@ -164,7 +180,7 @@ You can override document to make scrollarea works inside iframe. **Default: document** #### smoothScrolling -When set to true, smooth scrolling for both scrollbars is enabled. +When set to true, smooth scrolling for both scrollbars is enabled. **Default: false** #### minScrollSize @@ -237,7 +253,7 @@ It allows to scroll to the right of `ScrollArea` component. It moves horizontal scrollbar. `leftPosition` is a distance between left edge of `scrollArea` container and left edge of `scrollArea` content. # Change log -Every release is documented on the Github [Releases](https://github.com/souhe/reactScrollbar/releases) page. +Every release is documented on the Github [Releases](https://github.com/souhe/reactScrollbar/releases) page. # License MIT diff --git a/examples/js/changing-children.jsx b/examples/js/changing-children.jsx index f38ddae..57bbab1 100644 --- a/examples/js/changing-children.jsx +++ b/examples/js/changing-children.jsx @@ -1,4 +1,5 @@ -import React from 'react' +import React from 'react'; +import PropTypes from 'prop-types'; import ScrollArea from 'react-scrollbar'; class ChangingChildren extends React.Component{ @@ -52,31 +53,31 @@ class Content extends React.Component { - + ); } - + handleScrollTopButtonClick() { this.context.scrollArea.scrollTop(); } - + handleScrollBottomButtonClick() { this.context.scrollArea.scrollBottom(); } - + handleScroll100ButtonClick() { this.context.scrollArea.scrollXTo(100); } - + handleScrollLeftButtonClick() { this.context.scrollArea.scrollLeft(); } - + handleScrollRightButtonClick() { this.context.scrollArea.scrollRight(); } - + handleScrollY40ButtonClick() { this.context.scrollArea.scrollYTo(40); } @@ -91,7 +92,7 @@ class Content extends React.Component { } Content.contextTypes = { - scrollArea: React.PropTypes.object + scrollArea: PropTypes.object }; export default ChangingChildren; diff --git a/package.json b/package.json index 25630e4..9f8b88b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "ScrollArea component for react", "main": "./dist/scrollArea.js", "scripts": { + "start": "gulp build && gulp watch", "test": "./node_modules/.bin/karma start karma.config.js --single-run --browsers PhantomJS" }, "repository": { @@ -21,10 +22,12 @@ "author": "souhe", "license": "MIT", "dependencies": { + "classnames": "^2.2.5", "config": "^1.24.0", "line-height": "^0.1.1", - "react": "^15.4.1", - "react-motion": "^0.4.3" + "prop-types": "^15.5.7", + "react": "^15.5.4", + "react-motion": "^0.4.7" }, "devDependencies": { "babel": "^6.5.2", @@ -58,7 +61,7 @@ "object-assign": "^4.0.1", "phantomjs-prebuilt": "^2.1.3", "react-addons-test-utils": "^15.1.0", - "react-dom": "^15.4.1", + "react-dom": "^15.5.4", "run-sequence": "^1.1.4", "style-loader": "^0.13.0", "webpack": "^1.12.2", diff --git a/src/js/ScrollArea.jsx b/src/js/ScrollArea.jsx index 45a5957..e3063ba 100644 --- a/src/js/ScrollArea.jsx +++ b/src/js/ScrollArea.jsx @@ -1,8 +1,10 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ScrollBar from './Scrollbar'; import {findDOMNode, warnAboutFunctionChild, warnAboutElementChild, positiveOrZero, modifyObjValues} from './utils'; import lineHeight from 'line-height'; import {Motion, spring} from 'react-motion'; +import classNames from 'classnames'; const eventTypes = { wheel: 'wheel', @@ -97,7 +99,9 @@ export default class ScrollArea extends React.Component { position={this.state.topPosition} onMove={this.handleScrollbarMove.bind(this)} onPositionChange={this.handleScrollbarYPositionChange.bind(this)} + containerClassName={this.props.verticalContainerClassName} containerStyle={this.props.verticalContainerStyle} + scrollbarClassName={this.props.verticalScrollbarClassName} scrollbarStyle={this.props.verticalScrollbarStyle} smoothScrolling={withMotion} minScrollSize={this.props.minScrollSize} @@ -113,7 +117,9 @@ export default class ScrollArea extends React.Component { position={this.state.leftPosition} onMove={this.handleScrollbarMove.bind(this)} onPositionChange={this.handleScrollbarXPositionChange.bind(this)} + containerClassName={this.props.horizontalContainerClassName} containerStyle={this.props.horizontalContainerStyle} + scrollbarClassName={this.props.horizontalScrollbarClassName} scrollbarStyle={this.props.horizontalScrollbarStyle} smoothScrolling={withMotion} minScrollSize={this.props.minScrollSize} @@ -128,8 +134,8 @@ export default class ScrollArea extends React.Component { warnAboutElementChild(); } - let classes = 'scrollarea ' + (className || ''); - let contentClasses = 'scrollarea-content ' + (contentClassName || ''); + let classes = classNames('scrollarea', className); + let contentClasses = classNames('scrollarea-content', contentClassName); let contentStyle = { marginTop: -this.state.topPosition, @@ -446,29 +452,33 @@ export default class ScrollArea extends React.Component { } ScrollArea.childContextTypes = { - scrollArea: React.PropTypes.object + scrollArea: PropTypes.object }; ScrollArea.propTypes = { - className: React.PropTypes.string, - style: React.PropTypes.object, - speed: React.PropTypes.number, - contentClassName: React.PropTypes.string, - contentStyle: React.PropTypes.object, - vertical: React.PropTypes.bool, - verticalContainerStyle: React.PropTypes.object, - verticalScrollbarStyle: React.PropTypes.object, - horizontal: React.PropTypes.bool, - horizontalContainerStyle: React.PropTypes.object, - horizontalScrollbarStyle: React.PropTypes.object, - onScroll: React.PropTypes.func, - contentWindow: React.PropTypes.any, - ownerDocument: React.PropTypes.any, - smoothScrolling: React.PropTypes.bool, - minScrollSize: React.PropTypes.number, - swapWheelAxes: React.PropTypes.bool, - stopScrollPropagation: React.PropTypes.bool, - focusableTabIndex: React.PropTypes.number + className: PropTypes.string, + style: PropTypes.object, + speed: PropTypes.number, + contentClassName: PropTypes.string, + contentStyle: PropTypes.object, + vertical: PropTypes.bool, + verticalContainerClassName: PropTypes.string, + verticalContainerStyle: PropTypes.object, + verticalScrollbarClassName: PropTypes.string, + verticalScrollbarStyle: PropTypes.object, + horizontal: PropTypes.bool, + horizontalContainerClassName: PropTypes.string, + horizontalContainerStyle: PropTypes.object, + horizontalScrollbarClassName: PropTypes.string, + horizontalScrollbarStyle: PropTypes.object, + onScroll: PropTypes.func, + contentWindow: PropTypes.any, + ownerDocument: PropTypes.any, + smoothScrolling: PropTypes.bool, + minScrollSize: PropTypes.number, + swapWheelAxes: PropTypes.bool, + stopScrollPropagation: PropTypes.bool, + focusableTabIndex: PropTypes.number }; ScrollArea.defaultProps = { diff --git a/src/js/Scrollbar.jsx b/src/js/Scrollbar.jsx index 98258d4..47cc0c0 100644 --- a/src/js/Scrollbar.jsx +++ b/src/js/Scrollbar.jsx @@ -1,6 +1,8 @@ import React from 'react'; +import PropTypes from 'prop-types'; import {Motion, spring} from 'react-motion'; import {modifyObjValues} from './utils'; +import classNames from 'classnames'; class ScrollBar extends React.Component { constructor(props){ @@ -59,25 +61,38 @@ class ScrollBar extends React.Component { } render(){ - let {smoothScrolling, isDragging, type, scrollbarStyle, containerStyle} = this.props; - let isVoriziontal = type === 'horizontal'; + let { + smoothScrolling, isDragging, type, + containerClassName, containerStyle, + scrollbarClassName, scrollbarStyle, + } = this.props; + let isHorizontal = type === 'horizontal'; let isVertical = type === 'vertical'; let scrollStyles = this.createScrollStyles(); let springifiedScrollStyles = smoothScrolling ? modifyObjValues(scrollStyles, x => spring(x)) : scrollStyles; - let scrollbarClasses = `scrollbar-container ${isDragging ? 'active' : ''} ${isVoriziontal ? 'horizontal' : ''} ${isVertical ? 'vertical' : ''}`; + const containerClassNames = classNames( + 'scrollbar-container', + { + active: isDragging, + horizontal: isHorizontal, + vertical: isVertical, + }, + containerClassName, + ); + const scrollbarClassNames = classNames('scrollbar', scrollbarClassName); return ( { style =>
this.scrollbarContainer = x } >
@@ -161,18 +176,20 @@ class ScrollBar extends React.Component { } ScrollBar.propTypes = { - onMove: React.PropTypes.func, - onPositionChange: React.PropTypes.func, - onFocus: React.PropTypes.func, - realSize: React.PropTypes.number, - containerSize: React.PropTypes.number, - position: React.PropTypes.number, - containerStyle: React.PropTypes.object, - scrollbarStyle: React.PropTypes.object, - type: React.PropTypes.oneOf(['vertical', 'horizontal']), - ownerDocument: React.PropTypes.any, - smoothScrolling: React.PropTypes.bool, - minScrollSize: React.PropTypes.number + onMove: PropTypes.func, + onPositionChange: PropTypes.func, + onFocus: PropTypes.func, + realSize: PropTypes.number, + containerSize: PropTypes.number, + position: PropTypes.number, + containerClassName: PropTypes.string, + containerStyle: PropTypes.object, + scrollbarClassName: PropTypes.string, + scrollbarStyle: PropTypes.object, + type: PropTypes.oneOf(['vertical', 'horizontal']), + ownerDocument: PropTypes.any, + smoothScrolling: PropTypes.bool, + minScrollSize: PropTypes.number }; ScrollBar.defaultProps = { diff --git a/src/js/utils.js b/src/js/utils.js index 9a885e9..e1ae4db 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -38,7 +38,7 @@ export function modifyObjValues (obj, modifier = x => x){ for(let key in obj){ if(obj.hasOwnProperty(key)) modifiedObj[key] = modifier(obj[key]); } - + return modifiedObj; } diff --git a/test/scrollArea.spec.js b/test/scrollArea.spec.js index 91ed624..4e6e9a0 100644 --- a/test/scrollArea.spec.js +++ b/test/scrollArea.spec.js @@ -59,7 +59,7 @@ describe('ScrollArea component', () => { expect(content).toEqualJSX(
{}} style={{}} - className="scrollarea-content " + className="scrollarea-content" onTouchStart={() => {}} onTouchMove={() => {}} onTouchEnd={() => {}} @@ -78,7 +78,7 @@ describe('ScrollArea component', () => {
{}} style={{}} tabIndex={100} - className="scrollarea-content " + className="scrollarea-content" onTouchStart={() => {}} onTouchMove={() => {}} onTouchEnd={() => {}} @@ -118,6 +118,25 @@ describe('ScrollArea component', () => { expect(content.props.style).toEqual({ test: 'contentStyle' }); }); + it('Should have proper scrollbars classes', () => { + let {scrollbars} = setupComponentWithMockedSizes({ + vertical: true, + verticalContainerClassName: 'verticalContainerClassName', + verticalScrollbarClassName: 'verticalScrollbarClassName', + horizontal: true, + horizontalContainerClassName: 'horizontalContainerClassName', + horizontalScrollbarClassName: 'horizontalScrollbarClassName', + }); + + let verticalScrollbar = scrollbars.filter(component => component.props.type === 'vertical')[0]; + let horizontalScrollbar = scrollbars.filter(component => component.props.type === 'horizontal')[0]; + + expect(verticalScrollbar.props.containerClassName).toInclude('verticalContainerClassName'); + expect(verticalScrollbar.props.scrollbarClassName).toInclude('verticalScrollbarClassName'); + expect(horizontalScrollbar.props.containerClassName).toInclude('horizontalContainerClassName'); + expect(horizontalScrollbar.props.scrollbarClassName).toInclude('horizontalScrollbarClassName'); + }); + it('Should have proper scrollbars styles', () => { let {content, scrollbars} = setupComponentWithMockedSizes({ vertical: true, diff --git a/test/scrollBar.spec.js b/test/scrollBar.spec.js index f6fe2a4..71f9a8b 100644 --- a/test/scrollBar.spec.js +++ b/test/scrollBar.spec.js @@ -16,7 +16,7 @@ function setupScrollbar(props){ let wrapper = output.props.children(); let content = wrapper.props.children; - + return { wrapper, content, @@ -33,10 +33,10 @@ function getRendererComponentInstance(renderer){ describe('ScrollBar component', () => { it('Vertical should have proper class', () => { let {wrapper} = setupScrollbar({type: 'vertical'}); - + expect(wrapper.props.className).toInclude('vertical'); }); - + it('Vertical should have proper container styles', () => { let {wrapper} = setupScrollbar({ type: 'vertical', @@ -54,13 +54,13 @@ describe('ScrollBar component', () => { expect(content.props.style).toEqual({test: 'scrollbarStyle'}); }); - + it('Horizontal should have proper class', () => { let {wrapper} = setupScrollbar({type: 'horizontal'}); - + expect(wrapper.props.className).toInclude('horizontal'); }); - + it('Horizontal should have proper container styles', () => { let {wrapper} = setupScrollbar({ type: 'horizontal', @@ -78,30 +78,42 @@ describe('ScrollBar component', () => { expect(content.props.style).toEqual({test: 'scrollbarStyle'}); }); - + + it('Scrollbar should have proper container class', () => { + let {wrapper} = setupScrollbar({containerClassName: 'test-class'}); + + expect(wrapper.props.className).toInclude('test-class'); + }); + + it('Scrollbar should have proper element class', () => { + let {content} = setupScrollbar({scrollbarClassName: 'test-class'}); + + expect(content.props.className).toInclude('test-class'); + }); + it('ScrollBar should be in proper position', () => { let {instance} = setupScrollbar({ - realSize: 400, - containerSize: 100, + realSize: 400, + containerSize: 100, position: 20 }); - - expect(instance.state.position).toBe(5); + + expect(instance.state.position).toBe(5); }); - + it('ScrollBar should have proper size', () => { let { instance } = setupScrollbar({ - realSize: 400, + realSize: 400, containerSize: 100 }); - - expect(instance.state.scrollSize).toBe(25); + + expect(instance.state.scrollSize).toBe(25); }); - + it('Should propagate onMove event after move vertical scrollbar', () => { let handleMoveSpy = expect.createSpy(); let {instance} = setupScrollbar({ - realSize: 200, + realSize: 200, containerSize: 100, onMove: handleMoveSpy, onFocus: () => {} @@ -110,15 +122,15 @@ describe('ScrollBar component', () => { let moveEvent = {clientY: 25, preventDefault: () => {}}; instance.handleMouseDown(mouseDownEvent); instance.handleMouseMoveForVertical(moveEvent); - + expect(handleMoveSpy.calls.length).toEqual(1); expect(handleMoveSpy.calls[0].arguments).toEqual([-50 , 0]); }); - + it('Should propagate onMove event after move horizontal scrollbar', () => { let handleMoveSpy = expect.createSpy(); let {instance} = setupScrollbar({ - realSize: 200, + realSize: 200, containerSize: 100, onMove: handleMoveSpy, type: 'horizontal', @@ -128,15 +140,15 @@ describe('ScrollBar component', () => { let moveEvent = {clientX: 25, preventDefault: () => {}}; instance.handleMouseDown(mouseDownEvent); instance.handleMouseMoveForHorizontal(moveEvent); - + expect(handleMoveSpy.calls.length).toEqual(1); expect(handleMoveSpy.calls[0].arguments).toEqual([0, -50]); }); - + it('Should propagate onMove event multiple times', () => { let handleMoveSpy = expect.createSpy(); let {instance} = setupScrollbar({ - realSize: 400, + realSize: 400, containerSize: 100, onMove: handleMoveSpy, onFocus: () => {} @@ -151,65 +163,65 @@ describe('ScrollBar component', () => { instance.handleMouseMoveForVertical(moveEvent); moveEvent.clientY = 40; instance.handleMouseMoveForVertical(moveEvent); - + expect(handleMoveSpy.calls.length).toEqual(4); expect(handleMoveSpy.calls[3].arguments).toEqual([-40 , 0]); }); - + it('Should be possible to set min scrollbar size', () => { let minScrollBarSize = 10; let {instance} = setupScrollbar({ - realSize: 10000, + realSize: 10000, containerSize: 100, type: 'vertical', minScrollSize: minScrollBarSize, onFocus: () => {} - }); - - expect(instance.state.scrollSize).toBe(minScrollBarSize); + }); + + expect(instance.state.scrollSize).toBe(minScrollBarSize); }); - + it('Method calculateFractionalPosition should work properly for realSize: 300, containerSize: 100, position: 0', () => { let {instance} = setupScrollbar(); - + expect(instance.calculateFractionalPosition(300, 100, 0)).toEqual(0); }); - + it('Method calculateFractionalPosition should work properly for realSize: 300, containerSize: 100, position: 200', () => { let {instance} = setupScrollbar(); - + expect(instance.calculateFractionalPosition(300, 100, 200)).toEqual(1); }); - + it('Method calculateFractionalPosition should work properly for realSize: 300, containerSize: 100, position: 200', () => { let {instance} = setupScrollbar(); - + expect(instance.calculateFractionalPosition(300, 100, 100)).toEqual(0.5); }); - + it('Method calculateFractionalPosition should work properly for realSize: 160, containerSize: 80, position: 20', () => { let {instance} = setupScrollbar(); - + expect(instance.calculateFractionalPosition(160, 80, 20)).toEqual(0.25); }); - + it('Position of scrollbar should be proper when minScrollBarSize is set', () => { let {instance} = setupScrollbar({ - position: 9900, + position: 9900, realSize: 10000, containerSize: 100, type: 'vertical', minScrollSize: 10 - }); - + }); + expect(instance.state.position).toBe(90); }); - + it('vertical scrollbar container click should move scrollbar', () => { let handlePositionChangeSpy = expect.createSpy(); let {instance} = setupScrollbar({ position: 0, - realSize: 500, + realSize: 500, containerSize: 100, type: 'vertical', onPositionChange: handlePositionChangeSpy @@ -221,16 +233,16 @@ describe('ScrollBar component', () => { }) }; instance.handleScrollBarContainerClick(mouseDownEvent); - + expect(handlePositionChangeSpy.calls.length).toEqual(1); - expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); + expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); }); - + it('vertical scrollbar container click should move scrollbar when minScrollbar size is set', () => { let handlePositionChangeSpy = expect.createSpy(); let {instance} = setupScrollbar({ position: 0, - realSize: 1000, + realSize: 1000, containerSize: 100, type: 'vertical', onPositionChange: handlePositionChangeSpy, @@ -243,16 +255,16 @@ describe('ScrollBar component', () => { }) }; instance.handleScrollBarContainerClick(mouseDownEvent); - + expect(handlePositionChangeSpy.calls.length).toEqual(1); - expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); + expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); }); - + it('horizontal scrollbar container click should move scrollbar', () => { let handlePositionChangeSpy = expect.createSpy(); let {instance} = setupScrollbar({ position: 0, - realSize: 500, + realSize: 500, containerSize: 100, type: 'horizontal', onPositionChange: handlePositionChangeSpy @@ -264,16 +276,16 @@ describe('ScrollBar component', () => { }) }; instance.handleScrollBarContainerClick(mouseDownEvent); - + expect(handlePositionChangeSpy.calls.length).toEqual(1); - expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); + expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); }); - + it('horizontal scrollbar container click should move scrollbar when minScrollbar size is set', () => { let handlePositionChangeSpy = expect.createSpy(); let {instance} = setupScrollbar({ position: 0, - realSize: 1000, + realSize: 1000, containerSize: 100, type: 'horizontal', onPositionChange: handlePositionChangeSpy, @@ -286,8 +298,8 @@ describe('ScrollBar component', () => { }) }; instance.handleScrollBarContainerClick(mouseDownEvent); - + expect(handlePositionChangeSpy.calls.length).toEqual(1); - expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); + expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); }); });