Skip to content

Commit

Permalink
RTL support
Browse files Browse the repository at this point in the history
  • Loading branch information
xobotyi committed Oct 17, 2018
1 parent 98032d7 commit 420d84c
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* No matter what changes the content - scrollbars always stay actual
* Total tests coverage
* Scrollbars nesting
* RTL support ([read more]())

>**IMPORTANT:** default component styles uses [Flexible Box Layout](https://developer.mozilla.org/ru/docs/Web/CSS/CSS_Flexible_Box_Layout) for proper scrollbars display.
>But you can customize it with help pf inline or linked styles as you wish ([see docs](https://github.com/xobotyi/react-scrollbars-custom/blob/master/docs/CUSTOMISATION.md)).
Expand Down
7 changes: 5 additions & 2 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* `defaultStyles`: _(boolean)_ Apply default inline styles _(default: false)_
* `fallbackScrollbarWidth`: _(number)_ Number of pixels that will be treated as scrollbar width if automated scrollbar width detection will fail. _This parameter used on mobiles, because scrollbars there has an absolute positioning and can't be measured._ _(default: 20)_
* `minimalThumbsSize`: _(number)_ Minimal size of thumb in pixels _(default: 30)_
* `noScroll`: _(boolean)_ Disable both vertical and horizontal scrolling_(default: false)_
* `rtl`: _(boolean)_ Override the direction style parameter _(default: undefined)_
* `noScroll`: _(boolean)_ Disable both vertical and horizontal scrolling _(default: false)_
* `noScrollY`: _(boolean)_ Disable vertical scrolling _(default: false)_
* `noScrollX`: _(boolean)_ Disable horizontal scrolling _(default: false)_
* `permanentScrollbars`: _(boolean)_ Display both, vertical and horizontal scrollbars permanently, in spite of scrolling possibility _(default: false)_
Expand Down Expand Up @@ -63,5 +64,7 @@
* `scrollToBottom()`: _(Scrollbar)_ Scroll to the bottom border
* `scrollToLeft()`: _(Scrollbar)_ Scroll to the left border
* `scrollToRight()`: _(Scrollbar)_ Scroll to the right border
* `update(forced=false)`: _(Scrollbar)_ Updates the scrollbars. By default if content or wrapper sizes did not changed - update will not be performed. But you can force the update by passing `true` as first parameter.
* `update(forced=false, rtlAutodetect=false)`: _(Scrollbar)_ Updates the scrollbars. By default if content or wrapper sizes did not changed - update will not be performed.
* `forced`: _(boolean)_ Whether to update the scrollbars even nothing has changed _(default: false)_
* `rtlAutodetect`: _(boolean)_ Whether to check and actualize CSS direction value _(default: false)_
Keep in mind that forced update will either trigger `onScroll` callback.
16 changes: 16 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,19 @@ class App extends Component
}
}
```

### RTL support
`react-scrollbars-custom` supports right-to-left direction out of the box, you don't have to pass extra properties to make it work, everything is automated, but you can override it.
But it has several nuances you should know:
* Due to performance reasons, direction detection happens in 3 situations:
* On component mount;
* On rtl property change;
* On call `scrollbar.update(undefined, true);`;
* When rtl direction detected - `ScrollbarsCustom-RTL` classname will be added to the holder;
* If `rtl` property has not set at all (undefined) - direction will be determined according to CSS;
* If `rtl` property has `true` - `direction: rtl;` style will be applied to holder;
* If `rtl` property has `false` - `direction: ltr;` style will be applied to holder;
* `rtl` property has priority over the `style` property.
```javascript
<Scrollbar style={{direction: 'ltr'}} rtl /> // will have RTL direction
```
1 change: 1 addition & 0 deletions examples/app/components/Body.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default class Head extends React.Component
<li>No matter what changes the content - scrollbars always stay actual</li>
<li>Total tests coverage</li>
<li>Scrollbars nesting</li>
<li>RTL support</li>
</ul>
<p><a href="https://github.com/xobotyi/react-scrollbars-custom/tree/master/docs">Docs on GitHub</a> | <a href="./#benchmark" target="_blank">Benchmark</a></p>
</div>
Expand Down
14 changes: 13 additions & 1 deletion examples/app/components/blocks/SandboxBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ export default class SandboxBlock extends React.Component
this.handleScrollBottomClick = this.handleScrollBottomClick.bind(this);
this.handleScrollLeftClick = this.handleScrollLeftClick.bind(this);
this.handleScrollRightClick = this.handleScrollRightClick.bind(this);
this.toggleRtl = this.toggleRtl.bind(this);

this.state = {
noScroll: false,
noScrollY: false,
noScrollX: false,

rtl: false,

permanentTracks: false,
permanentTrackY: false,
permanentTrackX: false,
Expand Down Expand Up @@ -91,6 +94,13 @@ export default class SandboxBlock extends React.Component
});
}

toggleRtl() {
this.setState({
...this.state,
rtl: !this.state.rtl,
});
}

handleAddParagraphClick() {
this.setState({
...this.state,
Expand Down Expand Up @@ -120,7 +130,7 @@ export default class SandboxBlock extends React.Component
}

render() {
const {noScroll, noScrollY, noScrollX, permanentTracks, permanentTrackY, permanentTrackX} = this.state;
const {noScroll, noScrollY, noScrollX, permanentTracks, permanentTrackY, permanentTrackX, rtl} = this.state;

return (
<div className="block" id="SandboxBlock">
Expand All @@ -133,6 +143,7 @@ export default class SandboxBlock extends React.Component
<div className="button" key="permanentTracks" onClick={ this.togglePermanentTracks }>{ permanentTracks ? "Show tracks if needed" : "Always show tracks" }</div>
<div className="button" key="permanentTracksY" onClick={ this.togglePermanentTrackY }>{ permanentTrackY || permanentTracks ? "Show track Y if needed" : "Always show track Y" }</div>
<div className="button" key="permanentTracksX" onClick={ this.togglePermanentTrackX }>{ permanentTrackX || permanentTracks ? "Show track X if needed" : "Always show track X" }</div>
<div className="button" key="direction" onClick={ this.toggleRtl }>{ rtl ? "set direction LRT" : "set direction RTL" }</div>
<br />
<div className="button" key="randomPosition" onClick={ this.handleRandomPositionClick }>Random position</div>
<div className="button" key="scrollTop" onClick={ this.handleScrollTopClick }>Scroll top</div>
Expand All @@ -149,6 +160,7 @@ export default class SandboxBlock extends React.Component
noScroll={ noScroll }
noScrollY={ noScrollY }
noScrollX={ noScrollX }
rtl={ rtl }
permanentScrollbars={ permanentTracks }
permanentScrollbarY={ permanentTrackY }
permanentScrollbarX={ permanentTrackX }>
Expand Down
41 changes: 32 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ export default class Scrollbar extends React.Component
LoopController.registerScrollbar(this);

this.addListeners();
this.update();
this.update(true, true);
}

componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.noScroll !== this.props.noScroll || prevProps.noScrollY !== this.props.noScrollY || prevProps.noScrollX !== this.props.noScrollX
if (prevProps.noScroll !== this.props.noScroll || prevProps.noScrollY !== this.props.noScrollY || prevProps.noScrollX !== this.props.noScrollX || prevProps.rtl !== this.props.rtl
|| prevProps.permanentScrollbars !== this.props.permanentScrollbars || prevProps.permanentScrollbarX !== this.props.permanentScrollbarX || prevProps.permanentScrollbarY !== this.props.permanentScrollbarY) {
this.update(true);
this.update(true, prevProps.rtl !== this.props.rtl);
}

this.addListeners();
Expand Down Expand Up @@ -498,8 +498,9 @@ export default class Scrollbar extends React.Component
* Performs an actualisation of scrollbars and its thumbs
*
* @param forced {boolean} Whether to perform an update even if nothing has changed
* @param rtlAutodetect {boolean} Whether to check the CSS value `direction`
*/
update = (forced = false) => {
update = (forced = false, rtlAutodetect = false) => {
// No need to update scrollbars if values had not changed
if (!forced && (this.previousScrollValues || false)) {
if (this.previousScrollValues.scrollTop === this.content.scrollTop &&
Expand All @@ -512,9 +513,23 @@ export default class Scrollbar extends React.Component
}
}

this.isRtl = this.props.rtl || this.isRtl || (rtlAutodetect ? getComputedStyle(this.content).direction === "rtl" : false);

this.holder.classList.toggle("ScrollbarsCustom-RTL", this.isRtl);

const verticalScrollPossible = this.content.scrollHeight > this.content.clientHeight && !this.props.noScroll && !this.props.noScrollY,
horizontalScrollPossible = this.content.scrollWidth > this.content.clientWidth && !this.props.noScroll && !this.props.noScrollX;

if (verticalScrollPossible && ((this.previousScrollValues || true) || this.isRtl !== (this.previousScrollValues.rtl || false))) {
const browserScrollbarWidth = getScrollbarWidth(),
fallbackScrollbarWidth = this.props.fallbackScrollbarWidth;

this.content.style.marginLeft = this.isRtl ? -(browserScrollbarWidth || fallbackScrollbarWidth) + "px" : null;
this.content.style.paddingLeft = this.isRtl ? (browserScrollbarWidth ? null : fallbackScrollbarWidth) + "px" : null;
this.content.style.marginRight = this.isRtl ? null : -(browserScrollbarWidth || fallbackScrollbarWidth) + "px";
this.content.style.paddingRight = this.isRtl ? null : (browserScrollbarWidth ? null : fallbackScrollbarWidth) + "px";
}

this.trackVertical.style.display = (verticalScrollPossible || this.props.permanentScrollbars || this.props.permanentScrollbarY) ? null : "none";
this.trackVertical.visibility = (verticalScrollPossible || this.props.permanentScrollbars || this.props.permanentScrollbarY) ? null : "hidden";

Expand All @@ -524,7 +539,9 @@ export default class Scrollbar extends React.Component
if (verticalScrollPossible) {
const trackVerticalInnerHeight = getInnerHeight(this.trackVertical);
const thumbVerticalHeight = this.computeThumbVerticalHeight(trackVerticalInnerHeight);
const thumbVerticalOffset = thumbVerticalHeight ? this.content.scrollTop / (this.content.scrollHeight - this.content.clientHeight) * (trackVerticalInnerHeight - thumbVerticalHeight) : 0;
const thumbVerticalOffset = thumbVerticalHeight
? this.content.scrollTop / (this.content.scrollHeight - this.content.clientHeight) * (trackVerticalInnerHeight - thumbVerticalHeight)
: 0;

this.thumbVertical.style.transform = `translateY(${thumbVerticalOffset}px)`;
this.thumbVertical.style.height = thumbVerticalHeight + "px";
Expand All @@ -537,7 +554,13 @@ export default class Scrollbar extends React.Component
if (horizontalScrollPossible) {
const trackHorizontalInnerWidth = getInnerWidth(this.trackHorizontal);
const thumbHorizontalWidth = this.computeThumbHorizontalWidth(trackHorizontalInnerWidth);
const thumbHorizontalOffset = thumbHorizontalWidth ? this.content.scrollLeft / (this.content.scrollWidth - this.content.clientWidth) * (trackHorizontalInnerWidth - thumbHorizontalWidth) : 0;
let thumbHorizontalOffset = thumbHorizontalWidth
? this.content.scrollLeft / (this.content.scrollWidth - this.content.clientWidth) * (trackHorizontalInnerWidth - thumbHorizontalWidth)
: 0;

if (this.isRtl) {
thumbHorizontalOffset = -(trackHorizontalInnerWidth - thumbHorizontalWidth - thumbHorizontalOffset);
}

this.thumbHorizontal.style.transform = `translateX(${thumbHorizontalOffset}px)`;
this.thumbHorizontal.style.width = thumbHorizontalWidth + "px";
Expand All @@ -554,6 +577,7 @@ export default class Scrollbar extends React.Component
scrollWidth: this.content.scrollWidth,
clientHeight: this.content.clientHeight,
clientWidth: this.content.clientWidth,
rtl: this.props.rtl,
};

(this.previousScrollValues || false) && this.props.onScroll && this.props.onScroll(currentScrollValues, this);
Expand All @@ -569,7 +593,7 @@ export default class Scrollbar extends React.Component
minimalThumbsSize, fallbackScrollbarWidth, scrollDetectionThreshold,

// boolean props
defaultStyles, noScroll, noScrollX, noScrollY, permanentScrollbars, permanentScrollbarX, permanentScrollbarY,
defaultStyles, noScroll, noScrollX, noScrollY, permanentScrollbars, permanentScrollbarX, permanentScrollbarY, rtl,

// holder element props
tagName, children, style, className,
Expand Down Expand Up @@ -614,6 +638,7 @@ export default class Scrollbar extends React.Component
const holderStyles = {
...style,
...(defaultStyles && defaultElementsStyles.holder),
...({direction: (rtl === true && "rtl") || (rtl === false && "ltr") || null}),
},
wrapperStyles = {
...wrapperStyle,
Expand All @@ -626,9 +651,7 @@ export default class Scrollbar extends React.Component
...defaultElementsStyles.content,
overflowX: "scroll",
overflowY: "scroll",
marginRight: -(browserScrollbarWidth || fallbackScrollbarWidth),
marginBottom: -(browserScrollbarWidth || fallbackScrollbarWidth),
paddingRight: (browserScrollbarWidth ? null : fallbackScrollbarWidth),
paddingBottom: (browserScrollbarWidth ? null : fallbackScrollbarWidth),
},
trackVerticalStyles = {
Expand Down
71 changes: 70 additions & 1 deletion tests/Scrollbar/rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ export default function performTests() {
expect(this.content.style.left).toBe(0 + 'px');
expect(this.content.style.right).toBe(0 + 'px');
expect(this.content.style.overflow).toBe("scroll");
expect(this.content.style.marginRight).toBe(-getScrollbarWidth() + 'px');
expect(this.content.style.marginBottom).toBe(-getScrollbarWidth() + 'px');
done();
});
Expand Down Expand Up @@ -286,6 +285,76 @@ export default function performTests() {
});
});

describe("when RTL is set", () => {
it("left part of content should be hidden", (done) => {
render(<Scrollbar style={ {width: 100, height: 100} } rtl>
<div style={ {width: 200, height: 200} } />
</Scrollbar>,
node,
function () {
setTimeout(() => {
expect(this.content.style.marginRight).toBe("");
expect(this.content.style.marginLeft).toBe(-getScrollbarWidth() + 'px');

done();
}, 100);
});
});

it("should override direction value", (done) => {
let scrollbar = null;
render(<div style={ {direction: "rtl"} }>
<Scrollbar style={ {width: 100, height: 100} } ref={ (ref) => {scrollbar = ref;} } rtl={ false }>
<div style={ {width: 200, height: 200} } />
</Scrollbar>
</div>,
node,
function () {
setTimeout(() => {
expect(scrollbar.holder.classList.contains("ScrollbarsCustom-RTL")).toBeFalsy();

done();
}, 100);
});
});
});

describe("when RTL is not set", () => {
it("should autodetect direction (when set rtl)", (done) => {
let scrollbar = null;
render(<div style={ {direction: "rtl"} }>
<Scrollbar style={ {width: 100, height: 100} } ref={ (ref) => {scrollbar = ref;} }>
<div style={ {width: 200, height: 200} } />
</Scrollbar>
</div>,
node,
function () {
setTimeout(() => {
expect(scrollbar.holder.classList.contains("ScrollbarsCustom-RTL")).toBeTruthy();

done();
}, 100);
});
});

it("should autodetect direction (when set ltr)", (done) => {
let scrollbar = null;
render(<div style={ {direction: "ltr"} }>
<Scrollbar style={ {width: 100, height: 100} } ref={ (ref) => {scrollbar = ref;} } rtl={ false }>
<div style={ {width: 200, height: 200} } />
</Scrollbar>
</div>,
node,
function () {
setTimeout(() => {
expect(scrollbar.holder.classList.contains("ScrollbarsCustom-RTL")).toBeFalsy();

done();
}, 100);
});
});
});

describe("only vertical scroll should be blocked", () => {
it("if noScrollY={ true } is passed", (done) => {
render(<Scrollbar style={ {width: 100, height: 100} } noScrollY={ true }>
Expand Down

0 comments on commit 420d84c

Please sign in to comment.