You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Select.tsx 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import styled from '@emotion/styled';
  21. import classNames from 'classnames';
  22. import { omit } from 'lodash';
  23. import * as React from 'react';
  24. import ReactSelect, {
  25. components,
  26. GroupTypeBase,
  27. IndicatorProps,
  28. NamedProps,
  29. OptionProps,
  30. OptionTypeBase,
  31. StylesConfig
  32. } from 'react-select';
  33. import AsyncReactSelect, { AsyncProps } from 'react-select/async';
  34. import AsyncCreatableReactSelect, {
  35. Props as AsyncCreatableProps
  36. } from 'react-select/async-creatable';
  37. import { LoadingIndicatorProps } from 'react-select/src/components/indicators';
  38. import { MultiValueRemoveProps } from 'react-select/src/components/MultiValue';
  39. import { colors, others, sizes, zIndexes } from '../../app/theme';
  40. import { ClearButton } from './buttons';
  41. const ArrowSpan = styled.span`
  42. border-color: ${colors.gray52} transparent transparent;
  43. border-style: solid;
  44. border-width: 4px 4px 2px;
  45. display: inline-block;
  46. height: 0;
  47. width: 0;
  48. `;
  49. export interface BasicSelectOption {
  50. label: string;
  51. value: string;
  52. }
  53. interface StyleExtensionProps {
  54. large?: boolean;
  55. }
  56. export function dropdownIndicator<
  57. Option extends OptionTypeBase,
  58. IsMulti extends boolean = false,
  59. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  60. >({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
  61. return <ArrowSpan {...innerProps} />;
  62. }
  63. export function clearIndicator<
  64. Option extends OptionTypeBase,
  65. IsMulti extends boolean = false,
  66. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  67. >({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
  68. return (
  69. <ClearButton
  70. className="button-tiny spacer-left spacer-right text-middle"
  71. iconProps={{ size: 12 }}
  72. {...innerProps}
  73. />
  74. );
  75. }
  76. export function loadingIndicator<
  77. Option extends OptionTypeBase,
  78. IsMulti extends boolean,
  79. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  80. >({ innerProps }: LoadingIndicatorProps<Option, IsMulti, Group>) {
  81. return (
  82. <i className={classNames('deferred-spinner spacer-left spacer-right', innerProps.className)} />
  83. );
  84. }
  85. export function multiValueRemove<
  86. Option extends OptionTypeBase,
  87. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  88. >(props: MultiValueRemoveProps<Option, Group>) {
  89. return <div {...props.innerProps}>×</div>;
  90. }
  91. export type SelectOptionProps<T, IsMulti extends boolean> = OptionProps<T, IsMulti>;
  92. export const SelectOption = components.Option;
  93. /* Keeping it as a class to simplify a dozen tests */
  94. export default class Select<
  95. Option,
  96. IsMulti extends boolean = false,
  97. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  98. > extends React.Component<NamedProps<Option, IsMulti, Group> & StyleExtensionProps> {
  99. render() {
  100. return (
  101. <ReactSelect
  102. {...omit(this.props, 'className', 'large')}
  103. styles={selectStyle<Option, IsMulti, Group>(this.props)}
  104. className={classNames('react-select', this.props.className)}
  105. classNamePrefix="react-select"
  106. components={{
  107. ...this.props.components,
  108. DropdownIndicator: dropdownIndicator,
  109. ClearIndicator: clearIndicator,
  110. MultiValueRemove: multiValueRemove
  111. }}
  112. />
  113. );
  114. }
  115. }
  116. export function CreatableSelect<
  117. Option,
  118. isMulti extends boolean,
  119. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  120. >(props: AsyncCreatableProps<Option, isMulti, Group>) {
  121. return (
  122. <AsyncCreatableReactSelect
  123. {...props}
  124. styles={selectStyle<Option, isMulti, Group>()}
  125. components={{
  126. ...props.components,
  127. DropdownIndicator: dropdownIndicator,
  128. ClearIndicator: clearIndicator,
  129. MultiValueRemove: multiValueRemove,
  130. LoadingIndicator: loadingIndicator
  131. }}
  132. />
  133. );
  134. }
  135. export function SearchSelect<
  136. Option,
  137. IsMulti extends boolean,
  138. Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
  139. >(props: NamedProps<Option, IsMulti, Group> & AsyncProps<Option, Group> & StyleExtensionProps) {
  140. return (
  141. <AsyncReactSelect
  142. {...omit(props, 'className', 'large')}
  143. styles={selectStyle<Option, IsMulti, Group>(props)}
  144. className={classNames('react-select', props.className)}
  145. classNamePrefix="react-select"
  146. components={{
  147. ...props.components,
  148. DropdownIndicator: dropdownIndicator,
  149. ClearIndicator: clearIndicator,
  150. MultiValueRemove: multiValueRemove,
  151. LoadingIndicator: loadingIndicator
  152. }}
  153. />
  154. );
  155. }
  156. export function selectStyle<Option, IsMulti extends boolean, Group extends GroupTypeBase<Option>>(
  157. props?: NamedProps<Option, IsMulti, Group> & AsyncProps<Option, Group> & StyleExtensionProps
  158. ): StylesConfig<Option, IsMulti, Group> {
  159. return {
  160. container: () => ({
  161. position: 'relative',
  162. display: 'inline-block',
  163. verticalAlign: 'middle',
  164. fontSize: '12px',
  165. textAlign: 'left',
  166. width: '100%'
  167. }),
  168. control: (_provided, state) => ({
  169. position: 'relative',
  170. display: 'flex',
  171. width: '100%',
  172. minHeight: `${sizes.controlHeight}`,
  173. lineHeight: `calc(${sizes.controlHeight} - 2px)`,
  174. border: `1px solid ${state.isFocused ? colors.blue : colors.gray80}`,
  175. borderCollapse: 'separate',
  176. borderRadius: '2px',
  177. backgroundColor: state.isDisabled ? colors.disableGrayBg : '#fff',
  178. boxSizing: 'border-box',
  179. color: `${colors.baseFontColor}`,
  180. cursor: 'default',
  181. outline: 'none',
  182. padding: props?.large ? '4px 0px' : '0'
  183. }),
  184. singleValue: () => ({
  185. bottom: 0,
  186. left: 0,
  187. lineHeight: '23px',
  188. padding: props?.large ? '4px 8px' : '0 8px',
  189. paddingLeft: '8px',
  190. paddingRight: '24px',
  191. position: 'absolute',
  192. right: 0,
  193. top: 0,
  194. maxWidth: '100%',
  195. overflow: 'hidden',
  196. textOverflow: 'ellipsis',
  197. whiteSpace: 'nowrap'
  198. }),
  199. valueContainer: (_provided, state) => {
  200. if (state.hasValue && state.isMulti) {
  201. return {
  202. lineHeight: '23px',
  203. paddingLeft: '1px'
  204. };
  205. }
  206. return {
  207. bottom: 0,
  208. left: 0,
  209. lineHeight: '23px',
  210. paddingLeft: '8px',
  211. paddingRight: '24px',
  212. position: 'absolute',
  213. right: 0,
  214. top: 0,
  215. maxWidth: '100%',
  216. overflow: 'hidden',
  217. textOverflow: 'ellipsis',
  218. whiteSpace: 'nowrap',
  219. display: 'flex'
  220. };
  221. },
  222. indicatorsContainer: (_provided, state) => ({
  223. position: 'relative',
  224. cursor: state.isDisabled ? 'default' : 'pointer',
  225. textAlign: 'end',
  226. verticalAlign: 'middle',
  227. width: '20px',
  228. paddingRight: '5px',
  229. flex: 1
  230. }),
  231. multiValue: () => ({
  232. display: 'inline-block',
  233. backgroundColor: 'rgba(0, 126, 255, 0.08)',
  234. borderRadius: '2px',
  235. border: '1px solid rgba(0, 126, 255, 0.24)',
  236. color: '#333',
  237. maxWidth: '200px',
  238. fontSize: '12px',
  239. lineHeight: '14px',
  240. margin: '1px 4px 1px 1px',
  241. verticalAlign: 'top'
  242. }),
  243. multiValueLabel: () => ({
  244. display: 'inline-block',
  245. cursor: 'default',
  246. padding: '2px 5px',
  247. overflow: 'hidden',
  248. marginRight: 'auto',
  249. maxWidth: 'calc(200px - 28px)',
  250. textOverflow: 'ellipsis',
  251. whiteSpace: 'nowrap',
  252. verticalAlign: 'middle'
  253. }),
  254. multiValueRemove: () => ({
  255. order: '-1',
  256. cursor: 'pointer',
  257. borderLeft: '1px solid rgba(0, 126, 255, 0.24)',
  258. verticalAlign: 'middle',
  259. padding: '1px 5px',
  260. fontSize: '12px',
  261. lineHeight: '14px',
  262. display: 'inline-block'
  263. }),
  264. menu: () => ({
  265. borderBottomRightRadius: '4px',
  266. borderBottomLeftRadius: '4px',
  267. backgroundColor: '#fff',
  268. border: '1px solid #ccc',
  269. borderTopColor: `${colors.barBorderColor}`,
  270. boxSizing: 'border-box',
  271. marginTop: '-1px',
  272. maxHeight: '200px',
  273. position: 'absolute',
  274. top: '100%',
  275. width: '100%',
  276. zIndex: `${zIndexes.dropdownMenuZIndex}`,
  277. webkitOverflowScrolling: 'touch',
  278. boxShadow: `${others.defaultShadow}`
  279. }),
  280. menuList: () => ({
  281. boxSizing: 'border-box',
  282. maxHeight: '198px',
  283. padding: '5px 0',
  284. overflowY: 'auto'
  285. }),
  286. placeholder: () => ({
  287. position: 'absolute',
  288. color: '#666'
  289. }),
  290. option: (_provided, state) => ({
  291. display: 'block',
  292. lineHeight: '20px',
  293. padding: props?.large ? '4px 8px' : '0 8px',
  294. boxSizing: 'border-box',
  295. color: state.isDisabled ? colors.disableGrayText : colors.baseFontColor,
  296. backgroundColor: state.isFocused ? colors.barBackgroundColor : colors.white,
  297. fontSize: `${sizes.smallFontSize}`,
  298. cursor: state.isDisabled ? 'default' : 'pointer',
  299. whiteSpace: 'nowrap',
  300. overflow: 'hidden',
  301. textOverflow: 'ellipsis'
  302. }),
  303. input: () => ({
  304. display: 'flex',
  305. alignItems: 'center'
  306. }),
  307. loadingIndicator: () => ({
  308. position: 'absolute',
  309. padding: '8px',
  310. fontSize: '4px'
  311. }),
  312. noOptionsMessage: () => ({
  313. color: `${colors.gray60}`,
  314. padding: '8px 10px'
  315. })
  316. };
  317. }