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.0KB

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