searchBar.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import {useCallback, useRef, useState} from 'react';
  2. import isPropValid from '@emotion/is-prop-valid';
  3. import styled from '@emotion/styled';
  4. import Button from 'sentry/components/button';
  5. import Input, {InputProps} from 'sentry/components/input';
  6. import {IconSearch} from 'sentry/icons';
  7. import {IconClose} from 'sentry/icons/iconClose';
  8. import {t} from 'sentry/locale';
  9. interface SearchBarProps extends Omit<InputProps, 'onChange'> {
  10. defaultQuery?: string;
  11. onChange?: (query: string) => void;
  12. onSearch?: (query: string) => void;
  13. query?: string;
  14. width?: string;
  15. }
  16. function SearchBar({
  17. query: queryProp,
  18. defaultQuery = '',
  19. onChange,
  20. onSearch,
  21. width,
  22. size,
  23. className,
  24. ...inputProps
  25. }: SearchBarProps) {
  26. const inputRef = useRef<HTMLInputElement>(null);
  27. const [query, setQuery] = useState(queryProp ?? defaultQuery);
  28. const onQueryChange = useCallback(
  29. (e: React.ChangeEvent<HTMLInputElement>) => {
  30. const {value} = e.target;
  31. setQuery(value);
  32. onChange?.(value);
  33. },
  34. [onChange]
  35. );
  36. const onSubmit = useCallback(
  37. (e: React.FormEvent<HTMLFormElement>) => {
  38. e.preventDefault();
  39. inputRef.current?.blur();
  40. onSearch?.(query);
  41. },
  42. [onSearch, query]
  43. );
  44. const clearSearch = useCallback(() => {
  45. setQuery('');
  46. onChange?.('');
  47. onSearch?.('');
  48. }, [onChange, onSearch]);
  49. return (
  50. <FormWrap onSubmit={onSubmit} className={className}>
  51. <StyledInput
  52. {...inputProps}
  53. ref={inputRef}
  54. type="text"
  55. name="query"
  56. autoComplete="off"
  57. value={query}
  58. onChange={onQueryChange}
  59. width={width}
  60. size={size}
  61. showClearButton={!!query}
  62. />
  63. <StyledIconSearch
  64. color="subText"
  65. size={size === 'xs' ? 'xs' : 'sm'}
  66. inputSize={size}
  67. />
  68. {!!query && (
  69. <SearchClearButton
  70. type="button"
  71. priority="link"
  72. onClick={clearSearch}
  73. size="xs"
  74. icon={<IconClose size="xs" />}
  75. aria-label={t('Clear')}
  76. inputSize={size}
  77. />
  78. )}
  79. </FormWrap>
  80. );
  81. }
  82. const FormWrap = styled('form')`
  83. display: block;
  84. position: relative;
  85. `;
  86. const StyledInput = styled(Input)<{showClearButton: boolean}>`
  87. width: ${p => (p.width ? p.width : undefined)};
  88. padding-left: ${p => `calc(
  89. ${p.theme.formPadding[p.size ?? 'md'].paddingLeft}px * 1.5 +
  90. ${p.theme.iconSizes.sm}
  91. )`};
  92. ${p =>
  93. p.showClearButton &&
  94. `
  95. padding-right: calc(
  96. ${p.theme.formPadding[p.size ?? 'md'].paddingRight}px * 1.5 +
  97. ${p.theme.iconSizes.xs}
  98. );
  99. `}
  100. `;
  101. const StyledIconSearch = styled(IconSearch, {
  102. shouldForwardProp: prop => typeof prop === 'string' && isPropValid(prop),
  103. })<{inputSize: InputProps['size']}>`
  104. position: absolute;
  105. top: 50%;
  106. left: ${p => p.theme.formPadding[p.inputSize ?? 'md'].paddingLeft}px;
  107. transform: translateY(-50%);
  108. pointer-events: none;
  109. `;
  110. const SearchClearButton = styled(Button)<{inputSize: InputProps['size']}>`
  111. position: absolute;
  112. top: 50%;
  113. transform: translateY(-50%);
  114. right: ${p => p.theme.formPadding[p.inputSize ?? 'md'].paddingRight}px;
  115. font-size: ${p => p.theme.fontSizeLarge};
  116. color: ${p => p.theme.subText};
  117. &:hover {
  118. color: ${p => p.theme.textColor};
  119. }
  120. `;
  121. export default SearchBar;