@dhis2-ui/transfer 10.16.2 → 10.16.3

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,17 @@
1
+ export * from './add-all-selectable-source-options.js'
2
+ export * from './add-individual-source-options.js'
3
+ export * from './create-double-click-handlers.js'
4
+ export * from './default-filter-callback.js'
5
+ export * from './get-highlighted-picked-indices.js'
6
+ export * from './get-option-click-handlers.js'
7
+ export * from './is-reorder-down-disabled.js'
8
+ export * from './is-reorder-up-disabled.js'
9
+ export * from './move-highlighted-picked-option-down.js'
10
+ export * from './move-highlighted-picked-option-to-bottom.js'
11
+ export * from './move-highlighted-picked-option-to-top.js'
12
+ export * from './move-highlighted-picked-option-up.js'
13
+ export * from './remove-all-picked-options.js'
14
+ export * from './remove-individual-picked-options.js'
15
+ export * from './use-filter.js'
16
+ export * from './use-highlighted-options.js'
17
+ export * from './use-options-key-monitor.js'
@@ -0,0 +1,34 @@
1
+ import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
2
+
3
+ /**
4
+ * @param {Object} args
5
+ * @param {string[]} args.highlightedPickedOptions
6
+ * @param {string[]} args.selected
7
+ * @param {boolean} [args.filterActivePicked] reorder is disabled while a filter is applied to the picked side
8
+ * @returns {bool}
9
+ */
10
+ export const isReorderDownDisabled = ({
11
+ highlightedPickedOptions,
12
+ selected,
13
+ filterActivePicked = false,
14
+ }) => {
15
+ if (filterActivePicked) {
16
+ return true
17
+ }
18
+
19
+ const indices = getHighlightedPickedIndices({
20
+ selected,
21
+ highlightedPickedOptions,
22
+ })
23
+
24
+ if (indices.length === 0) {
25
+ return true
26
+ }
27
+
28
+ const lastIndex = selected.length - 1
29
+
30
+ // Flush to the bottom: indices are [len-n, ..., len-1]
31
+ return indices.every(
32
+ (index, i) => index === lastIndex - (indices.length - 1 - i)
33
+ )
34
+ }
@@ -0,0 +1,30 @@
1
+ import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
2
+
3
+ /**
4
+ * @param {Object} args
5
+ * @param {string[]} args.highlightedPickedOptions
6
+ * @param {string[]} args.selected
7
+ * @param {boolean} [args.filterActivePicked] reorder is disabled while a filter is applied to the picked side
8
+ * @returns {bool}
9
+ */
10
+ export const isReorderUpDisabled = ({
11
+ highlightedPickedOptions,
12
+ selected,
13
+ filterActivePicked = false,
14
+ }) => {
15
+ if (filterActivePicked) {
16
+ return true
17
+ }
18
+
19
+ const indices = getHighlightedPickedIndices({
20
+ selected,
21
+ highlightedPickedOptions,
22
+ })
23
+
24
+ if (indices.length === 0) {
25
+ return true
26
+ }
27
+
28
+ // Flush to the top: indices are [0, 1, ..., n-1]
29
+ return indices.every((index, i) => index === i)
30
+ }
@@ -0,0 +1,54 @@
1
+ import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
2
+
3
+ /**
4
+ * Moves the highlighted picked options down by one slot as a group.
5
+ * If the selection is non-contiguous, the group collapses into a contiguous
6
+ * block (preserving relative order) with its bottom edge landing at
7
+ * `min(selected.length - 1, bottommostHighlightedIndex + 1)`.
8
+ *
9
+ * @param {Object} args
10
+ * @param {string[]} args.selected
11
+ * @param {string[]} args.highlightedPickedOptions
12
+ * @param {Function} args.onChange
13
+ * @returns {void}
14
+ */
15
+ export const moveHighlightedPickedOptionDown = ({
16
+ selected,
17
+ highlightedPickedOptions,
18
+ onChange,
19
+ }) => {
20
+ const indices = getHighlightedPickedIndices({
21
+ selected,
22
+ highlightedPickedOptions,
23
+ })
24
+
25
+ if (indices.length === 0) {
26
+ return
27
+ }
28
+
29
+ const lastIndex = selected.length - 1
30
+
31
+ // Already flush to the bottom — nothing to do
32
+ if (
33
+ indices.every(
34
+ (index, i) => index === lastIndex - (indices.length - 1 - i)
35
+ )
36
+ ) {
37
+ return
38
+ }
39
+
40
+ const indexSet = new Set(indices)
41
+ const highlightedBlock = indices.map((index) => selected[index])
42
+ const remaining = selected.filter((_, index) => !indexSet.has(index))
43
+ const bottommost = indices.at(-1)
44
+ const targetBottom = Math.min(lastIndex, bottommost + 1)
45
+ const insertPos = targetBottom - (indices.length - 1)
46
+
47
+ const reordered = [
48
+ ...remaining.slice(0, insertPos),
49
+ ...highlightedBlock,
50
+ ...remaining.slice(insertPos),
51
+ ]
52
+
53
+ onChange({ selected: reordered })
54
+ }
@@ -0,0 +1,44 @@
1
+ import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
2
+
3
+ /**
4
+ * Moves the highlighted picked options to the very bottom of the list as a
5
+ * single contiguous block, preserving their relative order. Non-contiguous
6
+ * selections are collapsed.
7
+ *
8
+ * @param {Object} args
9
+ * @param {string[]} args.selected
10
+ * @param {string[]} args.highlightedPickedOptions
11
+ * @param {Function} args.onChange
12
+ * @returns {void}
13
+ */
14
+ export const moveHighlightedPickedOptionToBottom = ({
15
+ selected,
16
+ highlightedPickedOptions,
17
+ onChange,
18
+ }) => {
19
+ const indices = getHighlightedPickedIndices({
20
+ selected,
21
+ highlightedPickedOptions,
22
+ })
23
+
24
+ if (indices.length === 0) {
25
+ return
26
+ }
27
+
28
+ const lastIndex = selected.length - 1
29
+
30
+ // Already a contiguous block flush to the bottom — nothing to do
31
+ if (
32
+ indices.every(
33
+ (index, i) => index === lastIndex - (indices.length - 1 - i)
34
+ )
35
+ ) {
36
+ return
37
+ }
38
+
39
+ const indexSet = new Set(indices)
40
+ const highlightedBlock = indices.map((index) => selected[index])
41
+ const remaining = selected.filter((_, index) => !indexSet.has(index))
42
+
43
+ onChange({ selected: [...remaining, ...highlightedBlock] })
44
+ }
@@ -0,0 +1,38 @@
1
+ import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
2
+
3
+ /**
4
+ * Moves the highlighted picked options to the very top of the list as a
5
+ * single contiguous block, preserving their relative order. Non-contiguous
6
+ * selections are collapsed.
7
+ *
8
+ * @param {Object} args
9
+ * @param {string[]} args.selected
10
+ * @param {string[]} args.highlightedPickedOptions
11
+ * @param {Function} args.onChange
12
+ * @returns {void}
13
+ */
14
+ export const moveHighlightedPickedOptionToTop = ({
15
+ selected,
16
+ highlightedPickedOptions,
17
+ onChange,
18
+ }) => {
19
+ const indices = getHighlightedPickedIndices({
20
+ selected,
21
+ highlightedPickedOptions,
22
+ })
23
+
24
+ if (indices.length === 0) {
25
+ return
26
+ }
27
+
28
+ // Already a contiguous block flush to the top — nothing to do
29
+ if (indices.every((index, i) => index === i)) {
30
+ return
31
+ }
32
+
33
+ const indexSet = new Set(indices)
34
+ const highlightedBlock = indices.map((index) => selected[index])
35
+ const remaining = selected.filter((_, index) => !indexSet.has(index))
36
+
37
+ onChange({ selected: [...highlightedBlock, ...remaining] })
38
+ }
@@ -0,0 +1,47 @@
1
+ import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
2
+
3
+ /**
4
+ * Moves the highlighted picked options up by one slot as a group.
5
+ * If the selection is non-contiguous, the group collapses into a contiguous
6
+ * block (preserving relative order) with its top edge landing at
7
+ * `max(0, topmostHighlightedIndex - 1)`.
8
+ *
9
+ * @param {Object} args
10
+ * @param {string[]} args.selected
11
+ * @param {string[]} args.highlightedPickedOptions
12
+ * @param {Function} args.onChange
13
+ * @returns {void}
14
+ */
15
+ export const moveHighlightedPickedOptionUp = ({
16
+ selected,
17
+ highlightedPickedOptions,
18
+ onChange,
19
+ }) => {
20
+ const indices = getHighlightedPickedIndices({
21
+ selected,
22
+ highlightedPickedOptions,
23
+ })
24
+
25
+ if (indices.length === 0) {
26
+ return
27
+ }
28
+
29
+ // Already flush to the top — nothing to do
30
+ if (indices.every((index, i) => index === i)) {
31
+ return
32
+ }
33
+
34
+ const indexSet = new Set(indices)
35
+ const highlightedBlock = indices.map((index) => selected[index])
36
+ const remaining = selected.filter((_, index) => !indexSet.has(index))
37
+ const topmost = indices[0]
38
+ const insertPos = Math.max(0, topmost - 1)
39
+
40
+ const reordered = [
41
+ ...remaining.slice(0, insertPos),
42
+ ...highlightedBlock,
43
+ ...remaining.slice(insertPos),
44
+ ]
45
+
46
+ onChange({ selected: reordered })
47
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {Function} args.setHighlightedPickedOptions
4
+ * @param {Function} args.onChange
5
+ * @returns {void}
6
+ */
7
+ export const removeAllPickedOptions = ({
8
+ setHighlightedPickedOptions,
9
+ onChange,
10
+ }) => {
11
+ setHighlightedPickedOptions([])
12
+ onChange({ selected: [] })
13
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {string[]} args.highlightedPickedOptions
4
+ * @param {string[]} args.selected
5
+ * @param {Function} args.setHighlightedPickedOptions
6
+ * @param {Function} args.onChange
7
+ * @returns {void}
8
+ */
9
+ export const removeIndividualPickedOptions = ({
10
+ filterablePicked,
11
+ pickedOptions,
12
+ highlightedPickedOptions,
13
+ onChange,
14
+ selected,
15
+ setHighlightedPickedOptions,
16
+ }) => {
17
+ /**
18
+ * Creates a subset of the highlighted options to reflect a changed
19
+ * filter value in case previously highlighted options are now
20
+ * hidden.
21
+ *
22
+ * This enables us to keep items highlighted while searching for
23
+ * a particular one.
24
+ *
25
+ * With this subset we only select the subset when the user
26
+ * clicks the "add individuals" button
27
+ */
28
+ const filteredHighlightedPickedOptions = filterablePicked
29
+ ? highlightedPickedOptions.filter((value) =>
30
+ pickedOptions.find(
31
+ (filteredOption) => filteredOption.value === value
32
+ )
33
+ )
34
+ : highlightedPickedOptions
35
+
36
+ const newSelected = selected.filter(
37
+ (selectedOption) =>
38
+ !filteredHighlightedPickedOptions.includes(selectedOption)
39
+ )
40
+
41
+ setHighlightedPickedOptions([])
42
+
43
+ /**
44
+ * as the maximum amount of selected items
45
+ * is already restricted through the selection mechanism,
46
+ * there's no need to handle `maxSelection` here
47
+ */
48
+ onChange({ selected: newSelected })
49
+ }
@@ -0,0 +1,17 @@
1
+ import { useState } from 'react'
2
+
3
+ const identity = (value) => value
4
+
5
+ export const useFilter = ({
6
+ initialSearchTerm,
7
+ onFilterChange,
8
+ externalSearchTerm,
9
+ filterable,
10
+ filterCallback,
11
+ }) => {
12
+ const [internalFilter, setInternalFilter] = useState(initialSearchTerm)
13
+ const filterValue = onFilterChange ? externalSearchTerm : internalFilter
14
+ const filter = filterable ? filterCallback : identity
15
+
16
+ return { filterValue, filter, setInternalFilter }
17
+ }
@@ -0,0 +1,64 @@
1
+ import { ADD_MODE, RANGE_MODE } from '../../common/index.js'
2
+ import { toggleAdd } from './toggle-add.js'
3
+ import { toggleRange } from './toggle-range.js'
4
+ import { toggleReplace } from './toggle-replace.js'
5
+
6
+ /**
7
+ * @param {Object} args
8
+ * @param {bool} args.disabled
9
+ * @param {string[]} args.highlightedOptions
10
+ * @param {Function} args.setHighlightedOptions
11
+ * @param {number} args.maxSelections
12
+ * @param {Function} args.setLastClicked
13
+ * @param {Object[]} args.options
14
+ * @param {string} args.lastClicked
15
+ * @returns {void}
16
+ */
17
+ export const createToggleHighlightedOption =
18
+ ({
19
+ disabled,
20
+ highlightedOptions,
21
+ setHighlightedOptions,
22
+ maxSelections,
23
+ setLastClicked,
24
+ options,
25
+ lastClicked,
26
+ }) =>
27
+ ({ option, mode }) => {
28
+ if (disabled) {
29
+ return
30
+ }
31
+
32
+ setHighlightedOptions([])
33
+
34
+ if (mode === ADD_MODE) {
35
+ setLastClicked(option.value)
36
+
37
+ return toggleAdd({
38
+ highlightedOptions,
39
+ maxSelections,
40
+ option,
41
+ setHighlightedOptions,
42
+ })
43
+ }
44
+
45
+ if (mode === RANGE_MODE) {
46
+ return toggleRange({
47
+ highlightedOptions,
48
+ options,
49
+ option,
50
+ setHighlightedOptions,
51
+ lastClicked,
52
+ maxSelections,
53
+ })
54
+ }
55
+
56
+ // REPLACE_MODE
57
+ setLastClicked(option.value)
58
+
59
+ return toggleReplace({
60
+ option,
61
+ highlightedOptions,
62
+ setHighlightedOptions,
63
+ })
64
+ }
@@ -0,0 +1,20 @@
1
+ import { toggleValue } from '../../common/index.js'
2
+
3
+ /**
4
+ * @param {Object} args
5
+ * @param {number} args.maxSelections
6
+ * @param {string[]} args.highlightedOptions
7
+ * @param {Object} args.option
8
+ * @param {Function} args.setHighlightedOption
9
+ * @returns {void}
10
+ */
11
+ export const toggleAdd = ({
12
+ highlightedOptions,
13
+ maxSelections,
14
+ option,
15
+ setHighlightedOptions,
16
+ }) => {
17
+ const afterToggled = toggleValue(highlightedOptions, option.value)
18
+ const capped = afterToggled.slice(-1 * maxSelections)
19
+ setHighlightedOptions(capped)
20
+ }
@@ -0,0 +1,61 @@
1
+ import { findOptionIndex } from '../../common/index.js'
2
+
3
+ /**
4
+ * @param {Object} args
5
+ * @param {number} args.maxSelections
6
+ * @param {string[]} args.highlightedOptions
7
+ * @param {Object[]} args.options
8
+ * @param {Object} args.option
9
+ * @param {string} args.lastClicked
10
+ * @param {Function} args.setHighlightedOption
11
+ * @returns {void}
12
+ */
13
+ export const toggleRange = ({
14
+ highlightedOptions,
15
+ options,
16
+ option,
17
+ setHighlightedOptions,
18
+ lastClicked,
19
+ maxSelections,
20
+ }) => {
21
+ if (highlightedOptions.length === 0) {
22
+ setHighlightedOptions([option.value])
23
+ } else {
24
+ let from, to
25
+
26
+ const clickedOptionIndex = findOptionIndex(options, option)
27
+ const lastClickedSourceOptionWithoutRangeModeIndex = lastClicked
28
+ ? options.findIndex((curOption) => curOption.value === lastClicked)
29
+ : -1
30
+
31
+ if (lastClickedSourceOptionWithoutRangeModeIndex !== -1) {
32
+ from = lastClickedSourceOptionWithoutRangeModeIndex
33
+ to = clickedOptionIndex
34
+ } else {
35
+ /**
36
+ * A filter-change has removed the most recently highlighted option
37
+ */
38
+ const firstHighlightedInList = options.findIndex((option) =>
39
+ highlightedOptions.find(
40
+ (highlightedOption) => highlightedOption === option.value
41
+ )
42
+ )
43
+
44
+ from = firstHighlightedInList
45
+ to = clickedOptionIndex
46
+ }
47
+
48
+ // this is so we can also selected
49
+ // a range of options above "from" option.
50
+ // -> Just how slice works ;)
51
+ const lower = Math.min(from, to)
52
+ const higher = Math.max(from, to)
53
+ const newHighlightedSourceOptions = options
54
+ .slice(lower, higher + 1)
55
+ .filter((option) => !option.disabled)
56
+ .slice(maxSelections * -1)
57
+ .map(({ value }) => value)
58
+
59
+ setHighlightedOptions(newHighlightedSourceOptions)
60
+ }
61
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {string[]} args.highlightedOptions
4
+ * @param {Object} args.option
5
+ * @param {Function} args.setHighlightedOption
6
+ * @returns {void}
7
+ */
8
+ export const toggleReplace = ({
9
+ option,
10
+ highlightedOptions,
11
+ setHighlightedOptions,
12
+ }) => {
13
+ if (highlightedOptions.length > 1) {
14
+ setHighlightedOptions([option.value])
15
+ } else {
16
+ const optionIndex = highlightedOptions.findIndex(
17
+ (highlightedOption) => highlightedOption === option.value
18
+ )
19
+
20
+ if (optionIndex === -1) {
21
+ setHighlightedOptions([option.value])
22
+ } else {
23
+ setHighlightedOptions([])
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,34 @@
1
+ import { useState } from 'react'
2
+ import { createToggleHighlightedOption } from './use-highlighted-options/create-toggle-highlighted-option.js'
3
+
4
+ /**
5
+ * @param {Object} args
6
+ * @param {bool} args.disabled
7
+ * @param {number} args.maxSelection
8
+ * @param {Object[]} args.options
9
+ * @returns {Object} highlighted options & helpers
10
+ */
11
+ export const useHighlightedOptions = ({ disabled, maxSelections, options }) => {
12
+ /**
13
+ * These are important so the stored element can be used
14
+ * as range-start when using shift multiple times consecutively
15
+ */
16
+ const [lastClicked, setLastClicked] = useState(null)
17
+ const [highlightedOptions, setHighlightedOptions] = useState([])
18
+
19
+ const toggleHighlightedOption = createToggleHighlightedOption({
20
+ disabled,
21
+ highlightedOptions,
22
+ setHighlightedOptions,
23
+ maxSelections,
24
+ setLastClicked,
25
+ options,
26
+ lastClicked,
27
+ })
28
+
29
+ return {
30
+ highlightedOptions,
31
+ setHighlightedOptions,
32
+ toggleHighlightedOption,
33
+ }
34
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { INTERSECTION_DETECTOR_HEIGHT } from '../end-intersection-detector.js'
3
+
4
+ const isEndIntersectionDetectorWithinScrollBox = (scrollBoxRef, listRef) => {
5
+ if (!scrollBoxRef.current || !listRef.current) {
6
+ return false
7
+ }
8
+ const scrollBoxRect = scrollBoxRef.current.getBoundingClientRect()
9
+ const listRect = listRef.current.getBoundingClientRect()
10
+ return listRect.bottom - scrollBoxRect.bottom < INTERSECTION_DETECTOR_HEIGHT
11
+ }
12
+
13
+ export const useOptionsKeyMonitor = ({
14
+ scrollBoxRef,
15
+ listRef,
16
+ allOptionsKey,
17
+ onEndReached,
18
+ }) => {
19
+ /* Store in ref so this works even if a consumer does not pass a stable
20
+ * function reference */
21
+ const onEndReachedRef = useRef(onEndReached)
22
+ const prevAllOptionsKey = useRef(allOptionsKey)
23
+ onEndReachedRef.current = onEndReached
24
+
25
+ useEffect(() => {
26
+ /* When new options are loaded and the list end is (still) in
27
+ * view we need to call onEndReached, because the end of the list
28
+ * has indeed been reached but the interception detector will not pick
29
+ * up on this */
30
+ if (
31
+ onEndReachedRef.current &&
32
+ prevAllOptionsKey.current !== allOptionsKey &&
33
+ isEndIntersectionDetectorWithinScrollBox(scrollBoxRef, listRef)
34
+ ) {
35
+ onEndReachedRef.current()
36
+ }
37
+ prevAllOptionsKey.current = allOptionsKey
38
+ /* This effect will only run on mount and when allOptionsKey
39
+ * changes because scrollBoxRef, listRef are stable references */
40
+ }, [scrollBoxRef, listRef, allOptionsKey])
41
+ }