Skip to content

Commit e4cb316

Browse files
committed
Add RTL support with htmlDir property
Resolves #76.
1 parent fe5d102 commit e4cb316

9 files changed

Lines changed: 255 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### New Features
66

7+
* [#76]: **(breaking)** Add `htmlDir` property to support RTL languages (defaults to LTR)
78
* [#146]: Add styles for small viewport devices (<576px)
89
* [#203]: Add `controlKey` to the `onChange` function such that developers may identify which control triggered the change
910
* [#209]: **(breaking)** Add `iconsClass`, make icons more semantic, and support Unicode icons

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ class Widget extends React.Component {
306306
| `disabled` | bool | If true, both "available" and "selected" list boxes will be disabled. | `false` |
307307
| `filterCallback` | function | The filter function to run on a given option and input string: `function(option, filterInput) {}`. See **Filtering**. | `() => { ... }` |
308308
| `filterPlaceholder` | string | The text placeholder used when the filter search boxes are empty. | `"Search..."` |
309+
| `htmlDir` | string | The [directionality][mdn-directionality] of the component's elements. Set to `'rtl'` if using a right-to-left language. | `'ltr'` |
309310
| `icons` | object | A key-value pairing of action icons and their React nodes. See **Changing the Default Icons** for further info. | `{ ... }` |
310311
| `iconsClass` | string | A value specifying which overarching icon class to use. Built-in support for `fa5`, `fa6`, and `native` icons. | `'fa5'` |
311312
| `id` | string | An HTML ID prefix for the various sub elements. | `null` |
@@ -331,4 +332,5 @@ class Widget extends React.Component {
331332
332333
[controlled]: https://facebook.github.io/react/docs/forms.html#controlled-components
333334
[lang-file]: https://github.com/jakezatecky/react-dual-listbox/blob/master/src/js/lang/default.js
335+
[mdn-directionality]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
334336
[ref]: https://reactjs.org/docs/refs-and-the-dom.html

examples/src/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ <h3>Allow Duplicates Example</h3>
9393
</p>
9494
<div id="allow-duplicates-example"></div>
9595

96+
<h3>RTL Support Example</h3>
97+
<p>
98+
To support right-to-left languages, pass <code>htmlFor="rtl"</code> to the component.
99+
</p>
100+
<div id="rtl-example"></div>
101+
96102
<h3>Restrict Available Example</h3>
97103
<p>
98104
In the following example, we are restricting which options are available for selection. This may be useful if

examples/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import FilterExample from './js/FilterExample';
99
import PreserveSelectOrderExample from './js/PreserveSelectOrderExample';
1010
import OptGroupExample from './js/OptGroupExample';
1111
import RestrictAvailable from './js/RestrictAvailableExample';
12+
import RtlExample from './js/RtlExample';
1213

1314
ReactDOM.render(<BasicExample />, document.getElementById('basic-example'));
1415
ReactDOM.render(<OptGroupExample />, document.getElementById('optgroup-example'));
@@ -17,4 +18,5 @@ ReactDOM.render(<AlignTopExample />, document.getElementById('align-top-example'
1718
ReactDOM.render(<DisabledExample />, document.getElementById('disabled-example'));
1819
ReactDOM.render(<PreserveSelectOrderExample />, document.getElementById('preserve-select-order-example'));
1920
ReactDOM.render(<AllowDuplicatesExample />, document.getElementById('allow-duplicates-example'));
21+
ReactDOM.render(<RtlExample />, document.getElementById('rtl-example'));
2022
ReactDOM.render(<RestrictAvailable />, document.getElementById('restrict-available-example'));

examples/src/js/RtlExample.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import DualListBox from 'react-dual-listbox';
3+
4+
const options = [
5+
{
6+
label: 'Earth',
7+
options: [
8+
{ value: 'luna', label: 'Moon' },
9+
],
10+
},
11+
{
12+
label: 'Mars',
13+
options: [
14+
{ value: 'phobos', label: 'Phobos' },
15+
{ value: 'deimos', label: 'Deimos' },
16+
],
17+
},
18+
{
19+
label: 'Jupiter',
20+
options: [
21+
{ value: 'io', label: 'Io' },
22+
{ value: 'europa', label: 'Europa' },
23+
{ value: 'ganymede', label: 'Ganymede' },
24+
{ value: 'callisto', label: 'Callisto' },
25+
],
26+
},
27+
];
28+
29+
class RtlExample extends React.Component {
30+
state = { selected: ['phobos', 'europa', 'callisto'] };
31+
32+
constructor(props) {
33+
super(props);
34+
35+
this.onChange = this.onChange.bind(this);
36+
}
37+
38+
onChange(selected) {
39+
this.setState({ selected });
40+
}
41+
42+
render() {
43+
const { selected } = this.state;
44+
45+
return (
46+
<DualListBox
47+
htmlDir="rtl"
48+
options={options}
49+
selected={selected}
50+
onChange={this.onChange}
51+
/>
52+
);
53+
}
54+
}
55+
56+
export default RtlExample;

src/js/DualListBox.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class DualListBox extends React.Component {
7171
}),
7272
filterCallback: PropTypes.func,
7373
filterPlaceholder: PropTypes.string,
74+
htmlDir: PropTypes.string,
7475
icons: iconsShape,
7576
iconsClass: PropTypes.string,
7677
id: PropTypes.string,
@@ -98,6 +99,7 @@ class DualListBox extends React.Component {
9899
filter: null,
99100
filterPlaceholder: 'Search...',
100101
filterCallback: defaultFilter,
102+
htmlDir: 'ltr',
101103
icons: defaultIcons,
102104
iconsClass: 'fa5',
103105
id: null,
@@ -820,6 +822,7 @@ class DualListBox extends React.Component {
820822
canFilter,
821823
className,
822824
disabled,
825+
htmlDir,
823826
icons,
824827
iconsClass,
825828
lang,
@@ -868,7 +871,7 @@ class DualListBox extends React.Component {
868871
const value = this.getFlatOptions(selected).join(',');
869872

870873
return (
871-
<div className={rootClassName} id={id}>
874+
<div className={rootClassName} dir={htmlDir} id={id}>
872875
{this.renderListBox('available', availableOptions, availableRef, actionsRight)}
873876
{alignActions === ALIGNMENTS.MIDDLE ? (
874877
<div className="rdl-actions">

src/less/react-dual-listbox.less

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,27 +206,61 @@
206206

207207
&:first-child {
208208
margin-bottom: 0;
209-
border-right: 0;
210-
border-top-right-radius: 0;
211209
border-bottom-left-radius: 0;
212210
border-bottom-right-radius: 0;
213211
}
214212

215213
&:last-child {
216-
border-top-left-radius: 0;
217214
border-bottom-left-radius: 0;
218215
border-bottom-right-radius: 0;
219216
}
220217
}
221218

219+
&[dir="ltr"] {
220+
.rdl-list-box .rdl-move {
221+
&:first-child {
222+
border-right: 0;
223+
border-top-right-radius: 0;
224+
}
225+
226+
&:last-child {
227+
border-top-right-radius: 0;
228+
}
229+
}
230+
}
231+
232+
&[dir="rtl"] {
233+
.rdl-list-box .rdl-move {
234+
&:first-child {
235+
border-left: 0;
236+
border-top-left-radius: 0;
237+
}
238+
239+
&:last-child {
240+
border-top-left-radius: 0;
241+
}
242+
}
243+
}
222244

223245
@media (min-width: @rdl-desktop-min-width) {
224-
.rdl-available {
225-
margin: 0 10px 0 0;
246+
&[dir="ltr"] {
247+
.rdl-available {
248+
margin: 0 10px 0 0;
249+
}
250+
251+
.rdl-selected {
252+
margin-left: 10px;
253+
}
226254
}
227255

228-
.rdl-selected {
229-
margin-left: 10px;
256+
&[dir="rtl"] {
257+
.rdl-available {
258+
margin: 0 0 0 10px;
259+
}
260+
261+
.rdl-selected {
262+
margin-right: 10px;
263+
}
230264
}
231265
}
232266
}
@@ -275,6 +309,25 @@
275309
.rdl-icon-move-all-right::before {
276310
content: "\f101";
277311
}
312+
313+
// Change directions when right-to-left
314+
&[dir="rtl"] {
315+
.rdl-icon-move-left::before {
316+
content: "\f105";
317+
}
318+
319+
.rdl-icon-move-all-left::before {
320+
content: "\f101";
321+
}
322+
323+
.rdl-icon-move-right::before {
324+
content: "\f104";
325+
}
326+
327+
.rdl-icon-move-all-right::before {
328+
content: "\f100";
329+
}
330+
}
278331
}
279332
}
280333

@@ -324,5 +377,24 @@
324377
.rdl-icon-move-all-right::before {
325378
content: "";
326379
}
380+
381+
// Change directions when right-to-left
382+
&[dir="rtl"] {
383+
.rdl-icon-move-left::before {
384+
content: "";
385+
}
386+
387+
.rdl-icon-move-all-left::before {
388+
content: "";
389+
}
390+
391+
.rdl-icon-move-right::before {
392+
content: "";
393+
}
394+
395+
.rdl-icon-move-all-right::before {
396+
content: "";
397+
}
398+
}
327399
}
328400
}

src/scss/react-dual-listbox.scss

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,26 +209,61 @@ $rdl-desktop-min-width: 576px !default;
209209

210210
&:first-child {
211211
margin-bottom: 0;
212-
border-right: 0;
213-
border-top-right-radius: 0;
214212
border-bottom-left-radius: 0;
215213
border-bottom-right-radius: 0;
216214
}
217215

218216
&:last-child {
219-
border-top-left-radius: 0;
220217
border-bottom-left-radius: 0;
221218
border-bottom-right-radius: 0;
222219
}
223220
}
224221

222+
&[dir="ltr"] {
223+
.rdl-list-box .rdl-move {
224+
&:first-child {
225+
border-right: 0;
226+
border-top-right-radius: 0;
227+
}
228+
229+
&:last-child {
230+
border-top-right-radius: 0;
231+
}
232+
}
233+
}
234+
235+
&[dir="rtl"] {
236+
.rdl-list-box .rdl-move {
237+
&:first-child {
238+
border-left: 0;
239+
border-top-left-radius: 0;
240+
}
241+
242+
&:last-child {
243+
border-top-left-radius: 0;
244+
}
245+
}
246+
}
247+
225248
@media (min-width: $rdl-desktop-min-width) {
226-
.rdl-available {
227-
margin: 0 10px 0 0;
249+
&[dir="ltr"] {
250+
.rdl-available {
251+
margin: 0 10px 0 0;
252+
}
253+
254+
.rdl-selected {
255+
margin-left: 10px;
256+
}
228257
}
229258

230-
.rdl-selected {
231-
margin-left: 10px;
259+
&[dir="rtl"] {
260+
.rdl-available {
261+
margin: 0 0 0 10px;
262+
}
263+
264+
.rdl-selected {
265+
margin-right: 10px;
266+
}
232267
}
233268
}
234269
}
@@ -277,6 +312,25 @@ $rdl-desktop-min-width: 576px !default;
277312
.rdl-icon-move-all-right::before {
278313
content: unicode("f101");
279314
}
315+
316+
// Change directions when right-to-left
317+
&[dir="rtl"] {
318+
.rdl-icon-move-left::before {
319+
content: unicode("f105");
320+
}
321+
322+
.rdl-icon-move-all-left::before {
323+
content: unicode("f101");
324+
}
325+
326+
.rdl-icon-move-right::before {
327+
content: unicode("f104");
328+
}
329+
330+
.rdl-icon-move-all-right::before {
331+
content: unicode("f100");
332+
}
333+
}
280334
}
281335
}
282336

@@ -326,5 +380,24 @@ $rdl-desktop-min-width: 576px !default;
326380
.rdl-icon-move-all-right::before {
327381
content: "";
328382
}
383+
384+
// Change directions when right-to-left
385+
&[dir="rtl"] {
386+
.rdl-icon-move-left::before {
387+
content: "";
388+
}
389+
390+
.rdl-icon-move-all-left::before {
391+
content: "";
392+
}
393+
394+
.rdl-icon-move-right::before {
395+
content: "";
396+
}
397+
398+
.rdl-icon-move-all-right::before {
399+
content: "";
400+
}
401+
}
329402
}
330403
}

0 commit comments

Comments
 (0)