@patternfly/react-core 4.221.13 → 4.221.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/esm/components/DualListSelector/DualListSelector.d.ts +4 -0
- package/dist/esm/components/DualListSelector/DualListSelector.d.ts.map +1 -1
- package/dist/esm/components/DualListSelector/DualListSelector.js +13 -2
- package/dist/esm/components/DualListSelector/DualListSelector.js.map +1 -1
- package/dist/js/components/DualListSelector/DualListSelector.d.ts +4 -0
- package/dist/js/components/DualListSelector/DualListSelector.d.ts.map +1 -1
- package/dist/js/components/DualListSelector/DualListSelector.js +13 -2
- package/dist/js/components/DualListSelector/DualListSelector.js.map +1 -1
- package/dist/umd/react-core.js +13 -2
- package/dist/umd/react-core.min.css +69 -69
- package/dist/umd/react-core.min.js +1 -1
- package/package.json +5 -5
- package/src/components/DualListSelector/DualListSelector.tsx +14 -2
- package/src/components/DualListSelector/examples/DualListSelector.md +38 -767
- package/src/components/DualListSelector/examples/DualListSelectorBasic.tsx +26 -0
- package/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx +27 -0
- package/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx +34 -0
- package/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx +146 -0
- package/src/components/DualListSelector/examples/DualListSelectorComposable.tsx +214 -0
- package/src/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx +190 -0
- package/src/components/DualListSelector/examples/{ComposableTree.tsx → DualListSelectorComposableTree.tsx} +3 -3
- package/src/components/DualListSelector/examples/DualListSelectorTree.tsx +91 -0
- package/src/components/FileUpload/examples/FileUpload.md +6 -270
- package/src/components/FileUpload/examples/FileUploadCustomPreview.tsx +40 -0
- package/src/components/FileUpload/examples/FileUploadCustomUpload.tsx +116 -0
- package/src/components/FileUpload/examples/FileUploadSimpleFile.tsx +31 -0
- package/src/components/FileUpload/examples/FileUploadSimpleText.tsx +51 -0
- package/src/components/FileUpload/examples/FileUploadTextWithEdits.tsx +51 -0
- package/src/components/FileUpload/examples/FileUploadTextWithRestrictions.tsx +71 -0
- package/src/components/FormSelect/examples/FormSelect.md +5 -291
- package/src/components/FormSelect/examples/FormSelectBasic.tsx +28 -0
- package/src/components/FormSelect/examples/FormSelectDisabled.tsx +28 -0
- package/src/components/FormSelect/examples/FormSelectGrouped.tsx +49 -0
- package/src/components/FormSelect/examples/FormSelectIconSpriteVariant.tsx +57 -0
- package/src/components/FormSelect/examples/FormSelectValidated.tsx +69 -0
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
id: Dual list selector
|
|
3
3
|
section: components
|
|
4
4
|
cssPrefix: 'pf-c-dual-list-selector'
|
|
5
|
-
propComponents:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
propComponents:
|
|
6
|
+
[
|
|
7
|
+
'DualListSelector',
|
|
8
|
+
'DualListSelectorPane',
|
|
9
|
+
'DualListSelectorControl',
|
|
10
|
+
'DualListSelectorControlsWrapper',
|
|
11
|
+
'DualListSelectorTree',
|
|
12
|
+
'DualListSelectorTreeItemData',
|
|
13
|
+
]
|
|
13
14
|
beta: true
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -23,806 +24,76 @@ import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pfic
|
|
|
23
24
|
|
|
24
25
|
### Basic
|
|
25
26
|
|
|
26
|
-
```
|
|
27
|
-
import React from 'react';
|
|
28
|
-
import { DualListSelector } from '@patternfly/react-core';
|
|
29
|
-
|
|
30
|
-
class BasicDualListSelector extends React.Component {
|
|
31
|
-
constructor(props) {
|
|
32
|
-
super(props);
|
|
33
|
-
this.state = {
|
|
34
|
-
availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
|
|
35
|
-
chosenOptions: []
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
this.onListChange = (newAvailableOptions, newChosenOptions) => {
|
|
39
|
-
this.setState({
|
|
40
|
-
availableOptions: newAvailableOptions.sort(),
|
|
41
|
-
chosenOptions: newChosenOptions.sort()
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
render() {
|
|
47
|
-
return (
|
|
48
|
-
<DualListSelector
|
|
49
|
-
availableOptions={this.state.availableOptions}
|
|
50
|
-
chosenOptions={this.state.chosenOptions}
|
|
51
|
-
onListChange={this.onListChange}
|
|
52
|
-
id="basicSelector"
|
|
53
|
-
/>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
27
|
+
```ts file="./DualListSelectorBasic.tsx"
|
|
57
28
|
```
|
|
58
29
|
|
|
59
30
|
### Basic with tooltips
|
|
60
31
|
|
|
61
|
-
```
|
|
62
|
-
import React from 'react';
|
|
63
|
-
import { DualListSelector } from '@patternfly/react-core';
|
|
64
|
-
|
|
65
|
-
class BasicDualListSelector extends React.Component {
|
|
66
|
-
constructor(props) {
|
|
67
|
-
super(props);
|
|
68
|
-
this.state = {
|
|
69
|
-
availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
|
|
70
|
-
chosenOptions: []
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
this.onListChange = (newAvailableOptions, newChosenOptions) => {
|
|
74
|
-
this.setState({
|
|
75
|
-
availableOptions: newAvailableOptions.sort(),
|
|
76
|
-
chosenOptions: newChosenOptions.sort()
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
render() {
|
|
82
|
-
return (
|
|
83
|
-
<DualListSelector
|
|
84
|
-
availableOptions={this.state.availableOptions}
|
|
85
|
-
chosenOptions={this.state.chosenOptions}
|
|
86
|
-
onListChange={this.onListChange}
|
|
87
|
-
addAllTooltip="Add all options"
|
|
88
|
-
addAllTooltipProps={{ position: 'top' }}
|
|
89
|
-
addSelectedTooltip="Add selected options"
|
|
90
|
-
addSelectedTooltipProps={{ position: 'right' }}
|
|
91
|
-
removeSelectedTooltip="Remove selected options"
|
|
92
|
-
removeSelectedTooltipProps={{ position: 'left' }}
|
|
93
|
-
removeAllTooltip="Remove all options"
|
|
94
|
-
removeAllTooltipProps={{ position: 'bottom' }}
|
|
95
|
-
id="basicSelector"
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
32
|
+
```ts file="./DualListSelectorBasicTooltips.tsx"
|
|
100
33
|
```
|
|
101
34
|
|
|
102
35
|
### Basic with search
|
|
103
36
|
|
|
104
|
-
```
|
|
105
|
-
import React from 'react';
|
|
106
|
-
import { DualListSelector } from '@patternfly/react-core';
|
|
107
|
-
|
|
108
|
-
class BasicDualListSelectorWithSearch extends React.Component {
|
|
109
|
-
constructor(props) {
|
|
110
|
-
super(props);
|
|
111
|
-
this.state = {
|
|
112
|
-
availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
|
|
113
|
-
chosenOptions: []
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
this.onListChange = (newAvailableOptions, newChosenOptions) => {
|
|
117
|
-
this.setState({
|
|
118
|
-
availableOptions: newAvailableOptions.sort(),
|
|
119
|
-
chosenOptions: newChosenOptions.sort()
|
|
120
|
-
});
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
render() {
|
|
125
|
-
return (
|
|
126
|
-
<DualListSelector
|
|
127
|
-
isSearchable
|
|
128
|
-
availableOptions={this.state.availableOptions}
|
|
129
|
-
chosenOptions={this.state.chosenOptions}
|
|
130
|
-
onListChange={this.onListChange}
|
|
131
|
-
id="basicSelectorWithSearch"
|
|
132
|
-
/>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
37
|
+
```ts file="./DualListSelectorBasicSearch.tsx"
|
|
136
38
|
```
|
|
137
39
|
|
|
138
40
|
### Using more complex options with actions
|
|
139
41
|
|
|
140
|
-
```
|
|
141
|
-
import React from 'react';
|
|
142
|
-
import { Button, ButtonVariant, Checkbox, Dropdown, DropdownItem, DualListSelector, KebabToggle } from '@patternfly/react-core';
|
|
143
|
-
import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon';
|
|
144
|
-
|
|
145
|
-
class ComplexDualListSelector extends React.Component {
|
|
146
|
-
constructor(props) {
|
|
147
|
-
super(props);
|
|
148
|
-
this.state = {
|
|
149
|
-
availableOptions: [<span>Option 1</span>, <span>Option 3</span>, <span>Option 4</span>, <span>Option 2</span>],
|
|
150
|
-
chosenOptions: [],
|
|
151
|
-
isAvailableKebabOpen: false,
|
|
152
|
-
isChosenKebabOpen: false,
|
|
153
|
-
isDisabled: false
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
this.onSort = panel => {
|
|
157
|
-
if (panel === 'available') {
|
|
158
|
-
this.setState(prevState => {
|
|
159
|
-
const available = prevState.availableOptions.sort((a, b) => {
|
|
160
|
-
let returnValue = 0;
|
|
161
|
-
if (a.props.children > b.props.children) returnValue = 1;
|
|
162
|
-
if (a.props.children < b.props.children) returnValue = -1;
|
|
163
|
-
return returnValue;
|
|
164
|
-
});
|
|
165
|
-
return {
|
|
166
|
-
availableOptions: available
|
|
167
|
-
};
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (panel === 'chosen') {
|
|
172
|
-
this.setState(prevState => {
|
|
173
|
-
const chosen = prevState.chosenOptions.sort((a, b) => {
|
|
174
|
-
let returnValue = 0;
|
|
175
|
-
if (a.props.children > b.props.children) returnValue = 1;
|
|
176
|
-
if (a.props.children < b.props.children) returnValue = -1;
|
|
177
|
-
return returnValue;
|
|
178
|
-
});
|
|
179
|
-
return {
|
|
180
|
-
chosenOptions: chosen
|
|
181
|
-
};
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
this.onListChange = (newAvailableOptions, newChosenOptions) => {
|
|
187
|
-
this.setState({
|
|
188
|
-
availableOptions: newAvailableOptions,
|
|
189
|
-
chosenOptions: newChosenOptions
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
this.onToggle = (isOpen, pane) => {
|
|
194
|
-
this.setState(prevState => {
|
|
195
|
-
return {
|
|
196
|
-
isAvailableKebabOpen: pane === 'available' ? isOpen : prevState.isAvailableKebabOpen,
|
|
197
|
-
isChosenKebabOpen: pane === 'chosen' ? isOpen : prevState.isChosenKebabOpen
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
this.filterOption = (option, input) => {
|
|
203
|
-
return option.props.children.includes(input);
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
render() {
|
|
208
|
-
const dropdownItems = [
|
|
209
|
-
<DropdownItem key="link">Link</DropdownItem>,
|
|
210
|
-
<DropdownItem key="action" component="button">
|
|
211
|
-
Action
|
|
212
|
-
</DropdownItem>,
|
|
213
|
-
<DropdownItem key="second action" component="button">
|
|
214
|
-
Second Action
|
|
215
|
-
</DropdownItem>
|
|
216
|
-
];
|
|
217
|
-
|
|
218
|
-
const availableOptionsActions = [
|
|
219
|
-
<Button
|
|
220
|
-
variant={ButtonVariant.plain}
|
|
221
|
-
onClick={() => this.onSort('available')}
|
|
222
|
-
aria-label="Sort"
|
|
223
|
-
key="availableSortButton"
|
|
224
|
-
isDisabled={this.state.isDisabled}
|
|
225
|
-
>
|
|
226
|
-
<PficonSortCommonAscIcon />
|
|
227
|
-
</Button>,
|
|
228
|
-
<Dropdown
|
|
229
|
-
toggle={<KebabToggle isDisabled={this.state.isDisabled} onToggle={isOpen => this.onToggle(isOpen, 'available')} id="toggle-id-1" />}
|
|
230
|
-
isOpen={this.state.isAvailableKebabOpen}
|
|
231
|
-
isPlain
|
|
232
|
-
dropdownItems={dropdownItems}
|
|
233
|
-
key="availableDropdown"
|
|
234
|
-
/>
|
|
235
|
-
];
|
|
236
|
-
|
|
237
|
-
const chosenOptionsActions = [
|
|
238
|
-
<Button
|
|
239
|
-
variant={ButtonVariant.plain}
|
|
240
|
-
onClick={() => this.onSort('chosen')}
|
|
241
|
-
aria-label="Sort"
|
|
242
|
-
key="chosenSortButton"
|
|
243
|
-
isDisabled={this.state.isDisabled}
|
|
244
|
-
>
|
|
245
|
-
<PficonSortCommonAscIcon />
|
|
246
|
-
</Button>,
|
|
247
|
-
<Dropdown
|
|
248
|
-
toggle={<KebabToggle isDisabled={this.state.isDisabled} onToggle={isOpen => this.onToggle(isOpen, 'chosen')} id="toggle-id-2" />}
|
|
249
|
-
isOpen={this.state.isChosenKebabOpen}
|
|
250
|
-
isPlain
|
|
251
|
-
dropdownItems={dropdownItems}
|
|
252
|
-
key="chosenDropdown"
|
|
253
|
-
/>
|
|
254
|
-
];
|
|
255
|
-
|
|
256
|
-
return (
|
|
257
|
-
<React.Fragment>
|
|
258
|
-
<DualListSelector
|
|
259
|
-
isSearchable
|
|
260
|
-
availableOptions={this.state.availableOptions}
|
|
261
|
-
availableOptionsActions={availableOptionsActions}
|
|
262
|
-
chosenOptions={this.state.chosenOptions}
|
|
263
|
-
chosenOptionsActions={chosenOptionsActions}
|
|
264
|
-
addAll={this.onListChange}
|
|
265
|
-
removeAll={this.onListChange}
|
|
266
|
-
addSelected={this.onListChange}
|
|
267
|
-
removeSelected={this.onListChange}
|
|
268
|
-
filterOption={this.filterOption}
|
|
269
|
-
isDisabled={this.state.isDisabled}
|
|
270
|
-
id="withActions"
|
|
271
|
-
/>
|
|
272
|
-
<Checkbox
|
|
273
|
-
key="isDisabled"
|
|
274
|
-
id="isDisabled"
|
|
275
|
-
label="isDisabled"
|
|
276
|
-
aria-label="isDisabled"
|
|
277
|
-
isChecked={this.state.isDisabled}
|
|
278
|
-
onChange={() =>
|
|
279
|
-
this.setState({
|
|
280
|
-
isDisabled: !this.state.isDisabled
|
|
281
|
-
})
|
|
282
|
-
}
|
|
283
|
-
/>
|
|
284
|
-
</React.Fragment>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
42
|
+
```ts file="./DualListSelectorComplexOptionsActions.tsx"
|
|
288
43
|
```
|
|
289
44
|
|
|
290
|
-
###
|
|
291
|
-
|
|
292
|
-
```js
|
|
293
|
-
import React from 'react';
|
|
294
|
-
import { DualListSelector } from '@patternfly/react-core';
|
|
45
|
+
### With tree
|
|
295
46
|
|
|
296
|
-
|
|
297
|
-
constructor(props) {
|
|
298
|
-
super(props);
|
|
299
|
-
this.state = {
|
|
300
|
-
chosenOptions: [
|
|
301
|
-
{
|
|
302
|
-
id: 'CF1',
|
|
303
|
-
text: 'Chosen Folder 1',
|
|
304
|
-
isChecked: false,
|
|
305
|
-
checkProps: { 'aria-label': 'Chosen Folder 1' },
|
|
306
|
-
hasBadge: true,
|
|
307
|
-
badgeProps: { isRead: true },
|
|
308
|
-
children: [
|
|
309
|
-
{ id: 'CO1', text: 'Chosen Option 1', isChecked: false, checkProps: { 'aria-label': 'Chosen Option 1' } },
|
|
310
|
-
{
|
|
311
|
-
id: 'CF1A',
|
|
312
|
-
text: 'Chosen Folder 1A',
|
|
313
|
-
isChecked: false,
|
|
314
|
-
checkProps: { 'aria-label': 'Chosen Folder 1A' },
|
|
315
|
-
children: [
|
|
316
|
-
{
|
|
317
|
-
id: 'CO2',
|
|
318
|
-
text: 'Chosen Option 2',
|
|
319
|
-
isChecked: false,
|
|
320
|
-
checkProps: { 'aria-label': 'Chosen Option 2' }
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
id: 'CO3',
|
|
324
|
-
text: 'Chosen Option 3',
|
|
325
|
-
isChecked: false,
|
|
326
|
-
checkProps: { 'aria-label': 'Chosen Option 3' }
|
|
327
|
-
}
|
|
328
|
-
]
|
|
329
|
-
},
|
|
330
|
-
{ id: 'CO4', text: 'Chosen Option 4', isChecked: false, checkProps: { 'aria-label': 'Chosen Option 4' } }
|
|
331
|
-
]
|
|
332
|
-
}
|
|
333
|
-
],
|
|
334
|
-
availableOptions: [
|
|
335
|
-
{
|
|
336
|
-
id: 'F1',
|
|
337
|
-
text: 'Folder 1',
|
|
338
|
-
isChecked: false,
|
|
339
|
-
checkProps: { 'aria-label': 'Folder 1' },
|
|
340
|
-
hasBadge: true,
|
|
341
|
-
badgeProps: { isRead: true },
|
|
342
|
-
children: [
|
|
343
|
-
{ id: 'O1', text: 'Option 1', isChecked: false, checkProps: { 'aria-label': 'Option 1' } },
|
|
344
|
-
{
|
|
345
|
-
id: 'F1A',
|
|
346
|
-
text: 'Folder 1A',
|
|
347
|
-
isChecked: false,
|
|
348
|
-
checkProps: { 'aria-label': 'Folder 1A' },
|
|
349
|
-
children: [
|
|
350
|
-
{ id: 'O2', text: 'Option 2', isChecked: false, checkProps: { 'aria-label': 'Option 2' } },
|
|
351
|
-
{ id: 'O3', text: 'Option 3', isChecked: false, checkProps: { 'aria-label': 'Option 3' } }
|
|
352
|
-
]
|
|
353
|
-
},
|
|
354
|
-
{ id: 'O4', text: 'Option 4', isChecked: false, checkProps: { 'aria-label': 'Option 4' } }
|
|
355
|
-
]
|
|
356
|
-
},
|
|
357
|
-
{ id: 'O5', text: 'Option 5', isChecked: false, checkProps: { 'aria-label': 'Option 5' } },
|
|
358
|
-
{
|
|
359
|
-
id: 'F2',
|
|
360
|
-
text: 'Folder 2',
|
|
361
|
-
isChecked: false,
|
|
362
|
-
checkProps: { 'aria-label': 'Folder 2' },
|
|
363
|
-
children: [
|
|
364
|
-
{ id: 'O6', text: 'Option 6', isChecked: false, checkProps: { 'aria-label': 'Option 6' } },
|
|
365
|
-
{ id: 'O7', text: 'Option 5', isChecked: false, checkProps: { 'aria-label': 'Option 5 duplicate' } }
|
|
366
|
-
]
|
|
367
|
-
}
|
|
368
|
-
]
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
this.onListChange = (newAvailableOptions, newChosenOptions) => {
|
|
372
|
-
this.setState({
|
|
373
|
-
availableOptions: newAvailableOptions,
|
|
374
|
-
chosenOptions: newChosenOptions
|
|
375
|
-
});
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
render() {
|
|
380
|
-
return (
|
|
381
|
-
<DualListSelector
|
|
382
|
-
availableOptions={this.state.availableOptions}
|
|
383
|
-
chosenOptions={this.state.chosenOptions}
|
|
384
|
-
onListChange={this.onListChange}
|
|
385
|
-
id="basicSelector"
|
|
386
|
-
isSearchable
|
|
387
|
-
isTree
|
|
388
|
-
/>
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
47
|
+
```ts file="./DualListSelectorTree.tsx"
|
|
392
48
|
```
|
|
393
49
|
|
|
394
50
|
### Composable dual list selector
|
|
395
51
|
|
|
396
|
-
For more flexibility, a
|
|
397
|
-
relationships are arranged as follows:
|
|
398
|
-
|
|
399
|
-
```js noLive
|
|
400
|
-
import React from 'react';
|
|
401
|
-
import { DualListSelector, DualListSelectorPane, DualListSelectorList, DualListSelectorListItem, DualListSelectorControlsWrapper, DualListSelectorControl } from '@patternfly/react-core';
|
|
52
|
+
For more flexibility, a dual list selector can be built using sub components. When doing so, the intended component relationships are arranged as follows:
|
|
402
53
|
|
|
54
|
+
```noLive
|
|
403
55
|
<DualListSelector>
|
|
404
|
-
|
|
405
56
|
<DualListSelectorPane>
|
|
406
57
|
<DualListSelectorList>
|
|
407
|
-
<DualListSelectorListItem/>
|
|
58
|
+
<DualListSelectorListItem />
|
|
408
59
|
</DualListSelectorList>
|
|
409
60
|
</DualListSelectorPane>
|
|
410
|
-
|
|
61
|
+
|
|
411
62
|
<DualListSelectorControlsWrapper>
|
|
412
|
-
<DualListSelectorControl/>
|
|
63
|
+
<DualListSelectorControl /> /* The standard Dual list selector has 4 controls */
|
|
413
64
|
</DualListSelectorControlsWrapper>
|
|
414
|
-
|
|
65
|
+
|
|
415
66
|
<DualListSelectorPane isChosen>
|
|
416
67
|
<DualListSelectorList>
|
|
417
|
-
<DualListSelectorListItem/>
|
|
68
|
+
<DualListSelectorListItem />
|
|
418
69
|
</DualListSelectorList>
|
|
419
70
|
</DualListSelectorPane>
|
|
420
|
-
|
|
421
71
|
</DualListSelector>
|
|
422
72
|
```
|
|
423
73
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
```js
|
|
427
|
-
import React from 'react';
|
|
428
|
-
import {
|
|
429
|
-
Button,
|
|
430
|
-
ButtonVariant,
|
|
431
|
-
DualListSelector,
|
|
432
|
-
DualListSelectorPane,
|
|
433
|
-
DualListSelectorList,
|
|
434
|
-
DualListSelectorListItem,
|
|
435
|
-
DualListSelectorControlsWrapper,
|
|
436
|
-
DualListSelectorControl,
|
|
437
|
-
SearchInput
|
|
438
|
-
} from '@patternfly/react-core';
|
|
439
|
-
import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon';
|
|
440
|
-
import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
|
|
441
|
-
import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon';
|
|
442
|
-
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
|
|
443
|
-
import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon';
|
|
444
|
-
|
|
445
|
-
const ComposableDualListSelector = () => {
|
|
446
|
-
const [availableOptions, setAvailableOptions] = React.useState([
|
|
447
|
-
{ text: 'Apple', selected: false, isVisible: true },
|
|
448
|
-
{ text: 'Banana', selected: false, isVisible: true },
|
|
449
|
-
{ text: 'Pineapple', selected: false, isVisible: true },
|
|
450
|
-
{ text: 'Orange', selected: false, isVisible: true },
|
|
451
|
-
{ text: 'Grape', selected: false, isVisible: true },
|
|
452
|
-
{ text: 'Peach', selected: false, isVisible: true },
|
|
453
|
-
{ text: 'Strawberry', selected: false, isVisible: true }
|
|
454
|
-
]);
|
|
455
|
-
const [chosenOptions, setChosenOptions] = React.useState([]);
|
|
456
|
-
const [availableFilter, setAvailableFilter] = React.useState('');
|
|
457
|
-
const [chosenFilter, setChosenFilter] = React.useState('');
|
|
458
|
-
|
|
459
|
-
// callback for moving selected options between lists
|
|
460
|
-
const moveSelected = (fromAvailable) => {
|
|
461
|
-
const sourceOptions = fromAvailable ? availableOptions : chosenOptions;
|
|
462
|
-
const destinationOptions = fromAvailable ? chosenOptions : availableOptions;
|
|
463
|
-
for (let i = 0; i < sourceOptions.length; i++) {
|
|
464
|
-
const option = sourceOptions[i];
|
|
465
|
-
if (option.selected && option.isVisible) {
|
|
466
|
-
sourceOptions.splice(i, 1);
|
|
467
|
-
destinationOptions.push(option);
|
|
468
|
-
option.selected = false;
|
|
469
|
-
i--;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (fromAvailable) {
|
|
473
|
-
setAvailableOptions([...sourceOptions]);
|
|
474
|
-
setChosenOptions([...destinationOptions]);
|
|
475
|
-
} else {
|
|
476
|
-
setChosenOptions([...sourceOptions]);
|
|
477
|
-
setAvailableOptions([...destinationOptions]);
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
// callback for moving all options between lists
|
|
482
|
-
const moveAll = (fromAvailable) => {
|
|
483
|
-
if (fromAvailable) {
|
|
484
|
-
setChosenOptions([...availableOptions.filter(x => x.isVisible), ...chosenOptions]);
|
|
485
|
-
setAvailableOptions([...availableOptions.filter(x => !x.isVisible)]);
|
|
486
|
-
} else {
|
|
487
|
-
setAvailableOptions([...chosenOptions.filter(x => x.isVisible), ...availableOptions]);
|
|
488
|
-
setChosenOptions([...chosenOptions.filter(x => !x.isVisible)]);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
// callback when option is selected
|
|
493
|
-
const onOptionSelect = (event, index, isChosen) => {
|
|
494
|
-
if (isChosen) {
|
|
495
|
-
const newChosen = [...chosenOptions];
|
|
496
|
-
newChosen[index].selected = !chosenOptions[index].selected;
|
|
497
|
-
setChosenOptions(newChosen);
|
|
498
|
-
} else {
|
|
499
|
-
const newAvailable = [...availableOptions];
|
|
500
|
-
newAvailable[index].selected = !availableOptions[index].selected;
|
|
501
|
-
setAvailableOptions(newAvailable);
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
// builds a search input - used in each dual list selector pane
|
|
506
|
-
const buildSearchInput = (isAvailable) => {
|
|
507
|
-
const onChange = (value) => {
|
|
508
|
-
isAvailable ? setAvailableFilter(value) : setChosenFilter(value);
|
|
509
|
-
const toFilter = isAvailable ? [...availableOptions] : [...chosenOptions];
|
|
510
|
-
toFilter.forEach((option) => {
|
|
511
|
-
option.isVisible = value === '' || option.text.toLowerCase().includes(value.toLowerCase());
|
|
512
|
-
})
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
return (
|
|
516
|
-
<SearchInput
|
|
517
|
-
value={isAvailable ? availableFilter : chosenFilter}
|
|
518
|
-
onChange={onChange}
|
|
519
|
-
onClear={() => onChange('')}
|
|
520
|
-
/>
|
|
521
|
-
);
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
// builds a sort control - passed to both dual list selector panes
|
|
525
|
-
const buildSort = (isAvailable) => {
|
|
526
|
-
const onSort = () => {
|
|
527
|
-
const toSort = isAvailable ? [...availableOptions] : [...chosenOptions];
|
|
528
|
-
toSort.sort((a,b) => {
|
|
529
|
-
if (a.text > b.text) return 1;
|
|
530
|
-
if (a.text < b.text) return -1;
|
|
531
|
-
return 0;
|
|
532
|
-
});
|
|
533
|
-
if (isAvailable) {
|
|
534
|
-
setAvailableOptions(toSort);
|
|
535
|
-
} else {
|
|
536
|
-
setChosenOptions(toSort);
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
return (
|
|
541
|
-
<Button
|
|
542
|
-
variant={ButtonVariant.plain}
|
|
543
|
-
onClick={onSort}
|
|
544
|
-
aria-label="Sort"
|
|
545
|
-
key="sortButton"
|
|
546
|
-
>
|
|
547
|
-
<PficonSortCommonAscIcon />
|
|
548
|
-
</Button>
|
|
549
|
-
);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
return (
|
|
553
|
-
<DualListSelector>
|
|
554
|
-
<DualListSelectorPane
|
|
555
|
-
title="Available"
|
|
556
|
-
status={`${availableOptions.filter(x => x.selected && x.isVisible).length} of ${availableOptions.filter(x => x.isVisible).length} options selected`}
|
|
557
|
-
searchInput={buildSearchInput(true)}
|
|
558
|
-
actions={[buildSort(true)]}
|
|
559
|
-
>
|
|
560
|
-
<DualListSelectorList>
|
|
561
|
-
{availableOptions.map((option, index) => {
|
|
562
|
-
return option.isVisible ? (
|
|
563
|
-
<DualListSelectorListItem
|
|
564
|
-
key={index}
|
|
565
|
-
isSelected={option.selected}
|
|
566
|
-
id={`composable-option-${index}`}
|
|
567
|
-
onOptionSelect={(e) => onOptionSelect(e, index, false)}
|
|
568
|
-
>
|
|
569
|
-
{option.text}
|
|
570
|
-
</DualListSelectorListItem>
|
|
571
|
-
) : null;
|
|
572
|
-
})}
|
|
573
|
-
</DualListSelectorList>
|
|
574
|
-
</DualListSelectorPane>
|
|
575
|
-
<DualListSelectorControlsWrapper
|
|
576
|
-
aria-label="Selector controls"
|
|
577
|
-
>
|
|
578
|
-
<DualListSelectorControl
|
|
579
|
-
isDisabled={!availableOptions.some(option => option.selected)}
|
|
580
|
-
onClick={() => moveSelected(true)}
|
|
581
|
-
aria-label="Add selected"
|
|
582
|
-
tooltipContent="Add selected"
|
|
583
|
-
>
|
|
584
|
-
<AngleRightIcon />
|
|
585
|
-
</DualListSelectorControl>
|
|
586
|
-
<DualListSelectorControl
|
|
587
|
-
isDisabled={availableOptions.length === 0}
|
|
588
|
-
onClick={() => moveAll(true)}
|
|
589
|
-
aria-label="Add all"
|
|
590
|
-
tooltipContent="Add all"
|
|
591
|
-
>
|
|
592
|
-
<AngleDoubleRightIcon />
|
|
593
|
-
</DualListSelectorControl>
|
|
594
|
-
<DualListSelectorControl
|
|
595
|
-
isDisabled={chosenOptions.length === 0}
|
|
596
|
-
onClick={() => moveAll(false)}
|
|
597
|
-
aria-label="Remove all"
|
|
598
|
-
tooltipContent="Remove all"
|
|
599
|
-
>
|
|
600
|
-
<AngleDoubleLeftIcon />
|
|
601
|
-
</DualListSelectorControl>
|
|
602
|
-
<DualListSelectorControl
|
|
603
|
-
onClick={() => moveSelected(false)}
|
|
604
|
-
isDisabled={!chosenOptions.some(option => option.selected)}
|
|
605
|
-
aria-label="Remove selected"
|
|
606
|
-
tooltipContent="Remove selected"
|
|
607
|
-
>
|
|
608
|
-
<AngleLeftIcon />
|
|
609
|
-
</DualListSelectorControl>
|
|
610
|
-
</DualListSelectorControlsWrapper>
|
|
611
|
-
<DualListSelectorPane
|
|
612
|
-
title="Chosen"
|
|
613
|
-
status={`${chosenOptions.filter(x => x.selected && x.isVisible).length} of ${chosenOptions.filter(x => x.isVisible).length} options selected`}
|
|
614
|
-
searchInput={buildSearchInput(false)}
|
|
615
|
-
actions={[buildSort(false)]}
|
|
616
|
-
isChosen
|
|
617
|
-
>
|
|
618
|
-
<DualListSelectorList>
|
|
619
|
-
{chosenOptions.map((option, index) => {
|
|
620
|
-
return option.isVisible ? (
|
|
621
|
-
<DualListSelectorListItem
|
|
622
|
-
key={index}
|
|
623
|
-
isSelected={option.selected}
|
|
624
|
-
id={`composable-option-${index}`}
|
|
625
|
-
onOptionSelect={(e) => onOptionSelect(e, index, true)}
|
|
626
|
-
>
|
|
627
|
-
{option.text}
|
|
628
|
-
</DualListSelectorListItem>
|
|
629
|
-
) : null;
|
|
630
|
-
})}
|
|
631
|
-
</DualListSelectorList>
|
|
632
|
-
</DualListSelectorPane>
|
|
633
|
-
</DualListSelector>
|
|
634
|
-
);
|
|
635
|
-
}
|
|
74
|
+
```ts file="./DualListSelectorComposable.tsx"
|
|
636
75
|
```
|
|
637
76
|
|
|
638
|
-
###
|
|
77
|
+
### Composable dual list selector with drag and drop
|
|
78
|
+
|
|
79
|
+
This example only allows reordering the contents of the "chosen" pane with drag and drop. To make a pane able to be reordered:
|
|
639
80
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
true to enable the 'drop' animation in the new location and false to enable the 'drop' animation back to the item's
|
|
81
|
+
- wrap the `DualListSelectorPane` in a `DragDrop` component
|
|
82
|
+
- wrap the `DualListSelectorList` in a `Droppable` component
|
|
83
|
+
- wrap the `DualListSelectorListItem` components in a `Draggable` component
|
|
84
|
+
- define an `onDrop` callback which reorders the sortable options.
|
|
85
|
+
- The `onDrop` function provides the starting location and destination location for a dragged item. It should return
|
|
86
|
+
true to enable the 'drop' animation in the new location and false to enable the 'drop' animation back to the item's
|
|
647
87
|
old position.
|
|
648
|
-
|
|
88
|
+
- define an `onDrag` callback which ensures that the drag event will not cross hairs with the `onOptionSelect` click
|
|
649
89
|
event set on the option. Note: the `ignoreNextOptionSelect` state value is used to prevent selection while dragging.
|
|
650
90
|
|
|
651
|
-
|
|
652
|
-
import React from 'react';
|
|
653
|
-
import {
|
|
654
|
-
DragDrop,
|
|
655
|
-
Droppable,
|
|
656
|
-
Draggable,
|
|
657
|
-
DualListSelector,
|
|
658
|
-
DualListSelectorPane,
|
|
659
|
-
DualListSelectorList,
|
|
660
|
-
DualListSelectorListItem,
|
|
661
|
-
DualListSelectorControlsWrapper,
|
|
662
|
-
DualListSelectorControl,
|
|
663
|
-
} from '@patternfly/react-core';
|
|
664
|
-
import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon';
|
|
665
|
-
import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
|
|
666
|
-
import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon';
|
|
667
|
-
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
|
|
668
|
-
|
|
669
|
-
const ComposableDualListSelector = () => {
|
|
670
|
-
const [ignoreNextOptionSelect, setIgnoreNextOptionSelect] = React.useState(false);
|
|
671
|
-
const [availableOptions, setAvailableOptions] = React.useState([
|
|
672
|
-
{ text: 'Apple', selected: false, isVisible: true },
|
|
673
|
-
{ text: 'Banana', selected: false, isVisible: true },
|
|
674
|
-
{ text: 'Pineapple', selected: false, isVisible: true }
|
|
675
|
-
]);
|
|
676
|
-
const [chosenOptions, setChosenOptions] = React.useState([
|
|
677
|
-
{ text: 'Orange', selected: false, isVisible: true },
|
|
678
|
-
{ text: 'Grape', selected: false, isVisible: true },
|
|
679
|
-
{ text: 'Peach', selected: false, isVisible: true },
|
|
680
|
-
{ text: 'Strawberry', selected: false, isVisible: true }
|
|
681
|
-
]);
|
|
682
|
-
|
|
683
|
-
const moveSelected = (fromAvailable) => {
|
|
684
|
-
const sourceOptions = fromAvailable ? availableOptions : chosenOptions;
|
|
685
|
-
const destinationOptions = fromAvailable ? chosenOptions : availableOptions;
|
|
686
|
-
for (let i = 0; i < sourceOptions.length; i++) {
|
|
687
|
-
const option = sourceOptions[i];
|
|
688
|
-
if (option.selected && option.isVisible) {
|
|
689
|
-
sourceOptions.splice(i, 1);
|
|
690
|
-
destinationOptions.push(option);
|
|
691
|
-
option.selected = false;
|
|
692
|
-
i--;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (fromAvailable) {
|
|
696
|
-
setAvailableOptions([...sourceOptions]);
|
|
697
|
-
setChosenOptions([...destinationOptions]);
|
|
698
|
-
} else {
|
|
699
|
-
setChosenOptions([...sourceOptions]);
|
|
700
|
-
setAvailableOptions([...destinationOptions]);
|
|
701
|
-
}
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
const moveAll = (fromAvailable) => {
|
|
705
|
-
if (fromAvailable) {
|
|
706
|
-
setChosenOptions([...availableOptions.filter(x => x.isVisible), ...chosenOptions]);
|
|
707
|
-
setAvailableOptions([...availableOptions.filter(x => !x.isVisible)]);
|
|
708
|
-
} else {
|
|
709
|
-
setAvailableOptions([...chosenOptions.filter(x => x.isVisible), ...availableOptions]);
|
|
710
|
-
setChosenOptions([...chosenOptions.filter(x => !x.isVisible)]);
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
const onOptionSelect = (event, index, isChosen) => {
|
|
715
|
-
if (ignoreNextOptionSelect) {
|
|
716
|
-
setIgnoreNextOptionSelect(false);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (isChosen) {
|
|
720
|
-
const newChosen = [...chosenOptions];
|
|
721
|
-
newChosen[index].selected = !chosenOptions[index].selected;
|
|
722
|
-
setChosenOptions(newChosen);
|
|
723
|
-
} else {
|
|
724
|
-
const newAvailable = [...availableOptions];
|
|
725
|
-
newAvailable[index].selected = !availableOptions[index].selected;
|
|
726
|
-
setAvailableOptions(newAvailable);
|
|
727
|
-
}
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
const onDrop = (source, dest) => {
|
|
731
|
-
if (dest){
|
|
732
|
-
const newList = [...chosenOptions];
|
|
733
|
-
const [removed] = newList.splice(source.index, 1);
|
|
734
|
-
newList.splice(dest.index, 0, removed);
|
|
735
|
-
setChosenOptions(newList);
|
|
736
|
-
return true;
|
|
737
|
-
}
|
|
738
|
-
return false;
|
|
739
|
-
};
|
|
91
|
+
Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development.
|
|
740
92
|
|
|
741
|
-
|
|
742
|
-
<DualListSelector>
|
|
743
|
-
<DualListSelectorPane
|
|
744
|
-
title="Available"
|
|
745
|
-
status={`${availableOptions.filter(x => x.selected && x.isVisible).length} of ${availableOptions.filter(x => x.isVisible).length} options selected`}
|
|
746
|
-
>
|
|
747
|
-
<DualListSelectorList>
|
|
748
|
-
{availableOptions.map((option, index) => {
|
|
749
|
-
return option.isVisible ? (
|
|
750
|
-
<DualListSelectorListItem
|
|
751
|
-
key={index}
|
|
752
|
-
isSelected={option.selected}
|
|
753
|
-
id={`composable-available-option-${index}`}
|
|
754
|
-
onOptionSelect={(e) => onOptionSelect(e, index, false)}
|
|
755
|
-
>
|
|
756
|
-
{option.text}
|
|
757
|
-
</DualListSelectorListItem>
|
|
758
|
-
) : null;
|
|
759
|
-
})}
|
|
760
|
-
</DualListSelectorList>
|
|
761
|
-
</DualListSelectorPane>
|
|
762
|
-
<DualListSelectorControlsWrapper
|
|
763
|
-
aria-label="Selector controls"
|
|
764
|
-
>
|
|
765
|
-
<DualListSelectorControl
|
|
766
|
-
isDisabled={!availableOptions.some(option => option.selected)}
|
|
767
|
-
onClick={() => moveSelected(true)}
|
|
768
|
-
aria-label="Add selected"
|
|
769
|
-
>
|
|
770
|
-
<AngleRightIcon />
|
|
771
|
-
</DualListSelectorControl>
|
|
772
|
-
<DualListSelectorControl
|
|
773
|
-
isDisabled={availableOptions.length === 0}
|
|
774
|
-
onClick={() => moveAll(true)}
|
|
775
|
-
aria-label="Add all"
|
|
776
|
-
>
|
|
777
|
-
<AngleDoubleRightIcon />
|
|
778
|
-
</DualListSelectorControl>
|
|
779
|
-
<DualListSelectorControl
|
|
780
|
-
isDisabled={chosenOptions.length === 0}
|
|
781
|
-
onClick={() => moveAll(false)}
|
|
782
|
-
aria-label="Remove all"
|
|
783
|
-
>
|
|
784
|
-
<AngleDoubleLeftIcon />
|
|
785
|
-
</DualListSelectorControl>
|
|
786
|
-
<DualListSelectorControl
|
|
787
|
-
onClick={() => moveSelected(false)}
|
|
788
|
-
isDisabled={!chosenOptions.some(option => option.selected)}
|
|
789
|
-
aria-label="Remove selected"
|
|
790
|
-
>
|
|
791
|
-
<AngleLeftIcon />
|
|
792
|
-
</DualListSelectorControl>
|
|
793
|
-
</DualListSelectorControlsWrapper>
|
|
794
|
-
<DragDrop onDrag={() => { setIgnoreNextOptionSelect(true); return true; }} onDrop={onDrop}>
|
|
795
|
-
<DualListSelectorPane
|
|
796
|
-
title="Chosen"
|
|
797
|
-
status={`${chosenOptions.filter(x => x.selected && x.isVisible).length} of ${chosenOptions.filter(x => x.isVisible).length} options selected`}
|
|
798
|
-
isChosen
|
|
799
|
-
>
|
|
800
|
-
<Droppable hasNoWrapper>
|
|
801
|
-
<DualListSelectorList>
|
|
802
|
-
{chosenOptions.map((option, index) => {
|
|
803
|
-
return option.isVisible ? (
|
|
804
|
-
<Draggable key={index} hasNoWrapper>
|
|
805
|
-
<DualListSelectorListItem
|
|
806
|
-
isSelected={option.selected}
|
|
807
|
-
id={`composable-chosen-option-${index}`}
|
|
808
|
-
onOptionSelect={(e) => onOptionSelect(e, index, true)}
|
|
809
|
-
isDraggable
|
|
810
|
-
>
|
|
811
|
-
{option.text}
|
|
812
|
-
</DualListSelectorListItem>
|
|
813
|
-
</Draggable>
|
|
814
|
-
) : null;
|
|
815
|
-
})}
|
|
816
|
-
</DualListSelectorList>
|
|
817
|
-
</Droppable>
|
|
818
|
-
</DualListSelectorPane>
|
|
819
|
-
</DragDrop>
|
|
820
|
-
</DualListSelector>
|
|
821
|
-
);
|
|
822
|
-
}
|
|
93
|
+
```ts file="DualListSelectorComposableDragDrop.tsx"
|
|
823
94
|
```
|
|
824
95
|
|
|
825
|
-
### Composable dual list selector tree
|
|
826
|
-
```ts file="ComposableTree.tsx"
|
|
827
|
-
```
|
|
96
|
+
### Composable dual list selector with tree
|
|
828
97
|
|
|
98
|
+
```ts file="DualListSelectorComposableTree.tsx"
|
|
99
|
+
```
|