SearchSelector.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import _ from "lodash";
  2. import React, { useMemo, useState } from "react";
  3. import styled from "styled-components";
  4. import Loading from "./Loading";
  5. type Props<T = any> = {
  6. options: T[];
  7. onSelect: (option: T) => void;
  8. label?: string;
  9. dropdownLabel?: string;
  10. getOptionLabel?: (option: T) => string;
  11. filterBy?: ((option: T) => string) | string;
  12. noOptionsText?: string;
  13. dropdownMaxHeight?: string;
  14. renderAddButton?: any;
  15. className?: string;
  16. renderOptionIcon?: (option: T) => React.ReactNode;
  17. placeholder?: string;
  18. showLoading?: boolean;
  19. };
  20. function SearchSelector<O = any>({
  21. options,
  22. onSelect,
  23. label,
  24. dropdownLabel,
  25. getOptionLabel,
  26. filterBy,
  27. noOptionsText,
  28. dropdownMaxHeight,
  29. renderAddButton,
  30. className,
  31. renderOptionIcon,
  32. placeholder = "Find or add a tag...", // legacy value to not break existing code
  33. showLoading = false,
  34. }: Props<O>) {
  35. const [isExpanded, setIsExpanded] = useState(false);
  36. const [filter, setFilter] = useState("");
  37. const handleOptionClick = (e: any, option: any) => {
  38. setIsExpanded(false);
  39. onSelect(option);
  40. setFilter("");
  41. };
  42. const getLabel = (option: any) => {
  43. if (typeof getOptionLabel === "function") {
  44. return getOptionLabel(option);
  45. }
  46. return React.isValidElement(option) ? option : "";
  47. };
  48. const filteredOptions = useMemo(() => {
  49. if (typeof filterBy === "function") {
  50. return options.filter((option) => filterBy(option).includes(filter));
  51. }
  52. if (typeof filterBy === "string") {
  53. return options.filter((option) =>
  54. _.get(option, filterBy).includes(filter)
  55. );
  56. }
  57. return options.filter((option) =>
  58. typeof option === "string" ? option.includes(filter) : true
  59. );
  60. }, [filter, options]);
  61. if (showLoading) {
  62. return (
  63. <>
  64. {label?.length ? <Label>{label}</Label> : null}
  65. <InputWrapper className={className}>
  66. <Loading />
  67. </InputWrapper>
  68. </>
  69. );
  70. }
  71. return (
  72. <>
  73. {label?.length ? <Label>{label}</Label> : null}
  74. <InputWrapper
  75. onBlur={() => {
  76. setIsExpanded(false);
  77. }}
  78. className={className}
  79. >
  80. <Input
  81. value={filter}
  82. placeholder={placeholder}
  83. onClick={(e) => {
  84. setIsExpanded(false);
  85. e.stopPropagation();
  86. setIsExpanded(true);
  87. }}
  88. onChange={(e) => setFilter(e.target.value)}
  89. />
  90. {isExpanded ? (
  91. <DropdownWrapper>
  92. <Dropdown dropdownMaxHeight={dropdownMaxHeight}>
  93. {!filteredOptions.length ? (
  94. <>
  95. {!renderAddButton ? (
  96. <DropdownLabel>
  97. {noOptionsText || "No options available for this filter"}
  98. </DropdownLabel>
  99. ) : (
  100. <div
  101. onMouseDown={(e) => {
  102. e.stopPropagation();
  103. e.preventDefault();
  104. setFilter("");
  105. }}
  106. >
  107. {renderAddButton()}
  108. </div>
  109. )}
  110. </>
  111. ) : (
  112. <>
  113. {renderAddButton && (
  114. <div
  115. onMouseDown={(e) => {
  116. e.stopPropagation();
  117. e.preventDefault();
  118. setFilter("");
  119. }}
  120. >
  121. {renderAddButton()}
  122. </div>
  123. )}
  124. {!renderAddButton && dropdownLabel && (
  125. <DropdownLabel>{dropdownLabel}</DropdownLabel>
  126. )}
  127. {filteredOptions.map((option, i) => (
  128. <Option
  129. key={i}
  130. onMouseDown={(e) => {
  131. e.stopPropagation();
  132. e.preventDefault();
  133. }}
  134. onClick={(e) => handleOptionClick(e, option)}
  135. >
  136. {typeof renderOptionIcon === "function"
  137. ? renderOptionIcon(option)
  138. : null}
  139. {getLabel(option)}
  140. </Option>
  141. ))}
  142. </>
  143. )}
  144. </Dropdown>
  145. </DropdownWrapper>
  146. ) : null}
  147. </InputWrapper>
  148. </>
  149. );
  150. }
  151. export default SearchSelector;
  152. const InputWrapper = styled.div`
  153. display: flex;
  154. margin-bottom: -1px;
  155. align-items: center;
  156. border: 1px solid #ffffff55;
  157. border-radius: 3px;
  158. background: #ffffff11;
  159. position: relative;
  160. width: 100%;
  161. min-height: 37px;
  162. `;
  163. const Input = styled.input`
  164. outline: none;
  165. border: none;
  166. font-size: 13px;
  167. background: none;
  168. color: #ffffff;
  169. padding: 5px 10px;
  170. min-height: 35px;
  171. max-height: 45px;
  172. width: 100%;
  173. `;
  174. const Label = styled.div`
  175. color: #ffffff;
  176. margin-bottom: 10px;
  177. display: flex;
  178. align-items: center;
  179. font-size: 13px;
  180. font-family: "Work Sans", sans-serif;
  181. `;
  182. const DropdownWrapper = styled.div`
  183. position: absolute;
  184. width: 100%;
  185. right: 0;
  186. z-index: 9999;
  187. top: calc(100% + 5px);
  188. `;
  189. const Dropdown = styled.div`
  190. background: #26282f;
  191. max-height: ${(props: { dropdownMaxHeight: string }) =>
  192. props.dropdownMaxHeight || "300px"};
  193. border-radius: 3px;
  194. z-index: 999;
  195. overflow-y: auto;
  196. margin-bottom: 20px;
  197. box-shadow: 0 8px 20px 0px #00000088;
  198. `;
  199. const DropdownLabel = styled.div`
  200. font-size: 13px;
  201. color: #ffffff44;
  202. font-weight: 500;
  203. margin: 10px 13px;
  204. `;
  205. const Option = styled.div`
  206. width: 100%;
  207. border-top: 1px solid #00000000;
  208. border-bottom: 1px solid #ffffff15;
  209. min-height: 35px;
  210. font-size: 13px;
  211. align-items: center;
  212. display: flex;
  213. align-items: center;
  214. padding-left: 15px;
  215. cursor: pointer;
  216. padding-right: 10px;
  217. white-space: nowrap;
  218. overflow: hidden;
  219. text-overflow: ellipsis;
  220. :last-child {
  221. border-bottom: 1px solid #ffffff00;
  222. }
  223. :hover {
  224. background: #ffffff22;
  225. }
  226. `;