Skip to content

Commit 420d84c

Browse files
committed
RTL support
1 parent 98032d7 commit 420d84c

File tree

7 files changed

+138
-13
lines changed

7 files changed

+138
-13
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* No matter what changes the content - scrollbars always stay actual
2121
* Total tests coverage
2222
* Scrollbars nesting
23+
* RTL support ([read more]())
2324

2425
>**IMPORTANT:** default component styles uses [Flexible Box Layout](https://developer.mozilla.org/ru/docs/Web/CSS/CSS_Flexible_Box_Layout) for proper scrollbars display.
2526
>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)).

docs/API.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* `defaultStyles`: _(boolean)_ Apply default inline styles _(default: false)_
77
* `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)_
88
* `minimalThumbsSize`: _(number)_ Minimal size of thumb in pixels _(default: 30)_
9-
* `noScroll`: _(boolean)_ Disable both vertical and horizontal scrolling_(default: false)_
9+
* `rtl`: _(boolean)_ Override the direction style parameter _(default: undefined)_
10+
* `noScroll`: _(boolean)_ Disable both vertical and horizontal scrolling _(default: false)_
1011
* `noScrollY`: _(boolean)_ Disable vertical scrolling _(default: false)_
1112
* `noScrollX`: _(boolean)_ Disable horizontal scrolling _(default: false)_
1213
* `permanentScrollbars`: _(boolean)_ Display both, vertical and horizontal scrollbars permanently, in spite of scrolling possibility _(default: false)_
@@ -63,5 +64,7 @@
6364
* `scrollToBottom()`: _(Scrollbar)_ Scroll to the bottom border
6465
* `scrollToLeft()`: _(Scrollbar)_ Scroll to the left border
6566
* `scrollToRight()`: _(Scrollbar)_ Scroll to the right border
66-
* `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.
67+
* `update(forced=false, rtlAutodetect=false)`: _(Scrollbar)_ Updates the scrollbars. By default if content or wrapper sizes did not changed - update will not be performed.
68+
* `forced`: _(boolean)_ Whether to update the scrollbars even nothing has changed _(default: false)_
69+
* `rtlAutodetect`: _(boolean)_ Whether to check and actualize CSS direction value _(default: false)_
6770
Keep in mind that forced update will either trigger `onScroll` callback.

docs/USAGE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,19 @@ class App extends Component
8787
}
8888
}
8989
```
90+
91+
### RTL support
92+
`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.
93+
But it has several nuances you should know:
94+
* Due to performance reasons, direction detection happens in 3 situations:
95+
* On component mount;
96+
* On rtl property change;
97+
* On call `scrollbar.update(undefined, true);`;
98+
* When rtl direction detected - `ScrollbarsCustom-RTL` classname will be added to the holder;
99+
* If `rtl` property has not set at all (undefined) - direction will be determined according to CSS;
100+
* If `rtl` property has `true` - `direction: rtl;` style will be applied to holder;
101+
* If `rtl` property has `false` - `direction: ltr;` style will be applied to holder;
102+
* `rtl` property has priority over the `style` property.
103+
```javascript
104+
<Scrollbar style={{direction: 'ltr'}} rtl /> // will have RTL direction
105+
```

examples/app/components/Body.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default class Head extends React.Component
2222
<li>No matter what changes the content - scrollbars always stay actual</li>
2323
<li>Total tests coverage</li>
2424
<li>Scrollbars nesting</li>
25+
<li>RTL support</li>
2526
</ul>
2627
<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>
2728
</div>

examples/app/components/blocks/SandboxBlock.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ export default class SandboxBlock extends React.Component
2020
this.handleScrollBottomClick = this.handleScrollBottomClick.bind(this);
2121
this.handleScrollLeftClick = this.handleScrollLeftClick.bind(this);
2222
this.handleScrollRightClick = this.handleScrollRightClick.bind(this);
23+
this.toggleRtl = this.toggleRtl.bind(this);
2324

