@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,22 @@
1
+ import { colors, spacers } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ export const LeftHeader = ({ children, dataTest }) => (
6
+ <div data-test={dataTest}>
7
+ {children}
8
+
9
+ <style jsx>{`
10
+ div {
11
+ border-bottom: 1px solid ${colors.grey400};
12
+ flex-grow: 0;
13
+ padding: 0 ${spacers.dp8};
14
+ }
15
+ `}</style>
16
+ </div>
17
+ )
18
+
19
+ LeftHeader.propTypes = {
20
+ children: PropTypes.node,
21
+ dataTest: PropTypes.string,
22
+ }
@@ -0,0 +1,34 @@
1
+ import { colors } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ export const LeftSide = ({ children, dataTest, width }) => (
6
+ <div data-test={dataTest}>
7
+ {children}
8
+
9
+ {
10
+ /**
11
+ * Flex basis 0px to make sure right and left side
12
+ * always have the same width
13
+ */ ''
14
+ }
15
+ <style jsx>{`
16
+ div {
17
+ display: flex;
18
+ flex-direction: column;
19
+ background-color: ${colors.white};
20
+ border-radius: 3px;
21
+ border: 1px solid ${colors.grey400};
22
+ min-height: 240px;
23
+ max-width: 100%;
24
+ width: ${width};
25
+ }
26
+ `}</style>
27
+ </div>
28
+ )
29
+
30
+ LeftSide.propTypes = {
31
+ width: PropTypes.string.isRequired,
32
+ children: PropTypes.node,
33
+ dataTest: PropTypes.string,
34
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "Reordering not allowed when filtering list": "Reordering not allowed when filtering list",
3
+ "Move selected items to top": "Move selected items to top",
4
+ "Move selected items up": "Move selected items up",
5
+ "Move selected items down": "Move selected items down",
6
+ "Move selected items to bottom": "Move selected items to bottom"
7
+ }
@@ -0,0 +1,16 @@
1
+ //------------------------------------------------------------------------------
2
+ // <auto-generated>
3
+ // This code was generated by d2-i18n-generate.
4
+ //
5
+ // Changes to this file may cause incorrect behavior and will be lost if
6
+ // the code is regenerated.
7
+ // </auto-generated>
8
+ //------------------------------------------------------------------------------
9
+ import i18n from '@dhis2/d2-i18n'
10
+
11
+ import enTranslations from './en/translations.json'
12
+
13
+ const namespace = 'default'
14
+ i18n.addResources('en', namespace, enTranslations)
15
+
16
+ export default i18n
@@ -0,0 +1,127 @@
1
+ import { CircularLoader } from '@dhis2-ui/loader'
2
+ import PropTypes from 'prop-types'
3
+ import React, { Fragment, useRef } from 'react'
4
+ import { EndIntersectionDetector } from './end-intersection-detector.js'
5
+ import { useOptionsKeyMonitor } from './transfer/use-options-key-monitor.js'
6
+
7
+ export const OptionsContainer = ({
8
+ allOptionsKey,
9
+ dataTest,
10
+ emptyComponent,
11
+ onEndReached,
12
+ getOptionClickHandlers,
13
+ highlightedOptions,
14
+ loading,
15
+ renderOption,
16
+ options,
17
+ selected = false,
18
+ selectionHandler,
19
+ toggleHighlightedOption,
20
+ }) => {
21
+ const scrollBoxRef = useRef(null)
22
+ const listRef = useRef(null)
23
+ useOptionsKeyMonitor({
24
+ scrollBoxRef,
25
+ listRef,
26
+ allOptionsKey,
27
+ onEndReached,
28
+ })
29
+ return (
30
+ <div className="optionsContainer">
31
+ {loading && (
32
+ <div className="loading">
33
+ <CircularLoader />
34
+ </div>
35
+ )}
36
+
37
+ <div className="container" data-test={dataTest} ref={scrollBoxRef}>
38
+ <div className="content-container" ref={listRef}>
39
+ {!options.length && emptyComponent}
40
+ {options.map((option) => {
41
+ const highlighted = !!highlightedOptions.find(
42
+ (highlightedSourceOption) =>
43
+ highlightedSourceOption === option.value
44
+ )
45
+
46
+ return (
47
+ <Fragment key={option.value}>
48
+ {renderOption({
49
+ ...option,
50
+ ...getOptionClickHandlers(
51
+ option,
52
+ selectionHandler,
53
+ toggleHighlightedOption
54
+ ),
55
+ highlighted,
56
+ selected,
57
+ })}
58
+ </Fragment>
59
+ )
60
+ })}
61
+
62
+ {onEndReached && (
63
+ <EndIntersectionDetector
64
+ dataTest={`${dataTest}-endintersectiondetector`}
65
+ rootRef={scrollBoxRef}
66
+ onEndReached={onEndReached}
67
+ />
68
+ )}
69
+ </div>
70
+ </div>
71
+
72
+ <style jsx>{`
73
+ .optionsContainer {
74
+ flex-grow: 1;
75
+ position: relative;
76
+ overflow: hidden;
77
+ }
78
+
79
+ .container {
80
+ overflow-y: auto;
81
+ height: 100%;
82
+ }
83
+
84
+ .loading {
85
+ display: flex;
86
+ height: 100%;
87
+ width: 100%;
88
+ align-items: center;
89
+ justify-content: center;
90
+ position: absolute;
91
+ z-index: 2;
92
+ top: 0;
93
+ inset-inline-start: 0;
94
+ }
95
+
96
+ .content-container {
97
+ z-index: 1;
98
+ position: relative;
99
+ }
100
+
101
+ .loading + .container .content-container {
102
+ filter: blur(2px);
103
+ }
104
+ `}</style>
105
+ </div>
106
+ )
107
+ }
108
+
109
+ OptionsContainer.propTypes = {
110
+ allOptionsKey: PropTypes.string.isRequired,
111
+ dataTest: PropTypes.string.isRequired,
112
+ getOptionClickHandlers: PropTypes.func.isRequired,
113
+ emptyComponent: PropTypes.node,
114
+ highlightedOptions: PropTypes.arrayOf(PropTypes.string),
115
+ loading: PropTypes.bool,
116
+ options: PropTypes.arrayOf(
117
+ PropTypes.shape({
118
+ label: PropTypes.string.isRequired,
119
+ value: PropTypes.string.isRequired,
120
+ })
121
+ ),
122
+ renderOption: PropTypes.func,
123
+ selected: PropTypes.bool,
124
+ selectionHandler: PropTypes.func,
125
+ toggleHighlightedOption: PropTypes.func,
126
+ onEndReached: PropTypes.func,
127
+ }
@@ -0,0 +1,27 @@
1
+ import { Button } from '@dhis2-ui/button'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+ import { IconRemoveAll } from './icons.js'
5
+
6
+ export const RemoveAll = ({ label, dataTest, disabled, onClick }) => (
7
+ <Button
8
+ dataTest={dataTest}
9
+ disabled={disabled}
10
+ onClick={onClick}
11
+ icon={
12
+ <IconRemoveAll
13
+ disabled={disabled}
14
+ dataTest={`${dataTest}-iconremoveall`}
15
+ />
16
+ }
17
+ >
18
+ {label}
19
+ </Button>
20
+ )
21
+
22
+ RemoveAll.propTypes = {
23
+ dataTest: PropTypes.string.isRequired,
24
+ onClick: PropTypes.func.isRequired,
25
+ disabled: PropTypes.bool,
26
+ label: PropTypes.string,
27
+ }
@@ -0,0 +1,27 @@
1
+ import { Button } from '@dhis2-ui/button'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+ import { IconRemoveIndividual } from './icons.js'
5
+
6
+ export const RemoveIndividual = ({ label, dataTest, disabled, onClick }) => (
7
+ <Button
8
+ dataTest={dataTest}
9
+ disabled={disabled}
10
+ onClick={onClick}
11
+ icon={
12
+ <IconRemoveIndividual
13
+ disabled={disabled}
14
+ dataTest={`${dataTest}-iconremoveindividual`}
15
+ />
16
+ }
17
+ >
18
+ {label}
19
+ </Button>
20
+ )
21
+
22
+ RemoveIndividual.propTypes = {
23
+ dataTest: PropTypes.string.isRequired,
24
+ onClick: PropTypes.func.isRequired,
25
+ disabled: PropTypes.bool,
26
+ label: PropTypes.string,
27
+ }
@@ -0,0 +1,136 @@
1
+ import { spacers } from '@dhis2/ui-constants'
2
+ import { Button } from '@dhis2-ui/button'
3
+ import { Tooltip } from '@dhis2-ui/tooltip'
4
+ import PropTypes from 'prop-types'
5
+ import React from 'react'
6
+ import {
7
+ IconMoveDown,
8
+ IconMoveToBottom,
9
+ IconMoveToTop,
10
+ IconMoveUp,
11
+ } from './icons.js'
12
+ import i18n from './locales/index.js'
13
+
14
+ const filterActiveTooltip = i18n.t('Reordering not allowed when filtering list')
15
+
16
+ export const ReorderingActions = ({
17
+ dataTest,
18
+ disabledDown,
19
+ disabledUp,
20
+ filterActive,
21
+ onChangeUp,
22
+ onChangeDown,
23
+ onChangeToTop,
24
+ onChangeToBottom,
25
+ }) => {
26
+ const moveToTopLabel = i18n.t('Move selected items to top')
27
+ const moveUpLabel = i18n.t('Move selected items up')
28
+ const moveDownLabel = i18n.t('Move selected items down')
29
+ const moveToBottomLabel = i18n.t('Move selected items to bottom')
30
+
31
+ const renderButtons = (tooltipHandlers = {}) => (
32
+ <div data-test={dataTest} {...tooltipHandlers}>
33
+ <Button
34
+ small
35
+ secondary
36
+ disabled={disabledUp}
37
+ onClick={() => !disabledUp && onChangeToTop()}
38
+ dataTest={`${dataTest}-buttonmovetotop`}
39
+ aria-label={moveToTopLabel}
40
+ icon={
41
+ <IconMoveToTop
42
+ dataTest={`${dataTest}-iconmovetotop`}
43
+ disabled={disabledUp}
44
+ />
45
+ }
46
+ />
47
+
48
+ <Button
49
+ small
50
+ secondary
51
+ disabled={disabledUp}
52
+ onClick={() => !disabledUp && onChangeUp()}
53
+ dataTest={`${dataTest}-buttonmoveup`}
54
+ aria-label={moveUpLabel}
55
+ icon={
56
+ <IconMoveUp
57
+ dataTest={`${dataTest}-iconmoveup`}
58
+ disabled={disabledUp}
59
+ />
60
+ }
61
+ />
62
+ <Button
63
+ small
64
+ secondary
65
+ disabled={disabledDown}
66
+ onClick={() => !disabledDown && onChangeDown()}
67
+ dataTest={`${dataTest}-buttonmovedown`}
68
+ aria-label={moveDownLabel}
69
+ icon={
70
+ <IconMoveDown
71
+ dataTest={`${dataTest}-iconmovedown`}
72
+ disabled={disabledDown}
73
+ />
74
+ }
75
+ />
76
+ <Button
77
+ small
78
+ secondary
79
+ disabled={disabledDown}
80
+ onClick={() => !disabledDown && onChangeToBottom()}
81
+ dataTest={`${dataTest}-buttonmovetobottom`}
82
+ aria-label={moveToBottomLabel}
83
+ icon={
84
+ <IconMoveToBottom
85
+ dataTest={`${dataTest}-iconmovetobottom`}
86
+ disabled={disabledDown}
87
+ />
88
+ }
89
+ />
90
+
91
+ <style jsx>{`
92
+ div {
93
+ display: flex;
94
+ justify-content: flex-end;
95
+ margin-inline-start: auto;
96
+ width: fit-content;
97
+ gap: ${spacers.dp4};
98
+ padding-top: ${spacers.dp8};
99
+ }
100
+
101
+ div:last-child {
102
+ padding-bottom: ${spacers.dp8};
103
+ }
104
+ `}</style>
105
+ </div>
106
+ )
107
+
108
+ if (filterActive) {
109
+ return (
110
+ <Tooltip openDelay={500} content={filterActiveTooltip}>
111
+ {({ onMouseOver, onMouseOut, onFocus, onBlur, ref }) =>
112
+ renderButtons({
113
+ ref,
114
+ onMouseOver,
115
+ onMouseOut,
116
+ onFocus,
117
+ onBlur,
118
+ })
119
+ }
120
+ </Tooltip>
121
+ )
122
+ }
123
+
124
+ return renderButtons()
125
+ }
126
+
127
+ ReorderingActions.propTypes = {
128
+ dataTest: PropTypes.string.isRequired,
129
+ onChangeDown: PropTypes.func.isRequired,
130
+ onChangeToBottom: PropTypes.func.isRequired,
131
+ onChangeToTop: PropTypes.func.isRequired,
132
+ onChangeUp: PropTypes.func.isRequired,
133
+ disabledDown: PropTypes.bool,
134
+ disabledUp: PropTypes.bool,
135
+ filterActive: PropTypes.bool,
136
+ }
@@ -0,0 +1,22 @@
1
+ import { colors, spacers } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ export const RightFooter = ({ children, dataTest }) => (
6
+ <div data-test={dataTest}>
7
+ {children}
8
+
9
+ <style jsx>{`
10
+ div {
11
+ flex-grow: 0;
12
+ border-top: 1px solid ${colors.grey400};
13
+ padding: 0 ${spacers.dp8};
14
+ }
15
+ `}</style>
16
+ </div>
17
+ )
18
+
19
+ RightFooter.propTypes = {
20
+ children: PropTypes.node,
21
+ dataTest: PropTypes.string,
22
+ }
@@ -0,0 +1,22 @@
1
+ import { colors, spacers } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ export const RightHeader = ({ children, dataTest }) => (
6
+ <div data-test={dataTest}>
7
+ {children}
8
+
9
+ <style jsx>{`
10
+ div {
11
+ border-bottom: 1px solid ${colors.grey400};
12
+ flex-grow: 0;
13
+ padding: 0 ${spacers.dp8};
14
+ }
15
+ `}</style>
16
+ </div>
17
+ )
18
+
19
+ RightHeader.propTypes = {
20
+ children: PropTypes.node,
21
+ dataTest: PropTypes.string,
22
+ }
@@ -0,0 +1,33 @@
1
+ import { colors } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+
5
+ export const RightSide = ({ children, dataTest, width }) => (
6
+ <div data-test={dataTest}>
7
+ {children}
8
+
9
+ {
10
+ /**
11
+ * Flex basis 0px to make sure right and left side
12
+ * always have the same width
13
+ */ ''
14
+ }
15
+ <style jsx>{`
16
+ div {
17
+ background-color: ${colors.white};
18
+ border: 1px solid ${colors.grey400};
19
+ border-radius: 3px;
20
+ display: flex;
21
+ flex-direction: column;
22
+ max-width: 100%;
23
+ width: ${width};
24
+ }
25
+ `}</style>
26
+ </div>
27
+ )
28
+
29
+ RightSide.propTypes = {
30
+ width: PropTypes.string.isRequired,
31
+ children: PropTypes.node,
32
+ dataTest: PropTypes.string,
33
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {Object[]} args.sourceOptions
4
+ * @param {string[]} args.selected
5
+ * @param {Function} args.onChange
6
+ * @param {Function} arg.setHighlightedSourceOptions
7
+ * @returns {void}
8
+ */
9
+ export const addAllSelectableSourceOptions = ({
10
+ sourceOptions,
11
+ onChange,
12
+ selected,
13
+ setHighlightedSourceOptions,
14
+ }) => {
15
+ const enabledSourceOptions = sourceOptions.filter(
16
+ ({ disabled }) => !disabled
17
+ )
18
+
19
+ const newSelected = enabledSourceOptions.reduce(
20
+ (accumulatedSelected, enabledSourceOption) => [
21
+ ...accumulatedSelected,
22
+ enabledSourceOption.value,
23
+ ],
24
+ selected
25
+ )
26
+
27
+ setHighlightedSourceOptions([])
28
+
29
+ // If we ever allow maxSelection to be any value
30
+ // other than 1 and Infinity, we need to think
31
+ // about how this should behave:
32
+ //
33
+ // Either we hide this button when it's not "Infinity",
34
+ // or we have to decide whether we want to take the first
35
+ // nth options or the last
36
+ onChange({ selected: newSelected })
37
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {bool} args.filterable
4
+ * @param {Object[]} args.sourceOptions
5
+ * @param {string[]} args.highlightedSourceOptions
6
+ * @param {string[]} args.selected
7
+ * @param {Function} args.onChange
8
+ * @param {Function} args.setHighlightedSourceOptions
9
+ * @returns void
10
+ */
11
+ export const addIndividualSourceOptions = ({
12
+ filterable,
13
+ sourceOptions,
14
+ highlightedSourceOptions,
15
+ maxSelections,
16
+ onChange,
17
+ selected,
18
+ setHighlightedSourceOptions,
19
+ }) => {
20
+ /**
21
+ * Creates a subset of the highlighted options to reflect a changed
22
+ * filter value in case previously highlighted options are now
23
+ * hidden.
24
+ *
25
+ * This enables us to keep items highlighted while searching for
26
+ * a particular one.
27
+ *
28
+ * With this subset we only select the subset when the user
29
+ * clicks the "add individuals" button
30
+ */
31
+ const filteredHighlightedSourceOptions = filterable
32
+ ? highlightedSourceOptions.filter((value) =>
33
+ sourceOptions.find(
34
+ (filteredOption) => filteredOption.value === value
35
+ )
36
+ )
37
+ : highlightedSourceOptions
38
+
39
+ const newSelected = filteredHighlightedSourceOptions.reduce(
40
+ (accumulatedSelected, value) => [
41
+ ...accumulatedSelected,
42
+ filteredHighlightedSourceOptions.find(
43
+ (filteredHighlightedSourceOption) =>
44
+ filteredHighlightedSourceOption === value
45
+ ),
46
+ ],
47
+ selected
48
+ )
49
+
50
+ setHighlightedSourceOptions([])
51
+
52
+ /**
53
+ * This will extract from the end, hence the "-1 *"
54
+ * As the "newest" additions are always at the end of the array,
55
+ * it's safe to just take the last nth (depending on maxSelection)
56
+ * to always get the right ones
57
+ */
58
+ onChange({
59
+ selected: newSelected.slice(-1 * maxSelections),
60
+ })
61
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {number} args.maxSelections
4
+ * @param {string[]} args.selected
5
+ * @param {Function} args.onChange
6
+ * @param {Function} args.setHighlightedSourceOptions
7
+ * @param {Function} args.setHighlightedPickedOptions
8
+ * @returns void
9
+ */
10
+ export const createDoubleClickHandlers = ({
11
+ maxSelections,
12
+ onChange,
13
+ selected,
14
+ setHighlightedPickedOptions,
15
+ setHighlightedSourceOptions,
16
+ }) => {
17
+ const selectSingleOption = ({ value }) => {
18
+ const newSelected = selected.includes(value)
19
+ ? selected
20
+ : [...selected, value]
21
+
22
+ setHighlightedSourceOptions([])
23
+ onChange({ selected: newSelected.slice(-1 * maxSelections) })
24
+ }
25
+
26
+ const deselectSingleOption = ({ value }) => {
27
+ const newSelected = selected.filter(
28
+ (curSelected) => curSelected !== value
29
+ )
30
+
31
+ setHighlightedPickedOptions([])
32
+ onChange({ selected: newSelected })
33
+ }
34
+
35
+ return { selectSingleOption, deselectSingleOption }
36
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @param {Object[]} options
3
+ * @param {string} filter
4
+ * @returns {Object[]}
5
+ */
6
+ export const defaultFilterCallback = (options, filter) => {
7
+ if (filter === '') {
8
+ return options
9
+ }
10
+ try {
11
+ const regex = new RegExp(filter, 'i')
12
+ return options.filter(({ label }) => label.match(regex))
13
+ } catch {
14
+ console.warn('Invalid regex filter:', filter)
15
+ return options
16
+ }
17
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Returns the indices, in ascending order, of `highlightedPickedOptions`
3
+ * within `selected`. Values in `highlightedPickedOptions` that don't appear
4
+ * in `selected` are ignored, so callers can pass a potentially stale
5
+ * highlight list without filtering first.
6
+ *
7
+ * Runs in O(n) over `selected` via a Set lookup.
8
+ *
9
+ * @param {Object} args
10
+ * @param {string[]} args.selected
11
+ * @param {string[]} args.highlightedPickedOptions
12
+ * @returns {number[]}
13
+ */
14
+ export const getHighlightedPickedIndices = ({
15
+ selected,
16
+ highlightedPickedOptions,
17
+ }) => {
18
+ const highlightedSet = new Set(highlightedPickedOptions)
19
+ const indices = []
20
+ selected.forEach((value, index) => {
21
+ if (highlightedSet.has(value)) {
22
+ indices.push(index)
23
+ }
24
+ })
25
+ return indices
26
+ }
@@ -0,0 +1,19 @@
1
+ import { getModeByModifierKey } from '../common/index.js'
2
+
3
+ /**
4
+ * @param {Object} option
5
+ * @param {Function} selectionHandler
6
+ * @param {Function} toggleHighlightedOption
7
+ * @returns {Object}
8
+ */
9
+ export const getOptionClickHandlers = (
10
+ option,
11
+ selectionHandler,
12
+ toggleHighlightedOption
13
+ ) => ({
14
+ onClick: (_, event) => {
15
+ const mode = getModeByModifierKey(event)
16
+ toggleHighlightedOption({ option, mode })
17
+ },
18
+ onDoubleClick: selectionHandler,
19
+ })