@thecb/components 10.12.0 → 10.12.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.
@@ -0,0 +1,144 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box } from "../../../atoms";
3
+ import FilterableListItem from "./FilterableListItem";
4
+ import useKeyboardNavigation from "./useKeyboardNavigation";
5
+ import {
6
+ sortItemsList,
7
+ filterItemsList,
8
+ selectOption,
9
+ isChecked,
10
+ isMaxSelectionReached
11
+ } from "./util";
12
+ import { GHOST_GREY } from "../../../../constants/colors";
13
+
14
+ const FilterableList = ({
15
+ id,
16
+ options,
17
+ appliedOptions,
18
+ selectedOptions,
19
+ maxSelections,
20
+ name,
21
+ setSelectedOptions,
22
+ searchTerm,
23
+ themeValues
24
+ }) => {
25
+ const [filteredOptions, setFilteredOptions] = useState([]);
26
+ const [filteredAppliedOptions, setFilteredAppliedOptions] = useState([]);
27
+
28
+ useEffect(() => {
29
+ setFilteredOptions(options);
30
+ setFilteredAppliedOptions(appliedOptions);
31
+ }, [options, appliedOptions]);
32
+
33
+ useEffect(() => {
34
+ const filteredOptionItems = filterItemsList(options, searchTerm?.rawValue);
35
+ const filteredAppliedItems = filterItemsList(
36
+ appliedOptions,
37
+ searchTerm?.rawValue
38
+ );
39
+
40
+ setFilteredOptions(
41
+ filteredOptionItems.length ? filteredOptionItems : filteredOptions
42
+ );
43
+ setFilteredAppliedOptions(filteredAppliedItems);
44
+ }, [searchTerm.rawValue]);
45
+
46
+ const handleSelectOption = option =>
47
+ selectOption(option, selectedOptions, setSelectedOptions);
48
+
49
+ const isAppliedOption = option =>
50
+ filteredAppliedOptions?.some(
51
+ appliedItem => appliedItem?.name === option?.name
52
+ );
53
+
54
+ const currentFilteredOptions = filteredOptions.filter(
55
+ option => !isAppliedOption(option)
56
+ );
57
+
58
+ const sortedOptions = sortItemsList(currentFilteredOptions);
59
+ const sortedAppliedOptions = sortItemsList(filteredAppliedOptions);
60
+
61
+ const { itemRefs, focusedIndex, handleKeyDown } = useKeyboardNavigation({
62
+ options: sortedOptions,
63
+ appliedOptions: sortedAppliedOptions,
64
+ selectedOptions,
65
+ maxSelections
66
+ });
67
+
68
+ return (
69
+ <Box
70
+ id={id}
71
+ role="listbox"
72
+ padding="0"
73
+ extraStyles={`
74
+ overflow-y: auto;
75
+ max-height: 250px;
76
+ display: flex;
77
+ flex-flow: column;
78
+ `}
79
+ onKeyDown={handleKeyDown}
80
+ >
81
+ {sortedAppliedOptions?.length > 0 && (
82
+ <Box
83
+ padding="0"
84
+ extraStyles={
85
+ sortedOptions.length > 0 && `border-bottom: 1px solid ${GHOST_GREY}`
86
+ }
87
+ >
88
+ {sortedAppliedOptions.map((option, index) => {
89
+ const checked = isChecked(option, selectedOptions);
90
+ const tabIndex =
91
+ index === focusedIndex || (index === 0 && focusedIndex === -1)
92
+ ? "0"
93
+ : "-1";
94
+ return (
95
+ <FilterableListItem
96
+ key={index}
97
+ ref={el => (itemRefs.current[index] = el)}
98
+ index={index}
99
+ option={option}
100
+ checked={checked}
101
+ selectOption={handleSelectOption}
102
+ tabIndex={tabIndex}
103
+ name={name}
104
+ themeValues={themeValues}
105
+ ></FilterableListItem>
106
+ );
107
+ })}
108
+ </Box>
109
+ )}
110
+ {sortedOptions.map((option, index) => {
111
+ const checked = isChecked(option, selectedOptions);
112
+ const isDisabled =
113
+ isMaxSelectionReached(maxSelections, selectedOptions) && !checked;
114
+ const indexOffset = sortedAppliedOptions?.length
115
+ ? sortedAppliedOptions?.length
116
+ : 0;
117
+ const currentIndex = index === 0 ? indexOffset : index + indexOffset;
118
+ const tabIndex =
119
+ currentIndex === focusedIndex ||
120
+ (indexOffset === 0 &&
121
+ currentIndex === indexOffset &&
122
+ focusedIndex === -1)
123
+ ? "0"
124
+ : "-1";
125
+ return (
126
+ <FilterableListItem
127
+ key={currentIndex}
128
+ ref={el => (itemRefs.current[currentIndex] = el)}
129
+ index={currentIndex}
130
+ option={option}
131
+ checked={checked}
132
+ selectOption={isDisabled ? noop : handleSelectOption}
133
+ disabled={isDisabled}
134
+ tabIndex={tabIndex}
135
+ name={name}
136
+ themeValues={themeValues}
137
+ ></FilterableListItem>
138
+ );
139
+ })}
140
+ </Box>
141
+ );
142
+ };
143
+
144
+ export default React.memo(FilterableList);
@@ -0,0 +1,67 @@
1
+ import React, { forwardRef } from "react";
2
+ import Checkbox from "../../../atoms/checkbox";
3
+ import { Box } from "../../../atoms";
4
+
5
+ const FilterableListItem = forwardRef(
6
+ (
7
+ {
8
+ index,
9
+ option,
10
+ checked,
11
+ selectOption,
12
+ disabled,
13
+ tabIndex,
14
+ name,
15
+ themeValues
16
+ },
17
+ ref
18
+ ) => {
19
+ return (
20
+ <Box
21
+ padding="0"
22
+ key={index}
23
+ extraStyles={`
24
+ :hover,
25
+ :active,
26
+ :focus {
27
+ background-color: ${themeValues.primaryColor};
28
+ }
29
+ `}
30
+ >
31
+ <Checkbox
32
+ ref={ref}
33
+ title={option.name}
34
+ name={option.name}
35
+ checked={checked}
36
+ onChange={() => selectOption(option)}
37
+ textExtraStyles={`font-size: 0.875rem; margin: 0;`}
38
+ disabled={disabled}
39
+ extraStyles={`
40
+ padding: 0.075rem 0.325rem;
41
+ margin: 0;
42
+ :hover,
43
+ :active,
44
+ :focus {
45
+ background-color: ${themeValues.primaryColor};
46
+ }
47
+ `}
48
+ checkboxMargin="0.3rem"
49
+ role="option"
50
+ checkboxExtraStyles={`
51
+ width: 1.375rem;
52
+ height: 1.375rem;
53
+ ${
54
+ checked && !disabled
55
+ ? `background: ` + themeValues.secondaryColor + `;`
56
+ : ""
57
+ }
58
+ `}
59
+ tabIndex={tabIndex}
60
+ dataQa={`${name}-option-${index}`}
61
+ />
62
+ </Box>
63
+ );
64
+ }
65
+ );
66
+
67
+ export default FilterableListItem;
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { Box, FormInput } from "../../../atoms";
3
+ import { GHOST_GREY } from "../../../../constants/colors";
4
+
5
+ const SearchBox = ({
6
+ autocompleteValue,
7
+ fields,
8
+ actions,
9
+ placeholder,
10
+ disabled,
11
+ showSearchBox
12
+ }) => {
13
+ return (
14
+ <Box padding="0 0 0.5rem">
15
+ {showSearchBox && (
16
+ <FormInput
17
+ autocompleteValue={autocompleteValue}
18
+ showFieldErrorRow={false}
19
+ errorMessages={{}}
20
+ field={fields.searchTerm}
21
+ fieldActions={actions.fields.searchTerm}
22
+ placeholder={placeholder}
23
+ disabled={disabled}
24
+ extraStyles={`
25
+ height: 2.875rem;
26
+ border: 0;
27
+ border-radius: 0;
28
+ padding: 0.45rem;
29
+ font-size: 0.875rem;
30
+ border-bottom: 1px solid ${GHOST_GREY};
31
+ `}
32
+ />
33
+ )}
34
+ </Box>
35
+ );
36
+ };
37
+
38
+ export default SearchBox;
@@ -0,0 +1,84 @@
1
+ import React, { useRef, useState, useEffect } from "react";
2
+ import { isMaxSelectionReached } from "./util";
3
+
4
+ const useKeyboardNavigation = ({
5
+ options,
6
+ appliedOptions,
7
+ selectedOptions,
8
+ maxSelections
9
+ }) => {
10
+ const [focusedIndex, setFocusedIndex] = useState(-1);
11
+ const itemRefs = useRef([]);
12
+ const totalItemsLength = options.length + appliedOptions.length;
13
+
14
+ const handleArrowUp = event => {
15
+ event.preventDefault();
16
+ setFocusedIndex(prevIndex =>
17
+ prevIndex > 0 ? prevIndex - 1 : totalItemsLength - 1
18
+ );
19
+ };
20
+
21
+ const handleArrowDown = event => {
22
+ event.preventDefault();
23
+ setFocusedIndex(prevIndex =>
24
+ prevIndex < totalItemsLength - 1 ? prevIndex + 1 : 0
25
+ );
26
+ };
27
+
28
+ const handleSpacebar = event => {
29
+ event.preventDefault();
30
+ const validFocusedIndex = focusedIndex < 0 ? 0 : focusedIndex;
31
+ // Select option on spacebar press if the maximum selection hasn't been reached.
32
+ if (
33
+ !isMaxSelectionReached(maxSelections, selectedOptions) &&
34
+ itemRefs.current &&
35
+ itemRefs.current[validFocusedIndex]
36
+ ) {
37
+ const nestedInput = itemRefs.current[validFocusedIndex].querySelector(
38
+ "input"
39
+ );
40
+ if (nestedInput) {
41
+ nestedInput.click();
42
+ }
43
+ }
44
+ };
45
+
46
+ const handleTab = event => {
47
+ // Reset focus when tabbing out of the list.
48
+ setFocusedIndex(-1);
49
+ };
50
+
51
+ const keyActions = {
52
+ " ": event => handleSpacebar(event),
53
+ Space: event => handleSpacebar(event),
54
+ Tab: event => handleTab(event),
55
+ ArrowUp: event => handleArrowUp(event),
56
+ ArrowDown: event => handleArrowDown(event)
57
+ };
58
+
59
+ const handleKeyDown = event => {
60
+ const eventKey = event.code || event.key;
61
+ const action = keyActions[eventKey];
62
+ if (action) {
63
+ action(event);
64
+ }
65
+ };
66
+
67
+ useEffect(() => {
68
+ if (
69
+ focusedIndex !== -1 &&
70
+ itemRefs.current &&
71
+ itemRefs.current[focusedIndex]
72
+ ) {
73
+ itemRefs.current[focusedIndex].focus(); // move focus to the active option
74
+ }
75
+ }, [focusedIndex]);
76
+
77
+ return {
78
+ itemRefs,
79
+ focusedIndex,
80
+ handleKeyDown
81
+ };
82
+ };
83
+
84
+ export default useKeyboardNavigation;
@@ -0,0 +1,31 @@
1
+ export const filterItemsList = (list, searchTerm) =>
2
+ list.filter(item =>
3
+ item?.name?.toLowerCase().includes(searchTerm?.toLowerCase())
4
+ );
5
+
6
+ export const sortItemsList = list =>
7
+ list
8
+ .slice()
9
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
10
+
11
+ export const isMaxSelectionReached = (maxSelection, selectedOptions) =>
12
+ maxSelection && maxSelection === selectedOptions?.length;
13
+
14
+ export const isChecked = (option, selectedOptions) =>
15
+ selectedOptions?.some(
16
+ selectedOption => selectedOption?.name === option?.name
17
+ );
18
+
19
+ export const selectValues = items => items.map(item => item.value);
20
+
21
+ export const selectOption = (option, selectedOptions, setSelectedOptions) => {
22
+ if (selectValues(selectedOptions).includes(option.value)) {
23
+ const fewerOptions = selectedOptions.filter(
24
+ selectedOption => selectedOption.value !== option.value
25
+ );
26
+ setSelectedOptions(fewerOptions);
27
+ } else {
28
+ const moreOptions = selectedOptions.concat(option);
29
+ setSelectedOptions(moreOptions);
30
+ }
31
+ };
@@ -18,12 +18,11 @@ const useOutsideClickHook = handler => {
18
18
  if (ref.current && !ref.current.contains(e.target)) {
19
19
  handler();
20
20
  }
21
+ };
21
22
 
22
- document.addEventListener("click", handleOutsideClick, true);
23
-
24
- return () => {
25
- document.removeEventListener("click", handleOutsideClick, true);
26
- };
23
+ document.addEventListener("click", handleOutsideClick, true);
24
+ return () => {
25
+ document.removeEventListener("click", handleOutsideClick, true);
27
26
  };
28
27
  }, [ref]);
29
28
 
Binary file