Select.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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={!readOnly && 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. color: ${props => props.readOnly ? "#ffffff44" : ""};
  145. padding: 5px 10px;
  146. padding-left: 15px;
  147. border-radius: 3px;
  148. display: flex;
  149. justify-content: space-between;
  150. align-items: center;
  151. cursor: ${(props) => (props.readOnly ? "not-allowed" : "pointer")};
  152. background: ${(props) => {
  153. if (props.expanded) {
  154. return "#ffffff33";
  155. }
  156. return "#ffffff11";
  157. }};
  158. :hover {
  159. background: ${(props) => {
  160. if (props.readOnly) {
  161. return "#ffffff11";
  162. } else if (props.expanded) {
  163. return "#ffffff33";
  164. }
  165. return "#ffffff22";
  166. }};
  167. }
  168. > i {
  169. font-size: 20px;
  170. transform: ${(props) => (props.expanded ? "rotate(180deg)" : "")};
  171. }
  172. `,
  173. Loading: styled.div`
  174. width: 100%;
  175. `,
  176. CurrentValue: styled.div`
  177. display: flex;
  178. align-items: center;
  179. width: 85%;
  180. > span {
  181. white-space: nowrap;
  182. overflow: hidden;
  183. text-overflow: ellipsis;
  184. z-index: 0;
  185. }
  186. `,
  187. Dropdown: {
  188. Wrapper: styled.div<{ width: string; maxH?: string }>`
  189. background: #26282f;
  190. width: ${(props) => props.width || "100%"};
  191. max-height: ${(props) => props.maxH || "300px"};
  192. border-radius: 3px;
  193. z-index: 999;
  194. overflow-y: auto;
  195. margin-bottom: 20px;
  196. box-shadow: 0 8px 20px 0px #00000088;
  197. position: absolute;
  198. `,
  199. Option: styled.div<{
  200. selected: boolean;
  201. lastItem: boolean;
  202. height?: string;
  203. }>`
  204. width: 100%;
  205. border-top: 1px solid #00000000;
  206. border-bottom: 1px solid
  207. ${(props) => (props.lastItem ? "#ffffff00" : "#ffffff15")};
  208. height: ${(props) => props.height || "37px"};
  209. font-size: 13px;
  210. align-items: center;
  211. display: flex;
  212. align-items: center;
  213. padding-left: 15px;
  214. cursor: pointer;
  215. padding-right: 10px;
  216. white-space: nowrap;
  217. overflow: hidden;
  218. text-overflow: ellipsis;
  219. background: ${(props) => (props.selected ? "#ffffff11" : "")};
  220. :hover {
  221. background: #ffffff22;
  222. }
  223. `,
  224. Label: styled.div`
  225. font-size: 13px;
  226. color: #ffffff44;
  227. font-weight: 500;
  228. margin: 10px 13px;
  229. `,
  230. NoOptions: styled.div`
  231. font-size: 13px;
  232. color: #ffffff44;
  233. font-weight: 500;
  234. margin: 10px 13px;
  235. :not(:first-child) {
  236. border-top: 1px solid #ffffff15;
  237. }
  238. `,
  239. },
  240. };