Browse Source

ref(searchBar): Add support for different input sizes (#37955)

* ref(searchBar): Add support for different input sizes

* ref(searchBar): Address PR comments
Vu Luong 2 years ago
parent
commit
7541656eda

+ 2 - 17
static/app/components/events/interfaces/searchBarAction.tsx

@@ -88,35 +88,20 @@ const Wrapper = styled('div')`
   }
 `;
 
-// TODO(matej): remove this once we refactor SearchBar to not use css classes
-// - it could accept size as a prop
 const StyledSearchBar = styled(SearchBar)<{blendWithFilter?: boolean}>`
   width: 100%;
-  position: relative;
-
-  .search-input {
-    height: 34px;
-  }
-  .search-clear-form,
-  .search-input-icon {
-    height: 32px;
-    display: flex;
-    align-items: center;
-  }
 
   ${p =>
     p.blendWithFilter &&
     `
-      .search-input,
-      .search-input:focus {
+      input {
         border-radius: ${p.theme.borderRadiusRight};
         border-left-width: 0;
       }
     `}
 
   @media (max-width: ${p => p.theme.breakpoints.small}) {
-    .search-input,
-    .search-input:focus {
+    input {
       border-radius: ${p => p.theme.borderRadius};
       border-left-width: 1px;
     }

+ 1 - 4
static/app/components/profiling/flamegraphSearch.tsx

@@ -312,6 +312,7 @@ function FlamegraphSearch({
 
   return (
     <StyledSearchBar
+      size="xs"
       placeholder={t('Find Frames')}
       query={search.query}
       onChange={handleChange}
@@ -321,10 +322,6 @@ function FlamegraphSearch({
 }
 
 const StyledSearchBar = styled(SearchBar)`
-  .search-input {
-    height: 28px;
-  }
-
   flex: 1 1 100%;
 `;
 

+ 103 - 128
static/app/components/searchBar.tsx

@@ -1,160 +1,135 @@
-import {createRef, PureComponent} from 'react';
+import {useCallback, useRef, useState} from 'react';
+import isPropValid from '@emotion/is-prop-valid';
 import styled from '@emotion/styled';
-import classNames from 'classnames';
 
 import Button from 'sentry/components/button';
 import Input, {InputProps} from 'sentry/components/input';
 import {IconSearch} from 'sentry/icons';
 import {IconClose} from 'sentry/icons/iconClose';
 import {t} from 'sentry/locale';
-import {callIfFunction} from 'sentry/utils/callIfFunction';
 
 interface SearchBarProps extends Omit<InputProps, 'onChange'> {
-  defaultQuery: string;
-  onSearch: (query: string) => void;
-  query: string;
+  defaultQuery?: string;
   onChange?: (query: string) => void;
+  onSearch?: (query: string) => void;
+  query?: string;
   width?: string;
 }
 
-type State = {
-  dropdownVisible: boolean;
-  query: string;
-};
-
-class SearchBar extends PureComponent<SearchBarProps, State> {
-  static defaultProps: Pick<SearchBarProps, 'query' | 'defaultQuery' | 'onSearch'> = {
-    query: '',
-    defaultQuery: '',
-    onSearch: function () {},
-  };
-
-  state: State = {
-    query: this.props.query || this.props.defaultQuery,
-    dropdownVisible: false,
-  };
-
-  UNSAFE_componentWillReceiveProps(nextProps: SearchBarProps) {
-    if (nextProps.query !== this.props.query) {
-      this.setState({
-        query: nextProps.query,
-      });
-    }
-  }
-
-  searchInputRef = createRef<HTMLInputElement>();
-
-  blur = () => {
-    if (this.searchInputRef.current) {
-      this.searchInputRef.current.blur();
-    }
-  };
-
-  onSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
-    evt.preventDefault();
-    this.blur();
-    this.props.onSearch(this.state.query);
-  };
-
-  clearSearch = () => {
-    this.setState({query: this.props.defaultQuery}, () => {
-      this.props.onSearch(this.state.query);
-      callIfFunction(this.props.onChange, this.state.query);
-    });
-  };
-
-  onQueryFocus = () => {
-    this.setState({
-      dropdownVisible: true,
-    });
-  };
-
-  onQueryBlur = () => {
-    this.setState({dropdownVisible: false});
-  };
-
-  onQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
-    const {value} = evt.target;
-
-    this.setState({query: value});
-    callIfFunction(this.props.onChange, value);
-  };
-
-  render() {
-    // Remove keys that should not be passed into Input
-    const {
-      className,
-      width,
-      query: _q,
-      defaultQuery,
-      onChange: _oC,
-      onSearch: _oS,
-      ...inputProps
-    } = this.props;
-
-    return (
-      <div className={classNames('search', className)}>
-        <form className="form-horizontal" onSubmit={this.onSubmit}>
-          <div>
-            <StyledInput
-              {...inputProps}
-              type="text"
-              className="search-input"
-              name="query"
-              ref={this.searchInputRef}
-              autoComplete="off"
-              value={this.state.query}
-              onBlur={this.onQueryBlur}
-              onChange={this.onQueryChange}
-              width={width}
-            />
-            <StyledIconSearch className="search-input-icon" size="sm" color="gray300" />
-            {this.state.query !== defaultQuery && (
-              <SearchClearButton
-                type="button"
-                className="search-clear-form"
-                priority="link"
-                onClick={this.clearSearch}
-                size="xs"
-                icon={<IconClose />}
-                aria-label={t('Clear')}
-              />
-            )}
-          </div>
-        </form>
-      </div>
-    );
-  }
+function SearchBar({
+  query: queryProp,
+  defaultQuery = '',
+  onChange,
+  onSearch,
+  width,
+  size,
+  className,
+  ...inputProps
+}: SearchBarProps) {
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  const [query, setQuery] = useState(queryProp ?? defaultQuery);
+
+  const onQueryChange = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      const {value} = e.target;
+      setQuery(value);
+      onChange?.(value);
+    },
+    [onChange]
+  );
+
+  const onSubmit = useCallback(
+    (e: React.FormEvent<HTMLFormElement>) => {
+      e.preventDefault();
+      inputRef.current?.blur();
+      onSearch?.(query);
+    },
+    [onSearch, query]
+  );
+
+  const clearSearch = useCallback(() => {
+    setQuery('');
+    onChange?.('');
+    onSearch?.('');
+  }, [onChange, onSearch]);
+
+  return (
+    <FormWrap onSubmit={onSubmit} className={className}>
+      <StyledInput
+        {...inputProps}
+        ref={inputRef}
+        type="text"
+        name="query"
+        autoComplete="off"
+        value={query}
+        onChange={onQueryChange}
+        width={width}
+        size={size}
+        showClearButton={!!query}
+      />
+      <StyledIconSearch
+        color="subText"
+        size={size === 'xs' ? 'xs' : 'sm'}
+        inputSize={size}
+      />
+      {!!query && (
+        <SearchClearButton
+          type="button"
+          priority="link"
+          onClick={clearSearch}
+          size="xs"
+          icon={<IconClose size="xs" />}
+          aria-label={t('Clear')}
+          inputSize={size}
+        />
+      )}
+    </FormWrap>
+  );
 }
 
-const StyledInput = styled(Input)`
-  width: ${p => (p.width ? p.width : undefined)};
+const FormWrap = styled('form')`
+  display: block;
+  position: relative;
+`;
 
-  &.focus-visible {
-    box-shadow: 0 0 0 1px ${p => p.theme.focusBorder};
-    border-color: ${p => p.theme.focusBorder};
-    outline: none;
-  }
+const StyledInput = styled(Input)<{showClearButton: boolean}>`
+  width: ${p => (p.width ? p.width : undefined)};
+  padding-left: ${p => `calc(
+    ${p.theme.formPadding[p.size ?? 'md'].paddingLeft}px * 1.5 +
+    ${p.theme.iconSizes.sm}
+  )`};
+
+  ${p =>
+    p.showClearButton &&
+    `
+      padding-right: calc(
+        ${p.theme.formPadding[p.size ?? 'md'].paddingRight}px * 1.5 +
+        ${p.theme.iconSizes.xs}
+      );
+    `}
 `;
 
-const StyledIconSearch = styled(IconSearch)`
+const StyledIconSearch = styled(IconSearch, {
+  shouldForwardProp: prop => typeof prop === 'string' && isPropValid(prop),
+})<{inputSize: InputProps['size']}>`
   position: absolute;
   top: 50%;
+  left: ${p => p.theme.formPadding[p.inputSize ?? 'md'].paddingLeft}px;
   transform: translateY(-50%);
-  left: 14px;
+  pointer-events: none;
 `;
 
-const SearchClearButton = styled(Button)`
+const SearchClearButton = styled(Button)<{inputSize: InputProps['size']}>`
   position: absolute;
   top: 50%;
-  height: 16px;
   transform: translateY(-50%);
-  right: 10px;
+  right: ${p => p.theme.formPadding[p.inputSize ?? 'md'].paddingRight}px;
   font-size: ${p => p.theme.fontSizeLarge};
-  color: ${p => p.theme.gray200};
+  color: ${p => p.theme.subText};
 
   &:hover {
-    color: ${p => p.theme.gray300};
+    color: ${p => p.theme.textColor};
   }
 `;
 

+ 0 - 4
static/app/styles/global.tsx

@@ -196,10 +196,6 @@ const styles = (theme: Theme, isDark: boolean) => css`
         .nav-header span.help-link a {
           color: ${theme.subText};
         }
-        .search .search-input {
-          background: ${theme.background};
-          color: ${theme.formText};
-        }
 
         /* Global Selection header date picker */
         .rdrCalendarWrapper {

+ 0 - 42
static/less/shared-components.less

@@ -509,48 +509,6 @@ table.table.key-value {
   }
 }
 
-/**
-* Search
-* ============================================================================
-*/
-
-.search {
-  form {
-    display: block;
-    position: relative;
-  }
-
-  .search-input {
-    // Match the height of buttons adjacent to the search input.
-    height: 40px;
-    padding: 8px 34px 8px 37px;
-    font-size: 14px;
-    background: #fff;
-    transition: none;
-  }
-
-  &.disabled {
-    position: relative;
-
-    .search-input {
-      border: 1px solid lighten(@trim, 4);
-      background: @white-dark;
-      color: @gray-light;
-      box-shadow: none;
-    }
-
-    &:after {
-      display: block;
-      content: '';
-      position: absolute;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-    }
-  }
-}
-
 // Sparkline grid
 
 .innerColumn(@columnSpan: 1) {