Select.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import Loading from "components/Loading";
  2. import React, { useRef, useState } from "react";
  3. import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
  4. import styled from "styled-components";
  5. export type SelectProps<T> = {
  6. value: T;
  7. options: T[];
  8. accessor: (option: T) => string | React.ReactNode;
  9. onChange: (value: T) => void;
  10. isOptionEqualToValue?: (option: T, value: T) => boolean;
  11. label: string;
  12. isLoading?: boolean;
  13. dropdown?: {
  14. maxH?: string;
  15. width?: string;
  16. label?: string;
  17. option?: {
  18. height?: string;
  19. };
  20. };
  21. placeholder: string;
  22. className?: string;
  23. readOnly?: boolean;
  24. };
  25. const Select = <T extends unknown>({
  26. value,
  27. options,
  28. accessor,
  29. onChange,
  30. isOptionEqualToValue,
  31. label,
  32. isLoading,
  33. placeholder,
  34. dropdown,
  35. className,
  36. readOnly,
  37. }: SelectProps<T>) => {
  38. const wrapperRef = useRef();
  39. const [expanded, setExpanded] = useState(false);
  40. useOutsideAlerter(wrapperRef, () => {
  41. setExpanded(false);
  42. });
  43. const handleOptionClick = (value: T) => {
  44. setExpanded(false);
  45. onChange(value);
  46. };
  47. const getLabel = () => {
  48. if (label) {
  49. return <SelectStyles.Label> {label} </SelectStyles.Label>;
  50. }
  51. return null;
  52. };
  53. if (isLoading) {
  54. return (
  55. <div>
  56. {getLabel()}
  57. <SelectStyles.Wrapper>
  58. <SelectStyles.Selector
  59. className={className}
  60. expanded={false}
  61. readOnly={readOnly}
  62. >
  63. <SelectStyles.Loading>
  64. <Loading />
  65. </SelectStyles.Loading>
  66. </SelectStyles.Selector>
  67. </SelectStyles.Wrapper>
  68. </div>
  69. );
  70. }
  71. const isSelected = (option: T, value: T) => {
  72. if (!value) {
  73. return false;
  74. }
  75. if (isOptionEqualToValue) {
  76. return isOptionEqualToValue(option, value);
  77. }
  78. };
  79. return (
  80. <div>
  81. {getLabel()}
  82. <SelectStyles.Wrapper ref={wrapperRef}>
  83. <SelectStyles.Selector
  84. className={className}
  85. onClick={() => setExpanded(!expanded)}
  86. expanded={expanded}
  87. readOnly={readOnly}
  88. >
  89. <SelectStyles.CurrentValue>
  90. <span>{value ? accessor(value) : placeholder}</span>
  91. </SelectStyles.CurrentValue>
  92. {readOnly ? null : <i className="material-icons">arrow_drop_down</i>}
  93. </SelectStyles.Selector>
  94. {expanded && !readOnly ? (
  95. <SelectStyles.Dropdown.Wrapper
  96. width={dropdown?.width}
  97. maxH={dropdown?.maxH}
  98. >
  99. {dropdown?.label && (
  100. <SelectStyles.Dropdown.Label>
  101. {dropdown?.label}
  102. </SelectStyles.Dropdown.Label>
  103. )}
  104. {options.length > 0 ? (
  105. <>
  106. {options.map((option, i) => (
  107. <SelectStyles.Dropdown.Option
  108. key={i}
  109. onClick={() => !readOnly && handleOptionClick(option)}
  110. lastItem={i === options.length - 1}
  111. selected={isSelected(option, value)}
  112. height={dropdown?.option?.height}
  113. >
  114. {accessor(option)}
  115. </SelectStyles.Dropdown.Option>
  116. ))}
  117. </>
  118. ) : (
  119. <SelectStyles.Dropdown.NoOptions>
  120. No options available
  121. </SelectStyles.Dropdown.NoOptions>
  122. )}
  123. </SelectStyles.Dropdown.Wrapper>
  124. ) : null}
  125. </SelectStyles.Wrapper>
  126. </div>
  127. );
  128. };
  129. export default Select;
  130. export const SelectStyles = {
  131. Wrapper: styled.div`
  132. position: relative;
  133. `,
  134. Label: styled.div`
  135. color: #ffffff;
  136. margin-bottom: 10px;
  137. margin-top: 20px;
  138. font-size: 13px;
  139. `,
  140. Selector: styled.div<{ expanded: boolean; readOnly: boolean }>`
  141. height: 35px;
  142. border: 1px solid #ffffff55;
  143. font-size: 13px;
  144. padding: 5px 10px;
  145. padding-left: 15px;
  146. border-radius: 3px;
  147. display: flex;
  148. justify-content: space-between;
  149. align-items: center;
  150. cursor: ${(props) => (props.readOnly ? "normal" : "pointer")};
  151. background: ${(props) => {
  152. if (props.readOnly) {
  153. return "#ffffff55";
  154. }
  155. if (props.expanded) {
  156. return "#ffffff33";
  157. }
  158. return "#ffffff11";
  159. }};
  160. :hover {
  161. background: ${(props) => {
  162. if (props.readOnly) {
  163. return "#ffffff55";
  164. }
  165. if (props.expanded) {
  166. return "#ffffff33";
  167. }
  168. return "#ffffff22";
  169. }};
  170. }
  171. > i {
  172. font-size: 20px;
  173. transform: ${(props) => (props.expanded ? "rotate(180deg)" : "")};
  174. }
  175. `,
  176. Loading: styled.div`
  177. width: 100%;
  178. `,
  179. CurrentValue: styled.div`
  180. display: flex;
  181. align-items: center;
  182. width: 85%;
  183. > span {
  184. white-space: nowrap;
  185. overflow: hidden;
  186. text-overflow: ellipsis;
  187. z-index: 0;
  188. }
  189. `,
  190. Dropdown: {
  191. Wrapper: styled.div<{ width: string; maxH?: string }>`
  192. background: #26282f;
  193. width: ${(props) => props.width || "100%"};
  194. max-height: ${(props) => props.maxH || "300px"};
  195. border-radius: 3px;
  196. z-index: 999;
  197. overflow-y: auto;
  198. margin-bottom: 20px;
  199. box-shadow: 0 8px 20px 0px #00000088;
  200. position: absolute;
  201. `,
  202. Option: styled.div<{
  203. selected: boolean;
  204. lastItem: boolean;
  205. height?: string;
  206. }>`
  207. width: 100%;
  208. border-top: 1px solid #00000000;
  209. border-bottom: 1px solid
  210. ${(props) => (props.lastItem ? "#ffffff00" : "#ffffff15")};
  211. height: ${(props) => props.height || "37px"};
  212. font-size: 13px;
  213. align-items: center;
  214. display: flex;
  215. align-items: center;
  216. padding-left: 15px;
  217. cursor: pointer;
  218. padding-right: 10px;
  219. white-space: nowrap;
  220. overflow: hidden;
  221. text-overflow: ellipsis;
  222. background: ${(props) => (props.selected ? "#ffffff11" : "")};
  223. :hover {
  224. background: #ffffff22;
  225. }
  226. `,
  227. Label: styled.div`
  228. font-size: 13px;
  229. color: #ffffff44;
  230. font-weight: 500;
  231. margin: 10px 13px;
  232. `,
  233. NoOptions: styled.div`
  234. font-size: 13px;
  235. color: #ffffff44;
  236. font-weight: 500;
  237. margin: 10px 13px;
  238. :not(:first-child) {
  239. border-top: 1px solid #ffffff15;
  240. }
  241. `,
  242. },
  243. };