2425
this.state = {
2526
noScroll: false,
2627
noScrollY: false,
2728
noScrollX: false,
2829

30+
rtl: false,
31+
2932
permanentTracks: false,
3033
permanentTrackY: false,
3134
permanentTrackX: false,
@@ -91,6 +94,13 @@ export default class SandboxBlock extends React.Component
9194
});
9295
}
9396

97+
toggleRtl() {
98+
this.setState({
99+
...this.state,
100+
rtl: !this.state.rtl,
101+
});
102+
}
103+
94104
handleAddParagraphClick() {
95105
this.setState({
96106
...this.state,
@@ -120,7 +130,7 @@ export default class SandboxBlock extends React.Component
120130
}
121131

122132
render() {
123-
const {noScroll, noScrollY, noScrollX, permanentTracks, permanentTrackY, permanentTrackX} = this.state;
133+
const {noScroll, noScrollY, noScrollX, permanentTracks, permanentTrackY, permanentTrackX, rtl} = this.state;
124134

125135
return (
126136
<div className="block" id="SandboxBlock">
@@ -133,6 +143,7 @@ export default class SandboxBlock extends React.Component
133143
<div className="button" key="permanentTracks" onClick={ this.togglePermanentTracks }>{ permanentTracks ? "Show tracks if needed" : "Always show tracks" }</div>
134144
<div className="button" key="permanentTracksY" onClick={ this.togglePermanentTrackY }>{ permanentTrackY || permanentTracks ? "Show track Y if needed" : "Always show track Y" }</div>
135145
<div className="button" key="permanentTracksX" onClick={ this.togglePermanentTrackX }>{ permanentTrackX || permanentTracks ? "Show track X if needed" : "Always show track X" }</div>
146+
<div className="button" key="direction" onClick={ this.toggleRtl }>{ rtl ? "set direction LRT" : "set direction RTL" }</div>
136147
<br />
137148
<div className="button" key="randomPosition" onClick={ this.handleRandomPositionClick }>Random position</div>
138149
<div className="button" key="scrollTop" onClick={ this.handleScrollTopClick }>Scroll top</div>
@@ -149,6 +160,7 @@ export default class SandboxBlock extends React.Component
149160
noScroll={ noScroll }
150161
noScrollY={ noScrollY }
151162
noScrollX={ noScrollX }
163+
rtl={ rtl }
152164
permanentScrollbars={ permanentTracks }
153165
permanentScrollbarY={ permanentTrackY }
154166
permanentScrollbarX={ permanentTrackX }>

src/index.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ export default class Scrollbar extends React.Component
127127
LoopController.registerScrollbar(this);
128128

129129
this.addListeners();
130-
this.update();
130+
this.update(true, true);
131131
}
132132

133133
componentDidUpdate(prevProps, prevState, snapshot) {
134-
if (prevProps.noScroll !== this.props.noScroll || prevProps.noScrollY !== this.props.noScrollY || prevProps.noScrollX !== this.props.noScrollX
134+
if (prevProps.noScroll !== this.props.noScroll || prevProps.noScrollY !== this.props.noScrollY || prevProps.noScrollX !== this.props.noScrollX || prevProps.rtl !== this.props.rtl
135135
|| prevProps.permanentScrollbars !== this.props.permanentScrollbars || prevProps.permanentScrollbarX !== this.props.permanentScrollbarX || prevProps.permanentScrollbarY !== this.props.permanentScrollbarY) {
136-
this.update(true);
136+
this.update(true, prevProps.rtl !== this.props.rtl);
137137
}
138138

139139
this.addListeners();
@@ -498,8 +498,9 @@ export default class Scrollbar extends React.Component
498498
* Performs an actualisation of scrollbars and its thumbs
499499
*
500500
* @param forced {boolean} Whether to perform an update even if nothing has changed
501+
* @param rtlAutodetect {boolean} Whether to check the CSS value `direction`
501502
*/
502-
update = (forced = false) => {
503+
update = (forced = false, rtlAutodetect = false) => {
503504
// No need to update scrollbars if values had not changed
504505
if (!forced && (this.previousScrollValues || false)) {
505506
if (this.previousScrollValues.scrollTop === this.content.scrollTop &&
@@ -512,9 +513,23 @@ export default class Scrollbar extends React.Component
512513
}
513514
}
514515

516+
this.isRtl = this.props.rtl || this.isRtl || (rtlAutodetect ? getComputedStyle(this.content).direction === "rtl" : false);
517+
518+
this.holder.classList.toggle("ScrollbarsCustom-RTL", this.isRtl);
519+
515520
const verticalScrollPossible = this.content.scrollHeight > this.content.clientHeight && !this.props.noScroll && !this.props.noScrollY,
516521
horizontalScrollPossible = this.content.scrollWidth > this.content.clientWidth && !this.props.noScroll && !this.props.noScrollX;
517522

523+
if (verticalScrollPossible && ((this.previousScrollValues || true) || this.isRtl !== (this.previousScrollValues.rtl || false))) {
524+
const browserScrollbarWidth = getScrollbarWidth(),
525+
fallbackScrollbarWidth = this.props.fallbackScrollbarWidth;
526+
527+
this.content.style.marginLeft = this.isRtl ? -(browserScrollbarWidth || fallbackScrollbarWidth) + "px" : null;
528+
this.content.style.paddingLeft = this.isRtl ? (browserScrollbarWidth ? null : fallbackScrollbarWidth) + "px" : null;
529+
this.content.style.marginRight = this.isRtl ? null : -(browserScrollbarWidth || fallbackScrollbarWidth) + "px";
530+
this.content.style.paddingRight = this.isRtl ? null : (browserScrollbarWidth ? null : fallbackScrollbarWidth) + "px";
531+
}
532+
518533
this.trackVertical.style.display = (verticalScrollPossible || this.props.permanentScrollbars || this.props.permanentScrollbarY) ? null : "none";
519534
this.trackVertical.visibility = (verticalScrollPossible || this.props.permanentScrollbars || this.props.permanentScrollbarY) ? null : "hidden";
520535

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

529546
this.thumbVertical.style.transform = `translateY(${thumbVerticalOffset}px)`;
530547
this.thumbVertical.style.height = thumbVerticalHeight + "px";
@@ -537,7 +554,13 @@ export default class Scrollbar extends React.Component
537554
if (horizontalScrollPossible) {
538555
const trackHorizontalInnerWidth = getInnerWidth(this.trackHorizontal);
539556
const thumbHorizontalWidth = this.computeThumbHorizontalWidth(trackHorizontalInnerWidth);
540-
const thumbHorizontalOffset = thumbHorizontalWidth ? this.content.scrollLeft / (this.content.scrollWidth - this.content.clientWidth) * (trackHorizontalInnerWidth - thumbHorizontalWidth) : 0;
557+
let thumbHorizontalOffset = thumbHorizontalWidth
558+
? this.content.scrollLeft / (this.content.scrollWidth - this.content.clientWidth) * (trackHorizontalInnerWidth - thumbHorizontalWidth)
559+
: 0;
560+
561+
if (this.isRtl) {
562+
thumbHorizontalOffset = -(trackHorizontalInnerWidth - thumbHorizontalWidth - thumbHorizontalOffset);
563+
}
541564

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

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

571595
// boolean props
572-
defaultStyles, noScroll, noScrollX, noScrollY, permanentScrollbars, permanentScrollbarX, permanentScrollbarY,
596+
defaultStyles, noScroll, noScrollX, noScrollY, permanentScrollbars, permanentScrollbarX, permanentScrollbarY, rtl,
573597

574598
// holder element props
575599
tagName, children, style, className,
@@ -614,6 +638,7 @@ export default class Scrollbar extends React.Component
614638
const holderStyles = {
615639
...style,
616640
...(defaultStyles && defaultElementsStyles.holder),
641+
...({direction: (rtl === true && "rtl") || (rtl === false && "ltr") || null}),
617642
},
618643
wrapperStyles = {
619644
...wrapperStyle,
@@ -626,9 +651,7 @@ export default class Scrollbar extends React.Component
626651
...defaultElementsStyles.content,
627652
overflowX: "scroll",
628653
overflowY: "scroll",
629-
marginRight: -(browserScrollbarWidth || fallbackScrollbarWidth),
630654
marginBottom: -(browserScrollbarWidth || fallbackScrollbarWidth),
631-
paddingRight: (browserScrollbarWidth ? null : fallbackScrollbarWidth),
632655
paddingBottom: (browserScrollbarWidth ? null : fallbackScrollbarWidth),
633656
},
634657
trackVerticalStyles = {

tests/Scrollbar/rendering.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ export default function performTests() {
198198
expect(this.content.style.left).toBe(0 + 'px');
199199
expect(this.content.style.right).toBe(0 + 'px');
200200
expect(this.content.style.overflow).toBe("scroll");
201-
expect(this.content.style.marginRight).toBe(-getScrollbarWidth() + 'px');
202201
expect(this.content.style.marginBottom).toBe(-getScrollbarWidth() + 'px');
203202
done();
204203
});
@@ -286,6 +285,76 @@ export default function performTests() {
286285
});
287286
});
288287

288+
describe("when RTL is set", () => {
289+
it("left part of content should be hidden", (done) => {
290+
render(<Scrollbar style={ {width: 100, height: 100} } rtl>
291+
<div style={ {width: 200, height: 200} } />
292+
</Scrollbar>,
293+
node,
294+
function () {
295+
setTimeout(() => {
296+
expect(this.content.style.marginRight).toBe("");
297+
expect(this.content.style.marginLeft).toBe(-getScrollbarWidth() + 'px');
298+
299+
done();
300+
}, 100);
301+
});
302+
});
303+
304+
it("should override direction value", (done) => {
305+
let scrollbar = null;
306+
render(<div style={ {direction: "rtl"} }>
307+
<Scrollbar style={ {width: 100, height: 100} } ref={ (ref) => {scrollbar = ref;} } rtl={ false }>
308+
<div style={ {width: 200, height: 200} } />
309+
</Scrollbar>
310+
</div>,
311+
node,
312+
function () {
313+
setTimeout(() => {
314+
expect(scrollbar.holder.classList.contains("ScrollbarsCustom-RTL")).toBeFalsy();
315+
316+
done();
317+
}, 100);
318+
});
319+
});
320+
});
321+
322+
describe("when RTL is not set", () => {
323+
it("should autodetect direction (when set rtl)", (done) => {
324+
let scrollbar = null;
325+
render(<div style={ {direction: "rtl"} }>
326+
<Scrollbar style={ {width: 100, height: 100} } ref={ (ref) => {scrollbar = ref;} }>
327+
<div style={ {width: 200, height: 200} } />
328+
</Scrollbar>
329+
</div>,
330+
node,
331+
function () {
332+
setTimeout(() => {
333+
expect(scrollbar.holder.classList.contains("ScrollbarsCustom-RTL")).toBeTruthy();
334+
335+
done();
336+
}, 100);
337+
});
338+
});
339+
340+
it("should autodetect direction (when set ltr)", (done) => {
341+
let scrollbar = null;
342+
render(<div style={ {direction: "ltr"} }>
343+
<Scrollbar style={ {width: 100, height: 100} } ref={ (ref) => {scrollbar = ref;} } rtl={ false }>
344+
<div style={ {width: 200, height: 200} } />
345+
</Scrollbar>
346+
</div>,
347+
node,
348+
function () {
349+
setTimeout(() => {
350+
expect(scrollbar.holder.classList.contains("ScrollbarsCustom-RTL")).toBeFalsy();
351+
352+
done();
353+
}, 100);
354+
});
355+
});
356+
});
357+
289358
describe("only vertical scroll should be blocked", () => {
290359
it("if noScrollY={ true } is passed", (done) => {
291360
render(<Scrollbar style={ {width: 100, height: 100} } noScrollY={ true }>

0 commit comments

Comments
 (0)