SearchInput.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { InputAdornment, makeStyles, TextField } from '@material-ui/core';
  2. import { useRef, FC, useState, useEffect, useMemo } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { useHistory } from 'react-router-dom';
  5. import Icons from '../icons/Icons';
  6. import { SearchType } from './Types';
  7. const useSearchStyles = makeStyles(theme => ({
  8. wrapper: {
  9. display: 'flex',
  10. },
  11. input: {
  12. backgroundColor: '#fff',
  13. borderRadius: '4px',
  14. padding: theme.spacing(1),
  15. width: '240px',
  16. border: '1px solid #e9e9ed',
  17. fontSize: '14px',
  18. transition: 'all 0.2s',
  19. '& .MuiAutocomplete-endAdornment': {
  20. right: theme.spacing(0.5),
  21. },
  22. '& .MuiInput-underline:before': {
  23. border: 'none',
  24. },
  25. '& .MuiInput-underline:after': {
  26. border: 'none',
  27. },
  28. /**
  29. * when input focus
  30. * 1. change parent wrapper border color
  31. * 2. hide input start search icon
  32. */
  33. '&:focus-within': {
  34. border: `1px solid ${theme.palette.primary.main}`,
  35. '& $searchIcon': {
  36. width: 0,
  37. },
  38. },
  39. },
  40. textfield: {
  41. padding: 0,
  42. height: '16px',
  43. '&:focus': {
  44. caretColor: theme.palette.primary.main,
  45. },
  46. },
  47. searchIcon: {
  48. color: '#aeaebb',
  49. cursor: 'pointer',
  50. fontSize: '20px',
  51. width: (props: { searched: boolean }) => `${props.searched ? 0 : '20px'}`,
  52. transition: 'width 0.2s',
  53. },
  54. clearIcon: {
  55. color: theme.palette.primary.main,
  56. cursor: 'pointer',
  57. },
  58. iconWrapper: {
  59. opacity: (props: { searched: boolean }) => `${props.searched ? 1 : 0}`,
  60. transition: 'opacity 0.2s',
  61. },
  62. searchWrapper: {
  63. display: 'flex',
  64. justifyContent: 'center',
  65. alignItems: 'center',
  66. },
  67. }));
  68. let timer: NodeJS.Timeout | null = null;
  69. const SearchInput: FC<SearchType> = props => {
  70. const { searchText = '', onClear = () => {}, onSearch = () => {} } = props;
  71. const [searchValue, setSearchValue] = useState<string | null>(
  72. searchText || null
  73. );
  74. const [isInit, setIsInit] = useState<boolean>(true);
  75. const searched = useMemo(
  76. () => searchValue !== '' && searchValue !== null,
  77. [searchValue]
  78. );
  79. const classes = useSearchStyles({ searched });
  80. const { t: commonTrans } = useTranslation();
  81. const history = useHistory();
  82. const inputRef = useRef<any>(null);
  83. const savedSearchFn = useRef<(value: string) => void>(() => {});
  84. useEffect(() => {
  85. savedSearchFn.current = onSearch;
  86. }, [onSearch]);
  87. useEffect(() => {
  88. if (timer) {
  89. clearTimeout(timer);
  90. }
  91. if (searchValue !== null && !isInit) {
  92. timer = setTimeout(() => {
  93. // save other params data and remove last time search info
  94. const location = history.location;
  95. const params = new URLSearchParams(location.search);
  96. params.delete('search');
  97. if (searchValue) {
  98. params.append('search', searchValue);
  99. }
  100. // add search value in url
  101. history.push({ search: params.toString() });
  102. savedSearchFn.current(searchValue);
  103. }, 300);
  104. }
  105. return () => {
  106. timer && clearTimeout(timer);
  107. };
  108. }, [searchValue, history, isInit]);
  109. const handleSearch = (value: string | null) => {
  110. if (value !== null) {
  111. onSearch(value);
  112. }
  113. };
  114. return (
  115. <div className={classes.wrapper}>
  116. <TextField
  117. inputRef={inputRef}
  118. variant="standard"
  119. classes={{ root: classes.input }}
  120. InputProps={{
  121. disableUnderline: true,
  122. classes: { input: classes.textfield },
  123. endAdornment: (
  124. <InputAdornment position="end">
  125. <span
  126. data-testid="clear-icon"
  127. className={`flex-center ${classes.iconWrapper}`}
  128. onClick={e => {
  129. setSearchValue('');
  130. setIsInit(false);
  131. inputRef.current.focus();
  132. onClear();
  133. }}
  134. >
  135. {Icons.clear({ classes: { root: classes.clearIcon } })}
  136. </span>
  137. </InputAdornment>
  138. ),
  139. startAdornment: (
  140. <InputAdornment position="start">
  141. <span
  142. className={classes.searchWrapper}
  143. onClick={() => handleSearch(searchValue)}
  144. >
  145. {Icons.search({ classes: { root: classes.searchIcon } })}
  146. </span>
  147. </InputAdornment>
  148. ),
  149. }}
  150. onChange={e => {
  151. const value = e.target.value.trim();
  152. setSearchValue(value);
  153. setIsInit(false);
  154. if (value === '') {
  155. onClear();
  156. }
  157. }}
  158. onKeyPress={e => {
  159. if (e.key === 'Enter') {
  160. // Do code here
  161. handleSearch(searchValue);
  162. e.preventDefault();
  163. }
  164. }}
  165. value={searchValue || ''}
  166. placeholder={commonTrans('search')}
  167. />
  168. </div>
  169. );
  170. };
  171. export default SearchInput;