@dhis2-ui/transfer 10.16.2 → 10.16.3-alpha.1
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/package.json +10 -9
- package/src/__e2e__/add_remove-highlighted-options.e2e.stories.js +30 -0
- package/src/__e2e__/common/options.js +90 -0
- package/src/__e2e__/common/stateful-decorator.js +33 -0
- package/src/__e2e__/common.js +0 -0
- package/src/__e2e__/disabled-transfer-buttons.e2e.stories.js +49 -0
- package/src/__e2e__/disabled-transfer-options.e2e.stories.js +21 -0
- package/src/__e2e__/display-order.e2e.stories.js +24 -0
- package/src/__e2e__/filter-options-list.e2e.stories.js +87 -0
- package/src/__e2e__/highlight-range-of-options.e2e.stories.js +52 -0
- package/src/__e2e__/loading_lists.e2e.stories.js +26 -0
- package/src/__e2e__/notify_at_end_of_list.e2e.stories.js +116 -0
- package/src/__e2e__/reorder-with-buttons.e2e.stories.js +35 -0
- package/src/__e2e__/set_unset-highlighted-option.e2e.stories.js +30 -0
- package/src/__e2e__/transferring-items.e2e.stories.js +27 -0
- package/src/__tests__/common.test.js +131 -0
- package/src/__tests__/helper/add-all-selectable-source-options.test.js +46 -0
- package/src/__tests__/helper/add-individual-source-options.test.js +80 -0
- package/src/__tests__/helper/default-filter-callback.test.js +45 -0
- package/src/__tests__/helper/is-reorder-down-disabled.test.js +96 -0
- package/src/__tests__/helper/is-reorder-up-disabled.test.js +96 -0
- package/src/__tests__/helper/move-highlighted-picked-option-down.test.js +111 -0
- package/src/__tests__/helper/move-highlighted-picked-option-to-bottom.test.js +101 -0
- package/src/__tests__/helper/move-highlighted-picked-option-to-top.test.js +101 -0
- package/src/__tests__/helper/move-highlighted-picked-option-up.test.js +111 -0
- package/src/__tests__/helper/remove-all-picked-options.test.js +29 -0
- package/src/__tests__/helper/remove-individual-picked-options.test.js +38 -0
- package/src/__tests__/helper/use-highlighted-option/create-toggle-highlighted-option.test.js +104 -0
- package/src/__tests__/helper/use-highlighted-option/toggle-add.test.js +84 -0
- package/src/__tests__/helper/use-highlighted-option/toggle-range.test.js +150 -0
- package/src/__tests__/helper/use-highlighted-option/toggle-replace.test.js +39 -0
- package/src/__tests__/helper/use-highlighted-option.test.js +41 -0
- package/src/__tests__/reordering-actions.test.js +165 -0
- package/src/__tests__/transfer.test.js +137 -0
- package/src/actions.js +33 -0
- package/src/add-all.js +27 -0
- package/src/add-individual.js +27 -0
- package/src/common/find-option-index.js +9 -0
- package/src/common/get-mode-by-modifier-key.js +35 -0
- package/src/common/index.js +5 -0
- package/src/common/is-option.js +7 -0
- package/src/common/modes.js +11 -0
- package/src/common/remove-option.js +19 -0
- package/src/common/toggle-value.js +18 -0
- package/src/container.js +23 -0
- package/src/end-intersection-detector.js +37 -0
- package/src/features/add_remove-highlighted-options/index.js +92 -0
- package/src/features/add_remove-highlighted-options.feature +41 -0
- package/src/features/common/index.js +8 -0
- package/src/features/disabled-transfer-buttons/index.js +118 -0
- package/src/features/disabled-transfer-buttons.feature +46 -0
- package/src/features/disabled-transfer-options/index.js +182 -0
- package/src/features/disabled-transfer-options.feature +42 -0
- package/src/features/display-order/index.js +205 -0
- package/src/features/display-order.feature +30 -0
- package/src/features/filter-options-list/index.js +133 -0
- package/src/features/filter-options-list.feature +40 -0
- package/src/features/highlight-range-of-options/index.js +336 -0
- package/src/features/highlight-range-of-options.feature +70 -0
- package/src/features/loading_lists/index.js +43 -0
- package/src/features/loading_lists.feature +19 -0
- package/src/features/notify_at_end_of_list/index.js +125 -0
- package/src/features/notify_at_end_of_list.feature +64 -0
- package/src/features/reorder-with-buttons/index.js +181 -0
- package/src/features/reorder-with-buttons.feature +138 -0
- package/src/features/set_unset-highlighted-option/index.js +121 -0
- package/src/features/set_unset-highlighted-option.feature +42 -0
- package/src/features/transferring-items/index.js +375 -0
- package/src/features/transferring-items.feature +44 -0
- package/src/filter.js +38 -0
- package/src/icons.js +194 -0
- package/src/index.js +2 -0
- package/src/left-footer.js +22 -0
- package/src/left-header.js +22 -0
- package/src/left-side.js +34 -0
- package/src/locales/en/translations.json +7 -0
- package/src/locales/index.js +16 -0
- package/src/options-container.js +127 -0
- package/src/remove-all.js +27 -0
- package/src/remove-individual.js +27 -0
- package/src/reordering-actions.js +136 -0
- package/src/right-footer.js +22 -0
- package/src/right-header.js +22 -0
- package/src/right-side.js +33 -0
- package/src/transfer/add-all-selectable-source-options.js +37 -0
- package/src/transfer/add-individual-source-options.js +61 -0
- package/src/transfer/create-double-click-handlers.js +36 -0
- package/src/transfer/default-filter-callback.js +17 -0
- package/src/transfer/get-highlighted-picked-indices.js +26 -0
- package/src/transfer/get-option-click-handlers.js +19 -0
- package/src/transfer/index.js +17 -0
- package/src/transfer/is-reorder-down-disabled.js +34 -0
- package/src/transfer/is-reorder-up-disabled.js +30 -0
- package/src/transfer/move-highlighted-picked-option-down.js +54 -0
- package/src/transfer/move-highlighted-picked-option-to-bottom.js +44 -0
- package/src/transfer/move-highlighted-picked-option-to-top.js +38 -0
- package/src/transfer/move-highlighted-picked-option-up.js +47 -0
- package/src/transfer/remove-all-picked-options.js +13 -0
- package/src/transfer/remove-individual-picked-options.js +49 -0
- package/src/transfer/use-filter.js +17 -0
- package/src/transfer/use-highlighted-options/create-toggle-highlighted-option.js +64 -0
- package/src/transfer/use-highlighted-options/toggle-add.js +20 -0
- package/src/transfer/use-highlighted-options/toggle-range.js +61 -0
- package/src/transfer/use-highlighted-options/toggle-replace.js +26 -0
- package/src/transfer/use-highlighted-options.js +34 -0
- package/src/transfer/use-options-key-monitor.js +41 -0
- package/src/transfer-option.js +91 -0
- package/src/transfer.js +539 -0
- package/src/transfer.prod.stories.js +621 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { colors, spacers } from '@dhis2/ui-constants'
|
|
2
|
+
import cx from 'classnames'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import React, { useRef } from 'react'
|
|
5
|
+
|
|
6
|
+
const DOUBLE_CLICK_MAX_DELAY = 500
|
|
7
|
+
|
|
8
|
+
export const TransferOption = ({
|
|
9
|
+
className,
|
|
10
|
+
disabled,
|
|
11
|
+
dataTest = 'dhis2-uicore-transferoption',
|
|
12
|
+
highlighted,
|
|
13
|
+
onClick,
|
|
14
|
+
onDoubleClick,
|
|
15
|
+
label,
|
|
16
|
+
value,
|
|
17
|
+
}) => {
|
|
18
|
+
const doubleClickTimeout = useRef(null)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
data-test={dataTest}
|
|
23
|
+
onClick={(event) => {
|
|
24
|
+
if (disabled) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (doubleClickTimeout.current) {
|
|
29
|
+
clearTimeout(doubleClickTimeout.current)
|
|
30
|
+
doubleClickTimeout.current = null
|
|
31
|
+
|
|
32
|
+
onDoubleClick({ value }, event)
|
|
33
|
+
} else {
|
|
34
|
+
doubleClickTimeout.current = setTimeout(() => {
|
|
35
|
+
clearTimeout(doubleClickTimeout.current)
|
|
36
|
+
doubleClickTimeout.current = null
|
|
37
|
+
}, DOUBLE_CLICK_MAX_DELAY)
|
|
38
|
+
|
|
39
|
+
onClick({ value }, event)
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
data-value={value}
|
|
43
|
+
className={cx(className, { highlighted, disabled })}
|
|
44
|
+
>
|
|
45
|
+
{label}
|
|
46
|
+
|
|
47
|
+
<style jsx>{`
|
|
48
|
+
div {
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
line-height: 16px;
|
|
51
|
+
padding: 4px 8px;
|
|
52
|
+
color: ${colors.grey900};
|
|
53
|
+
user-select: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
div:hover {
|
|
57
|
+
background: ${colors.grey200};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
div.highlighted {
|
|
61
|
+
background: ${colors.teal700};
|
|
62
|
+
color: ${colors.white};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
div.disabled {
|
|
66
|
+
color: ${colors.grey600};
|
|
67
|
+
cursor: not-allowed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
div:first-child {
|
|
71
|
+
margin-block-start: ${spacers.dp4};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
div:last-child {
|
|
75
|
+
margin-block-end: ${spacers.dp4};
|
|
76
|
+
}
|
|
77
|
+
`}</style>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TransferOption.propTypes = {
|
|
83
|
+
label: PropTypes.node.isRequired,
|
|
84
|
+
value: PropTypes.string.isRequired,
|
|
85
|
+
className: PropTypes.string,
|
|
86
|
+
dataTest: PropTypes.string,
|
|
87
|
+
disabled: PropTypes.bool,
|
|
88
|
+
highlighted: PropTypes.bool,
|
|
89
|
+
onClick: PropTypes.func,
|
|
90
|
+
onDoubleClick: PropTypes.func,
|
|
91
|
+
}
|
package/src/transfer.js
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { useEffect, useMemo, useRef } from 'react'
|
|
3
|
+
import { Actions } from './actions.js'
|
|
4
|
+
import { AddAll } from './add-all.js'
|
|
5
|
+
import { AddIndividual } from './add-individual.js'
|
|
6
|
+
import { Container } from './container.js'
|
|
7
|
+
import { Filter } from './filter.js'
|
|
8
|
+
import { LeftFooter } from './left-footer.js'
|
|
9
|
+
import { LeftHeader } from './left-header.js'
|
|
10
|
+
import { LeftSide } from './left-side.js'
|
|
11
|
+
import { OptionsContainer } from './options-container.js'
|
|
12
|
+
import { RemoveAll } from './remove-all.js'
|
|
13
|
+
import { RemoveIndividual } from './remove-individual.js'
|
|
14
|
+
import { ReorderingActions } from './reordering-actions.js'
|
|
15
|
+
import { RightFooter } from './right-footer.js'
|
|
16
|
+
import { RightHeader } from './right-header.js'
|
|
17
|
+
import { RightSide } from './right-side.js'
|
|
18
|
+
import {
|
|
19
|
+
addAllSelectableSourceOptions,
|
|
20
|
+
addIndividualSourceOptions,
|
|
21
|
+
createDoubleClickHandlers,
|
|
22
|
+
defaultFilterCallback,
|
|
23
|
+
getOptionClickHandlers,
|
|
24
|
+
isReorderDownDisabled,
|
|
25
|
+
isReorderUpDisabled,
|
|
26
|
+
moveHighlightedPickedOptionDown,
|
|
27
|
+
moveHighlightedPickedOptionToBottom,
|
|
28
|
+
moveHighlightedPickedOptionToTop,
|
|
29
|
+
moveHighlightedPickedOptionUp,
|
|
30
|
+
removeAllPickedOptions,
|
|
31
|
+
removeIndividualPickedOptions,
|
|
32
|
+
useFilter,
|
|
33
|
+
useHighlightedOptions,
|
|
34
|
+
} from './transfer/index.js'
|
|
35
|
+
import { TransferOption } from './transfer-option.js'
|
|
36
|
+
|
|
37
|
+
const identity = (value) => value
|
|
38
|
+
const defaultSelected = []
|
|
39
|
+
const defaultSelectedOptionsLookup = {}
|
|
40
|
+
|
|
41
|
+
export const Transfer = ({
|
|
42
|
+
options,
|
|
43
|
+
onChange,
|
|
44
|
+
|
|
45
|
+
addAllText,
|
|
46
|
+
addIndividualText,
|
|
47
|
+
className,
|
|
48
|
+
dataTest = 'dhis2-uicore-transfer',
|
|
49
|
+
disabled,
|
|
50
|
+
enableOrderChange,
|
|
51
|
+
filterCallback = defaultFilterCallback,
|
|
52
|
+
filterCallbackPicked = defaultFilterCallback,
|
|
53
|
+
filterLabel,
|
|
54
|
+
filterLabelPicked,
|
|
55
|
+
filterPlaceholder,
|
|
56
|
+
filterPlaceholderPicked,
|
|
57
|
+
filterable,
|
|
58
|
+
filterablePicked,
|
|
59
|
+
height = '240px',
|
|
60
|
+
hideFilterInput,
|
|
61
|
+
hideFilterInputPicked,
|
|
62
|
+
initialSearchTerm = '',
|
|
63
|
+
initialSearchTermPicked = '',
|
|
64
|
+
selectedOptionsLookup = defaultSelectedOptionsLookup,
|
|
65
|
+
leftFooter,
|
|
66
|
+
leftHeader,
|
|
67
|
+
loadingPicked,
|
|
68
|
+
loading,
|
|
69
|
+
maxSelections = Infinity,
|
|
70
|
+
optionsWidth = '320px',
|
|
71
|
+
removeAllText,
|
|
72
|
+
removeIndividualText,
|
|
73
|
+
renderOption = defaultRenderOption,
|
|
74
|
+
rightFooter,
|
|
75
|
+
rightHeader,
|
|
76
|
+
searchTerm,
|
|
77
|
+
searchTermPicked,
|
|
78
|
+
selected = defaultSelected,
|
|
79
|
+
selectedEmptyComponent,
|
|
80
|
+
selectedWidth = '320px',
|
|
81
|
+
sourceEmptyPlaceholder,
|
|
82
|
+
onFilterChange,
|
|
83
|
+
onFilterChangePicked,
|
|
84
|
+
onEndReached,
|
|
85
|
+
onEndReachedPicked,
|
|
86
|
+
}) => {
|
|
87
|
+
/* Source options search value:
|
|
88
|
+
* Depending on whether the onFilterChange callback has been provided
|
|
89
|
+
* either the internal or external search value is used */
|
|
90
|
+
const {
|
|
91
|
+
filterValue: actualFilter,
|
|
92
|
+
filter: actualFilterCallback,
|
|
93
|
+
setInternalFilter,
|
|
94
|
+
} = useFilter({
|
|
95
|
+
initialSearchTerm,
|
|
96
|
+
onFilterChange,
|
|
97
|
+
externalSearchTerm: searchTerm,
|
|
98
|
+
filterable,
|
|
99
|
+
filterCallback,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Actual source options:
|
|
104
|
+
* Extract the not-selected options.
|
|
105
|
+
* Filters options if filterable is true.
|
|
106
|
+
*/
|
|
107
|
+
const sourceOptions = actualFilterCallback(
|
|
108
|
+
options.filter(({ value }) => !selected.includes(value)),
|
|
109
|
+
actualFilter
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Picked options highlighting:
|
|
114
|
+
* These are all the highlighted options on the options side.
|
|
115
|
+
*/
|
|
116
|
+
const {
|
|
117
|
+
highlightedOptions: highlightedSourceOptions,
|
|
118
|
+
setHighlightedOptions: setHighlightedSourceOptions,
|
|
119
|
+
toggleHighlightedOption: toggleHighlightedSourceOption,
|
|
120
|
+
} = useHighlightedOptions({
|
|
121
|
+
options: sourceOptions,
|
|
122
|
+
disabled,
|
|
123
|
+
maxSelections,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/* Picked options search value:
|
|
127
|
+
* Depending on whether the onFilterChangePicked callback has been provided
|
|
128
|
+
* either the internal or external search value is used */
|
|
129
|
+
const {
|
|
130
|
+
filterValue: actualFilterPicked,
|
|
131
|
+
filter: actualFilterPickedCallback,
|
|
132
|
+
setInternalFilter: setInternalFilterPicked,
|
|
133
|
+
} = useFilter({
|
|
134
|
+
filterable: filterablePicked,
|
|
135
|
+
initialSearchTerm: initialSearchTermPicked,
|
|
136
|
+
onFilterChange: onFilterChangePicked,
|
|
137
|
+
externalSearchTerm: searchTermPicked,
|
|
138
|
+
filterCallback: filterCallbackPicked,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const filterActivePicked = Boolean(actualFilterPicked)
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* Actual picked options:
|
|
145
|
+
* Extract the selected options. Can't use `options.filter`
|
|
146
|
+
* because we need to keep the order of `selected`
|
|
147
|
+
* Note: Only map if selected is an array
|
|
148
|
+
*/
|
|
149
|
+
const pickedOptions = useMemo(
|
|
150
|
+
() =>
|
|
151
|
+
Array.isArray(selected)
|
|
152
|
+
? actualFilterPickedCallback(
|
|
153
|
+
selected
|
|
154
|
+
.map(
|
|
155
|
+
(value) =>
|
|
156
|
+
selectedOptionsLookup[value] ??
|
|
157
|
+
options.find(
|
|
158
|
+
(option) => value === option.value
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
// filter -> in case a selected value has been provided
|
|
162
|
+
// that does not exist as option
|
|
163
|
+
.filter(identity),
|
|
164
|
+
actualFilterPicked
|
|
165
|
+
)
|
|
166
|
+
: [],
|
|
167
|
+
[
|
|
168
|
+
selected,
|
|
169
|
+
options,
|
|
170
|
+
actualFilterPicked,
|
|
171
|
+
actualFilterPickedCallback,
|
|
172
|
+
selectedOptionsLookup,
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Source options highlighting:
|
|
178
|
+
* These are all the highlighted options on the selected side.
|
|
179
|
+
*/
|
|
180
|
+
const {
|
|
181
|
+
highlightedOptions: highlightedPickedOptions,
|
|
182
|
+
setHighlightedOptions: setHighlightedPickedOptions,
|
|
183
|
+
toggleHighlightedOption: toggleHighlightedPickedOption,
|
|
184
|
+
} = useHighlightedOptions({
|
|
185
|
+
options: pickedOptions,
|
|
186
|
+
disabled,
|
|
187
|
+
maxSelections,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Source & Picked options:
|
|
192
|
+
* These are the double click handlers for (de-)selection
|
|
193
|
+
*/
|
|
194
|
+
const { selectSingleOption, deselectSingleOption } =
|
|
195
|
+
createDoubleClickHandlers({
|
|
196
|
+
selected,
|
|
197
|
+
setHighlightedSourceOptions,
|
|
198
|
+
setHighlightedPickedOptions,
|
|
199
|
+
onChange,
|
|
200
|
+
maxSelections,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
/*
|
|
204
|
+
* Reorder scroll-into-view:
|
|
205
|
+
* After a reorder move, scroll the moved block so the leading edge
|
|
206
|
+
* (top on Up, bottom on Down) is visible inside the picked-side
|
|
207
|
+
* scroll container. `block: 'nearest'` means no scroll when the
|
|
208
|
+
* element is already fully in view.
|
|
209
|
+
*/
|
|
210
|
+
const reorderScrollTargetRef = useRef(null)
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const target = reorderScrollTargetRef.current
|
|
213
|
+
if (target == null) {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
reorderScrollTargetRef.current = null
|
|
217
|
+
const element = document.querySelector(
|
|
218
|
+
`[data-test="${dataTest}-pickedoptions"] [data-value="${CSS.escape(
|
|
219
|
+
target
|
|
220
|
+
)}"]`
|
|
221
|
+
)
|
|
222
|
+
element?.scrollIntoView({ block: 'nearest' })
|
|
223
|
+
}, [selected, dataTest])
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Disabled button states
|
|
227
|
+
*/
|
|
228
|
+
const isAddAllDisabled =
|
|
229
|
+
disabled ||
|
|
230
|
+
sourceOptions.filter(({ disabled }) => !disabled).length === 0
|
|
231
|
+
const isAddIndividualDisabled = disabled || !highlightedSourceOptions.length
|
|
232
|
+
const isRemoveAllDisabled = disabled || !selected.length
|
|
233
|
+
const isRemoveIndividualDisabled =
|
|
234
|
+
disabled || !highlightedPickedOptions.length
|
|
235
|
+
|
|
236
|
+
const allOptionsKey = useMemo(
|
|
237
|
+
() => options.map(({ value }) => value).join('|'),
|
|
238
|
+
[options]
|
|
239
|
+
)
|
|
240
|
+
const pickedOptionsKey = useMemo(
|
|
241
|
+
() => pickedOptions.map(({ value }) => value).join('|'),
|
|
242
|
+
[pickedOptions]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<Container dataTest={dataTest} className={className} height={height}>
|
|
247
|
+
<LeftSide dataTest={`${dataTest}-leftside`} width={optionsWidth}>
|
|
248
|
+
{(leftHeader || filterable) && (
|
|
249
|
+
<LeftHeader dataTest={`${dataTest}-leftheader`}>
|
|
250
|
+
{leftHeader}
|
|
251
|
+
|
|
252
|
+
{filterable && !hideFilterInput && (
|
|
253
|
+
<Filter
|
|
254
|
+
label={filterLabel}
|
|
255
|
+
placeholder={filterPlaceholder}
|
|
256
|
+
dataTest={`${dataTest}-filter`}
|
|
257
|
+
filter={actualFilter}
|
|
258
|
+
onChange={
|
|
259
|
+
onFilterChange
|
|
260
|
+
? onFilterChange
|
|
261
|
+
: ({ value }) =>
|
|
262
|
+
setInternalFilter(value)
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
266
|
+
</LeftHeader>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
<OptionsContainer
|
|
270
|
+
allOptionsKey={allOptionsKey}
|
|
271
|
+
dataTest={`${dataTest}-sourceoptions`}
|
|
272
|
+
emptyComponent={sourceEmptyPlaceholder}
|
|
273
|
+
getOptionClickHandlers={getOptionClickHandlers}
|
|
274
|
+
highlightedOptions={highlightedSourceOptions}
|
|
275
|
+
loading={loading}
|
|
276
|
+
options={sourceOptions}
|
|
277
|
+
renderOption={renderOption}
|
|
278
|
+
selectionHandler={selectSingleOption}
|
|
279
|
+
toggleHighlightedOption={toggleHighlightedSourceOption}
|
|
280
|
+
onEndReached={onEndReached}
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
{leftFooter && (
|
|
284
|
+
<LeftFooter dataTest={`${dataTest}-leftfooter`}>
|
|
285
|
+
{leftFooter}
|
|
286
|
+
</LeftFooter>
|
|
287
|
+
)}
|
|
288
|
+
</LeftSide>
|
|
289
|
+
|
|
290
|
+
<Actions dataTest={`${dataTest}-actions`}>
|
|
291
|
+
{maxSelections === Infinity && (
|
|
292
|
+
<AddAll
|
|
293
|
+
label={addAllText}
|
|
294
|
+
dataTest={`${dataTest}-actions-addall`}
|
|
295
|
+
disabled={isAddAllDisabled}
|
|
296
|
+
onClick={() =>
|
|
297
|
+
addAllSelectableSourceOptions({
|
|
298
|
+
sourceOptions,
|
|
299
|
+
selected,
|
|
300
|
+
onChange,
|
|
301
|
+
setHighlightedSourceOptions,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
/>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
<AddIndividual
|
|
308
|
+
label={addIndividualText}
|
|
309
|
+
dataTest={`${dataTest}-actions-addindividual`}
|
|
310
|
+
disabled={isAddIndividualDisabled}
|
|
311
|
+
onClick={() =>
|
|
312
|
+
addIndividualSourceOptions({
|
|
313
|
+
filterable,
|
|
314
|
+
sourceOptions,
|
|
315
|
+
highlightedSourceOptions,
|
|
316
|
+
selected,
|
|
317
|
+
maxSelections,
|
|
318
|
+
onChange,
|
|
319
|
+
setHighlightedSourceOptions,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
/>
|
|
323
|
+
|
|
324
|
+
{maxSelections === Infinity && (
|
|
325
|
+
<RemoveAll
|
|
326
|
+
label={removeAllText}
|
|
327
|
+
dataTest={`${dataTest}-actions-removeall`}
|
|
328
|
+
disabled={isRemoveAllDisabled}
|
|
329
|
+
onClick={() =>
|
|
330
|
+
removeAllPickedOptions({
|
|
331
|
+
setHighlightedPickedOptions,
|
|
332
|
+
onChange,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
<RemoveIndividual
|
|
339
|
+
label={removeIndividualText}
|
|
340
|
+
dataTest={`${dataTest}-actions-removeindividual`}
|
|
341
|
+
disabled={isRemoveIndividualDisabled}
|
|
342
|
+
onClick={() =>
|
|
343
|
+
removeIndividualPickedOptions({
|
|
344
|
+
filterablePicked,
|
|
345
|
+
pickedOptions,
|
|
346
|
+
highlightedPickedOptions,
|
|
347
|
+
onChange,
|
|
348
|
+
selected,
|
|
349
|
+
setHighlightedPickedOptions,
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
/>
|
|
353
|
+
</Actions>
|
|
354
|
+
|
|
355
|
+
<RightSide dataTest={`${dataTest}-rightside`} width={selectedWidth}>
|
|
356
|
+
{(rightHeader || filterablePicked) && (
|
|
357
|
+
<RightHeader dataTest={`${dataTest}-rightheader`}>
|
|
358
|
+
{rightHeader}
|
|
359
|
+
|
|
360
|
+
{filterablePicked && !hideFilterInputPicked && (
|
|
361
|
+
<Filter
|
|
362
|
+
label={filterLabelPicked}
|
|
363
|
+
placeholder={filterPlaceholderPicked}
|
|
364
|
+
dataTest={`${dataTest}-filter`}
|
|
365
|
+
filter={actualFilterPicked}
|
|
366
|
+
onChange={
|
|
367
|
+
onFilterChangePicked
|
|
368
|
+
? onFilterChangePicked
|
|
369
|
+
: ({ value }) =>
|
|
370
|
+
setInternalFilterPicked(value)
|
|
371
|
+
}
|
|
372
|
+
/>
|
|
373
|
+
)}
|
|
374
|
+
</RightHeader>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
<OptionsContainer
|
|
378
|
+
selected
|
|
379
|
+
allOptionsKey={pickedOptionsKey}
|
|
380
|
+
dataTest={`${dataTest}-pickedoptions`}
|
|
381
|
+
emptyComponent={selectedEmptyComponent}
|
|
382
|
+
getOptionClickHandlers={getOptionClickHandlers}
|
|
383
|
+
highlightedOptions={highlightedPickedOptions}
|
|
384
|
+
loading={loadingPicked}
|
|
385
|
+
options={pickedOptions}
|
|
386
|
+
renderOption={renderOption}
|
|
387
|
+
selectionHandler={deselectSingleOption}
|
|
388
|
+
toggleHighlightedOption={toggleHighlightedPickedOption}
|
|
389
|
+
onEndReached={onEndReachedPicked}
|
|
390
|
+
/>
|
|
391
|
+
|
|
392
|
+
{(rightFooter || enableOrderChange) && (
|
|
393
|
+
<RightFooter dataTest={`${dataTest}-rightfooter`}>
|
|
394
|
+
{enableOrderChange && (
|
|
395
|
+
<ReorderingActions
|
|
396
|
+
dataTest={`${dataTest}-reorderingactions`}
|
|
397
|
+
filterActive={filterActivePicked}
|
|
398
|
+
disabledDown={isReorderDownDisabled({
|
|
399
|
+
highlightedPickedOptions,
|
|
400
|
+
selected,
|
|
401
|
+
filterActivePicked,
|
|
402
|
+
})}
|
|
403
|
+
disabledUp={isReorderUpDisabled({
|
|
404
|
+
highlightedPickedOptions,
|
|
405
|
+
selected,
|
|
406
|
+
filterActivePicked,
|
|
407
|
+
})}
|
|
408
|
+
onChangeUp={() => {
|
|
409
|
+
const highlightedSet = new Set(
|
|
410
|
+
highlightedPickedOptions
|
|
411
|
+
)
|
|
412
|
+
reorderScrollTargetRef.current =
|
|
413
|
+
selected.find((value) =>
|
|
414
|
+
highlightedSet.has(value)
|
|
415
|
+
) ?? null
|
|
416
|
+
moveHighlightedPickedOptionUp({
|
|
417
|
+
selected,
|
|
418
|
+
highlightedPickedOptions,
|
|
419
|
+
onChange,
|
|
420
|
+
})
|
|
421
|
+
}}
|
|
422
|
+
onChangeDown={() => {
|
|
423
|
+
const highlightedSet = new Set(
|
|
424
|
+
highlightedPickedOptions
|
|
425
|
+
)
|
|
426
|
+
reorderScrollTargetRef.current =
|
|
427
|
+
selected.findLast((value) =>
|
|
428
|
+
highlightedSet.has(value)
|
|
429
|
+
) ?? null
|
|
430
|
+
moveHighlightedPickedOptionDown({
|
|
431
|
+
selected,
|
|
432
|
+
highlightedPickedOptions,
|
|
433
|
+
onChange,
|
|
434
|
+
})
|
|
435
|
+
}}
|
|
436
|
+
onChangeToTop={() => {
|
|
437
|
+
const highlightedSet = new Set(
|
|
438
|
+
highlightedPickedOptions
|
|
439
|
+
)
|
|
440
|
+
reorderScrollTargetRef.current =
|
|
441
|
+
selected.find((value) =>
|
|
442
|
+
highlightedSet.has(value)
|
|
443
|
+
) ?? null
|
|
444
|
+
moveHighlightedPickedOptionToTop({
|
|
445
|
+
selected,
|
|
446
|
+
highlightedPickedOptions,
|
|
447
|
+
onChange,
|
|
448
|
+
})
|
|
449
|
+
}}
|
|
450
|
+
onChangeToBottom={() => {
|
|
451
|
+
const highlightedSet = new Set(
|
|
452
|
+
highlightedPickedOptions
|
|
453
|
+
)
|
|
454
|
+
reorderScrollTargetRef.current =
|
|
455
|
+
selected.findLast((value) =>
|
|
456
|
+
highlightedSet.has(value)
|
|
457
|
+
) ?? null
|
|
458
|
+
moveHighlightedPickedOptionToBottom({
|
|
459
|
+
selected,
|
|
460
|
+
highlightedPickedOptions,
|
|
461
|
+
onChange,
|
|
462
|
+
})
|
|
463
|
+
}}
|
|
464
|
+
/>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{rightFooter}
|
|
468
|
+
</RightFooter>
|
|
469
|
+
)}
|
|
470
|
+
</RightSide>
|
|
471
|
+
</Container>
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const defaultRenderOption = (option) => <TransferOption {...option} />
|
|
476
|
+
|
|
477
|
+
Transfer.propTypes = {
|
|
478
|
+
options: PropTypes.arrayOf(
|
|
479
|
+
PropTypes.shape({
|
|
480
|
+
label: PropTypes.string.isRequired,
|
|
481
|
+
value: PropTypes.string.isRequired,
|
|
482
|
+
disabled: PropTypes.bool,
|
|
483
|
+
})
|
|
484
|
+
).isRequired,
|
|
485
|
+
onChange: PropTypes.func.isRequired,
|
|
486
|
+
|
|
487
|
+
addAllText: PropTypes.string,
|
|
488
|
+
addIndividualText: PropTypes.string,
|
|
489
|
+
className: PropTypes.string,
|
|
490
|
+
dataTest: PropTypes.string,
|
|
491
|
+
disabled: PropTypes.bool,
|
|
492
|
+
enableOrderChange: PropTypes.bool,
|
|
493
|
+
filterCallback: PropTypes.func,
|
|
494
|
+
filterCallbackPicked: PropTypes.func,
|
|
495
|
+
filterLabel: PropTypes.string,
|
|
496
|
+
filterLabelPicked: PropTypes.string,
|
|
497
|
+
filterPlaceholder: PropTypes.string,
|
|
498
|
+
filterPlaceholderPicked: PropTypes.string,
|
|
499
|
+
filterable: PropTypes.bool,
|
|
500
|
+
filterablePicked: PropTypes.bool,
|
|
501
|
+
height: PropTypes.string,
|
|
502
|
+
hideFilterInput: PropTypes.bool,
|
|
503
|
+
hideFilterInputPicked: PropTypes.bool,
|
|
504
|
+
initialSearchTerm: PropTypes.string,
|
|
505
|
+
initialSearchTermPicked: PropTypes.string,
|
|
506
|
+
leftFooter: PropTypes.node,
|
|
507
|
+
leftHeader: PropTypes.node,
|
|
508
|
+
loading: PropTypes.bool,
|
|
509
|
+
loadingPicked: PropTypes.bool,
|
|
510
|
+
maxSelections: PropTypes.number,
|
|
511
|
+
optionsWidth: PropTypes.string,
|
|
512
|
+
removeAllText: PropTypes.string,
|
|
513
|
+
removeIndividualText: PropTypes.string,
|
|
514
|
+
renderOption: PropTypes.func,
|
|
515
|
+
rightFooter: PropTypes.node,
|
|
516
|
+
rightHeader: PropTypes.node,
|
|
517
|
+
searchTerm: PropTypes.string,
|
|
518
|
+
searchTermPicked: PropTypes.string,
|
|
519
|
+
selected: PropTypes.arrayOf(PropTypes.string),
|
|
520
|
+
selectedEmptyComponent: PropTypes.node,
|
|
521
|
+
/**
|
|
522
|
+
* To be used in scenarios where selected options may not be present
|
|
523
|
+
* in the options array. Like when having options that lazy load or can
|
|
524
|
+
* be filtered async.
|
|
525
|
+
*/
|
|
526
|
+
selectedOptionsLookup: PropTypes.objectOf(
|
|
527
|
+
PropTypes.shape({
|
|
528
|
+
label: PropTypes.string.isRequired,
|
|
529
|
+
value: PropTypes.string.isRequired,
|
|
530
|
+
disabled: PropTypes.bool,
|
|
531
|
+
})
|
|
532
|
+
),
|
|
533
|
+
selectedWidth: PropTypes.string,
|
|
534
|
+
sourceEmptyPlaceholder: PropTypes.node,
|
|
535
|
+
onEndReached: PropTypes.func,
|
|
536
|
+
onEndReachedPicked: PropTypes.func,
|
|
537
|
+
onFilterChange: PropTypes.func,
|
|
538
|
+
onFilterChangePicked: PropTypes.func,
|
|
539
|
+
}
|