searchBar.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import {useCallback, useEffect, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {InputGroup, InputProps} from 'sentry/components/inputGroup';
  5. import {IconSearch} from 'sentry/icons';
  6. import {IconClose} from 'sentry/icons/iconClose';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. interface SearchBarProps extends Omit<InputProps, 'onChange'> {
  10. defaultQuery?: string;
  11. onChange?: (query: string) => void;
  12. onSearch?: (query: string) => void;
  13. query?: string;
  14. trailing?: React.ReactNode;
  15. width?: string;
  16. }
  17. function SearchBar({
  18. query: queryProp,
  19. defaultQuery = '',
  20. onChange,
  21. onSearch,
  22. width,
  23. size,
  24. className,
  25. trailing,
  26. ...inputProps
  27. }: SearchBarProps) {
  28. const inputRef = useRef<HTMLInputElement>(null);
  29. const [query, setQuery] = useState(queryProp ?? defaultQuery);
  30. // if query prop keeps changing we should treat this as
  31. // a controlled component and its internal state should be in sync
  32. useEffect(() => {
  33. if (typeof queryProp === 'string') {
  34. setQuery(queryProp);
  35. }
  36. }, [queryProp]);
  37. const onQueryChange = useCallback(
  38. (e: React.ChangeEvent<HTMLInputElement>) => {
  39. const {value} = e.target;
  40. setQuery(value);
  41. onChange?.(value);
  42. },
  43. [onChange]
  44. );
  45. const onSubmit = useCallback(
  46. (e: React.FormEvent<HTMLFormElement>) => {
  47. e.preventDefault();
  48. inputRef.current?.blur();
  49. onSearch?.(query);
  50. },
  51. [onSearch, query]
  52. );
  53. const clearSearch = useCallback(() => {
  54. setQuery('');
  55. onChange?.('');
  56. onSearch?.('');
  57. }, [onChange, onSearch]);
  58. return (
  59. <FormWrap onSubmit={onSubmit} className={className}>
  60. <InputGroup>
  61. <InputGroup.LeadingItems disablePointerEvents>
  62. <IconSearch color="subText" size={size === 'xs' ? 'xs' : 'sm'} />
  63. </InputGroup.LeadingItems>
  64. <StyledInput
  65. {...inputProps}
  66. ref={inputRef}
  67. type="text"
  68. name="query"
  69. autoComplete="off"
  70. value={query}
  71. onChange={onQueryChange}
  72. width={width}
  73. size={size}
  74. />
  75. <InputGroup.TrailingItems>
  76. {trailing}
  77. {!!query && (
  78. <SearchBarTrailingButton
  79. size="zero"
  80. borderless
  81. onClick={clearSearch}
  82. icon={<IconClose size="xs" />}
  83. aria-label={t('Clear')}
  84. />
  85. )}
  86. </InputGroup.TrailingItems>
  87. </InputGroup>
  88. </FormWrap>
  89. );
  90. }
  91. const FormWrap = styled('form')`
  92. display: block;
  93. position: relative;
  94. `;
  95. const StyledInput = styled(InputGroup.Input)`
  96. ${p => p.width && `width: ${p.width};`}
  97. `;
  98. export const SearchBarTrailingButton = styled(Button)`
  99. color: ${p => p.theme.subText};
  100. padding: ${space(0.5)};
  101. `;
  102. export default SearchBar;