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.

MultiSelect.tsx 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import React, { useCallback, useMemo, useRef } from 'react';
  2. import { makeStyles } from 'tss-react/mui';
  3. import { IconCloseLarge } from '../../../icons/svg';
  4. import { withPixelLineHeight } from '../../../styles/functions.web';
  5. import { MultiSelectItem } from '../types';
  6. import ClickableIcon from './ClickableIcon';
  7. import Input from './Input';
  8. interface IProps {
  9. autoFocus?: boolean;
  10. disabled?: boolean;
  11. error?: boolean;
  12. errorDialog?: JSX.Element | null;
  13. filterValue?: string;
  14. id: string;
  15. isOpen?: boolean;
  16. items: MultiSelectItem[];
  17. noMatchesText?: string;
  18. onFilterChange?: (value: string) => void;
  19. onRemoved: (item: any) => void;
  20. onSelected: (item: any) => void;
  21. placeholder?: string;
  22. selectedItems?: MultiSelectItem[];
  23. }
  24. const MULTI_SELECT_HEIGHT = 200;
  25. const useStyles = makeStyles()(theme => {
  26. return {
  27. container: {
  28. position: 'relative'
  29. },
  30. items: {
  31. '&.found': {
  32. position: 'absolute',
  33. boxShadow: '0px 5px 10px rgba(0, 0, 0, 0.75)'
  34. },
  35. marginTop: theme.spacing(2),
  36. width: '100%',
  37. backgroundColor: theme.palette.ui01,
  38. border: `1px solid ${theme.palette.ui04}`,
  39. borderRadius: `${Number(theme.shape.borderRadius)}px`,
  40. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  41. zIndex: 2,
  42. maxHeight: `${MULTI_SELECT_HEIGHT}px`,
  43. overflowY: 'auto',
  44. padding: '0'
  45. },
  46. listItem: {
  47. boxSizing: 'border-box',
  48. display: 'flex',
  49. padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
  50. alignItems: 'center',
  51. '& .content': {
  52. // 38px because of the icon before the content
  53. inlineSize: 'calc(100% - 38px)',
  54. overflowWrap: 'break-word',
  55. marginLeft: theme.spacing(2),
  56. color: theme.palette.text01,
  57. '&.with-remove': {
  58. // 60px because of the icon before the content and the remove button
  59. inlineSize: 'calc(100% - 60px)',
  60. marginRight: theme.spacing(2),
  61. '&.without-before': {
  62. marginLeft: 0,
  63. inlineSize: 'calc(100% - 38px)'
  64. }
  65. },
  66. '&.without-before': {
  67. marginLeft: 0,
  68. inlineSize: '100%'
  69. }
  70. },
  71. '&.found': {
  72. cursor: 'pointer',
  73. padding: `10px ${theme.spacing(3)}`,
  74. '&:hover': {
  75. backgroundColor: theme.palette.ui02
  76. }
  77. },
  78. '&.disabled': {
  79. cursor: 'not-allowed',
  80. '&:hover': {
  81. backgroundColor: theme.palette.ui01
  82. },
  83. color: theme.palette.text03
  84. }
  85. },
  86. errorMessage: {
  87. position: 'absolute',
  88. marginTop: theme.spacing(2),
  89. width: '100%'
  90. }
  91. };
  92. });
  93. const MultiSelect = ({
  94. autoFocus,
  95. disabled,
  96. error,
  97. errorDialog,
  98. placeholder,
  99. id,
  100. items,
  101. filterValue,
  102. onFilterChange,
  103. isOpen,
  104. noMatchesText,
  105. onSelected,
  106. selectedItems,
  107. onRemoved
  108. }: IProps) => {
  109. const { classes } = useStyles();
  110. const inputRef = useRef();
  111. const selectItem = useCallback(item => () => onSelected(item), [ onSelected ]);
  112. const removeItem = useCallback(item => () => onRemoved(item), [ onRemoved ]);
  113. const foundItems = useMemo(() => (
  114. <div className = { `${classes.items} found` }>
  115. {
  116. items.length > 0
  117. ? items.map(item => (
  118. <div
  119. className = { `${classes.listItem} ${item.isDisabled ? 'disabled' : ''} found` }
  120. key = { item.value }
  121. onClick = { item.isDisabled ? undefined : selectItem(item) }>
  122. {item.elemBefore}
  123. <div className = { `content ${item.elemBefore ? '' : 'without-before'}` }>
  124. {item.content}
  125. {item.description && <p>{item.description}</p>}
  126. </div>
  127. </div>
  128. ))
  129. : <div className = { classes.listItem }>{noMatchesText}</div>
  130. }
  131. </div>
  132. ), [ items ]);
  133. const errorMessageDialog = useMemo(() =>
  134. error && <div className = { classes.errorMessage }>
  135. { errorDialog }
  136. </div>, [ error ]);
  137. return (
  138. <div className = { classes.container }>
  139. <Input
  140. autoFocus = { autoFocus }
  141. disabled = { disabled }
  142. id = { id }
  143. onChange = { onFilterChange }
  144. placeholder = { placeholder }
  145. ref = { inputRef }
  146. value = { filterValue ?? '' } />
  147. {isOpen && foundItems}
  148. { errorMessageDialog }
  149. { selectedItems && selectedItems?.length > 0 && (
  150. <div className = { classes.items }>
  151. { selectedItems.map(item => (
  152. <div
  153. className = { `${classes.listItem} ${item.isDisabled ? 'disabled' : ''}` }
  154. key = { item.value }>
  155. {item.elemBefore}
  156. <div className = { `content with-remove ${item.elemBefore ? '' : 'without-before'}` }>
  157. <p>{item.content}</p>
  158. </div>
  159. <ClickableIcon
  160. accessibilityLabel = { 'multi-select-unselect' }
  161. icon = { IconCloseLarge }
  162. id = 'modal-header-close-button'
  163. onClick = { removeItem(item) } />
  164. </div>
  165. ))}
  166. </div>
  167. )}
  168. </div>
  169. );
  170. };
  171. export default MultiSelect;