@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.
Files changed (109) hide show
  1. package/package.json +10 -9
  2. package/src/__e2e__/add_remove-highlighted-options.e2e.stories.js +30 -0
  3. package/src/__e2e__/common/options.js +90 -0
  4. package/src/__e2e__/common/stateful-decorator.js +33 -0
  5. package/src/__e2e__/common.js +0 -0
  6. package/src/__e2e__/disabled-transfer-buttons.e2e.stories.js +49 -0
  7. package/src/__e2e__/disabled-transfer-options.e2e.stories.js +21 -0
  8. package/src/__e2e__/display-order.e2e.stories.js +24 -0
  9. package/src/__e2e__/filter-options-list.e2e.stories.js +87 -0
  10. package/src/__e2e__/highlight-range-of-options.e2e.stories.js +52 -0
  11. package/src/__e2e__/loading_lists.e2e.stories.js +26 -0
  12. package/src/__e2e__/notify_at_end_of_list.e2e.stories.js +116 -0
  13. package/src/__e2e__/reorder-with-buttons.e2e.stories.js +35 -0
  14. package/src/__e2e__/set_unset-highlighted-option.e2e.stories.js +30 -0
  15. package/src/__e2e__/transferring-items.e2e.stories.js +27 -0
  16. package/src/__tests__/common.test.js +131 -0
  17. package/src/__tests__/helper/add-all-selectable-source-options.test.js +46 -0
  18. package/src/__tests__/helper/add-individual-source-options.test.js +80 -0
  19. package/src/__tests__/helper/default-filter-callback.test.js +45 -0
  20. package/src/__tests__/helper/is-reorder-down-disabled.test.js +96 -0
  21. package/src/__tests__/helper/is-reorder-up-disabled.test.js +96 -0
  22. package/src/__tests__/helper/move-highlighted-picked-option-down.test.js +111 -0
  23. package/src/__tests__/helper/move-highlighted-picked-option-to-bottom.test.js +101 -0
  24. package/src/__tests__/helper/move-highlighted-picked-option-to-top.test.js +101 -0
  25. package/src/__tests__/helper/move-highlighted-picked-option-up.test.js +111 -0
  26. package/src/__tests__/helper/remove-all-picked-options.test.js +29 -0
  27. package/src/__tests__/helper/remove-individual-picked-options.test.js +38 -0
  28. package/src/__tests__/helper/use-highlighted-option/create-toggle-highlighted-option.test.js +104 -0
  29. package/src/__tests__/helper/use-highlighted-option/toggle-add.test.js +84 -0
  30. package/src/__tests__/helper/use-highlighted-option/toggle-range.test.js +150 -0
  31. package/src/__tests__/helper/use-highlighted-option/toggle-replace.test.js +39 -0
  32. package/src/__tests__/helper/use-highlighted-option.test.js +41 -0
  33. package/src/__tests__/reordering-actions.test.js +165 -0
  34. package/src/__tests__/transfer.test.js +137 -0
  35. package/src/actions.js +33 -0
  36. package/src/add-all.js +27 -0
  37. package/src/add-individual.js +27 -0
  38. package/src/common/find-option-index.js +9 -0
  39. package/src/common/get-mode-by-modifier-key.js +35 -0
  40. package/src/common/index.js +5 -0
  41. package/src/common/is-option.js +7 -0
  42. package/src/common/modes.js +11 -0
  43. package/src/common/remove-option.js +19 -0
  44. package/src/common/toggle-value.js +18 -0
  45. package/src/container.js +23 -0
  46. package/src/end-intersection-detector.js +37 -0
  47. package/src/features/add_remove-highlighted-options/index.js +92 -0
  48. package/src/features/add_remove-highlighted-options.feature +41 -0
  49. package/src/features/common/index.js +8 -0
  50. package/src/features/disabled-transfer-buttons/index.js +118 -0
  51. package/src/features/disabled-transfer-buttons.feature +46 -0
  52. package/src/features/disabled-transfer-options/index.js +182 -0
  53. package/src/features/disabled-transfer-options.feature +42 -0
  54. package/src/features/display-order/index.js +205 -0
  55. package/src/features/display-order.feature +30 -0
  56. package/src/features/filter-options-list/index.js +133 -0
  57. package/src/features/filter-options-list.feature +40 -0
  58. package/src/features/highlight-range-of-options/index.js +336 -0
  59. package/src/features/highlight-range-of-options.feature +70 -0
  60. package/src/features/loading_lists/index.js +43 -0
  61. package/src/features/loading_lists.feature +19 -0
  62. package/src/features/notify_at_end_of_list/index.js +125 -0
  63. package/src/features/notify_at_end_of_list.feature +64 -0
  64. package/src/features/reorder-with-buttons/index.js +181 -0
  65. package/src/features/reorder-with-buttons.feature +138 -0
  66. package/src/features/set_unset-highlighted-option/index.js +121 -0
  67. package/src/features/set_unset-highlighted-option.feature +42 -0
  68. package/src/features/transferring-items/index.js +375 -0
  69. package/src/features/transferring-items.feature +44 -0
  70. package/src/filter.js +38 -0
  71. package/src/icons.js +194 -0
  72. package/src/index.js +2 -0
  73. package/src/left-footer.js +22 -0
  74. package/src/left-header.js +22 -0
  75. package/src/left-side.js +34 -0
  76. package/src/locales/en/translations.json +7 -0
  77. package/src/locales/index.js +16 -0
  78. package/src/options-container.js +127 -0
  79. package/src/remove-all.js +27 -0
  80. package/src/remove-individual.js +27 -0
  81. package/src/reordering-actions.js +136 -0
  82. package/src/right-footer.js +22 -0
  83. package/src/right-header.js +22 -0
  84. package/src/right-side.js +33 -0
  85. package/src/transfer/add-all-selectable-source-options.js +37 -0
  86. package/src/transfer/add-individual-source-options.js +61 -0
  87. package/src/transfer/create-double-click-handlers.js +36 -0
  88. package/src/transfer/default-filter-callback.js +17 -0
  89. package/src/transfer/get-highlighted-picked-indices.js +26 -0
  90. package/src/transfer/get-option-click-handlers.js +19 -0
  91. package/src/transfer/index.js +17 -0
  92. package/src/transfer/is-reorder-down-disabled.js +34 -0
  93. package/src/transfer/is-reorder-up-disabled.js +30 -0
  94. package/src/transfer/move-highlighted-picked-option-down.js +54 -0
  95. package/src/transfer/move-highlighted-picked-option-to-bottom.js +44 -0
  96. package/src/transfer/move-highlighted-picked-option-to-top.js +38 -0
  97. package/src/transfer/move-highlighted-picked-option-up.js +47 -0
  98. package/src/transfer/remove-all-picked-options.js +13 -0
  99. package/src/transfer/remove-individual-picked-options.js +49 -0
  100. package/src/transfer/use-filter.js +17 -0
  101. package/src/transfer/use-highlighted-options/create-toggle-highlighted-option.js +64 -0
  102. package/src/transfer/use-highlighted-options/toggle-add.js +20 -0
  103. package/src/transfer/use-highlighted-options/toggle-range.js +61 -0
  104. package/src/transfer/use-highlighted-options/toggle-replace.js +26 -0
  105. package/src/transfer/use-highlighted-options.js +34 -0
  106. package/src/transfer/use-options-key-monitor.js +41 -0
  107. package/src/transfer-option.js +91 -0
  108. package/src/transfer.js +539 -0
  109. 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
+ }
@@ -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
+ }