Browse Source

FE: Add a clear button to the search component (#3634)

David Bejanyan 2 years ago
parent
commit
db86942e47

+ 10 - 0
kafka-ui-react-app/src/components/common/Input/Input.styled.ts

@@ -29,6 +29,16 @@ export const Wrapper = styled.div`
     width: 16px;
     width: 16px;
     fill: ${({ theme }) => theme.input.icon.color};
     fill: ${({ theme }) => theme.input.icon.color};
   }
   }
+  svg:last-child {
+    position: absolute;
+    top: 8px;
+    line-height: 0;
+    z-index: 1;
+    left: unset;
+    right: 12px;
+    height: 16px;
+    width: 16px;
+  }
 `;
 `;
 
 
 export const Input = styled.input<InputProps>(
 export const Input = styled.input<InputProps>(

+ 21 - 15
kafka-ui-react-app/src/components/common/Input/Input.tsx

@@ -16,6 +16,7 @@ export interface InputProps
   withError?: boolean;
   withError?: boolean;
   label?: React.ReactNode;
   label?: React.ReactNode;
   hint?: React.ReactNode;
   hint?: React.ReactNode;
+  clearIcon?: React.ReactNode;
 
 
   // Some may only accept integer, like `Number of Partitions`
   // Some may only accept integer, like `Number of Partitions`
   // some may accept decimal
   // some may accept decimal
@@ -99,19 +100,22 @@ function pasteNumberCheck(
   return value;
   return value;
 }
 }
 
 
-const Input: React.FC<InputProps> = ({
-  name,
-  hookFormOptions,
-  search,
-  inputSize = 'L',
-  type,
-  positiveOnly,
-  integerOnly,
-  withError = false,
-  label,
-  hint,
-  ...rest
-}) => {
+const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
+  const {
+    name,
+    hookFormOptions,
+    search,
+    inputSize = 'L',
+    type,
+    positiveOnly,
+    integerOnly,
+    withError = false,
+    label,
+    hint,
+    clearIcon,
+    ...rest
+  } = props;
+
   const methods = useFormContext();
   const methods = useFormContext();
 
 
   const fieldId = React.useId();
   const fieldId = React.useId();
@@ -168,7 +172,6 @@ const Input: React.FC<InputProps> = ({
     // if the field is a part of react-hook-form form
     // if the field is a part of react-hook-form form
     inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
     inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
   }
   }
-
   return (
   return (
     <div>
     <div>
       {label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
       {label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
@@ -181,8 +184,11 @@ const Input: React.FC<InputProps> = ({
           type={type}
           type={type}
           onKeyPress={keyPressEventHandler}
           onKeyPress={keyPressEventHandler}
           onPaste={pasteEventHandler}
           onPaste={pasteEventHandler}
+          ref={ref}
           {...inputOptions}
           {...inputOptions}
         />
         />
+        {clearIcon}
+
         {withError && isHookFormField && (
         {withError && isHookFormField && (
           <S.FormError>
           <S.FormError>
             <ErrorMessage name={name} />
             <ErrorMessage name={name} />
@@ -192,6 +198,6 @@ const Input: React.FC<InputProps> = ({
       </S.Wrapper>
       </S.Wrapper>
     </div>
     </div>
   );
   );
-};
+});
 
 
 export default Input;
 export default Input;

+ 32 - 1
kafka-ui-react-app/src/components/common/Search/Search.tsx

@@ -1,7 +1,9 @@
-import React from 'react';
+import React, { useRef } from 'react';
 import { useDebouncedCallback } from 'use-debounce';
 import { useDebouncedCallback } from 'use-debounce';
 import Input from 'components/common/Input/Input';
 import Input from 'components/common/Input/Input';
 import { useSearchParams } from 'react-router-dom';
 import { useSearchParams } from 'react-router-dom';
+import CloseIcon from 'components/common/Icons/CloseIcon';
+import styled from 'styled-components';
 
 
 interface SearchProps {
 interface SearchProps {
   placeholder?: string;
   placeholder?: string;
@@ -10,6 +12,16 @@ interface SearchProps {
   value?: string;
   value?: string;
 }
 }
 
 
+const IconButtonWrapper = styled.span.attrs(() => ({
+  role: 'button',
+  tabIndex: '0',
+}))`
+  height: 16px !important;
+  display: inline-block;
+  &:hover {
+    cursor: pointer;
+  }
+`;
 const Search: React.FC<SearchProps> = ({
 const Search: React.FC<SearchProps> = ({
   placeholder = 'Search',
   placeholder = 'Search',
   disabled = false,
   disabled = false,
@@ -17,7 +29,11 @@ const Search: React.FC<SearchProps> = ({
   onChange,
   onChange,
 }) => {
 }) => {
   const [searchParams, setSearchParams] = useSearchParams();
   const [searchParams, setSearchParams] = useSearchParams();
+  const ref = useRef<HTMLInputElement>(null);
   const handleChange = useDebouncedCallback((e) => {
   const handleChange = useDebouncedCallback((e) => {
+    if (ref.current != null) {
+      ref.current.value = e.target.value;
+    }
     if (onChange) {
     if (onChange) {
       onChange(e.target.value);
       onChange(e.target.value);
     } else {
     } else {
@@ -28,6 +44,15 @@ const Search: React.FC<SearchProps> = ({
       setSearchParams(searchParams);
       setSearchParams(searchParams);
     }
     }
   }, 500);
   }, 500);
+  const clearSearchValue = () => {
+    if (searchParams.get('q')) {
+      searchParams.set('q', '');
+      setSearchParams(searchParams);
+    }
+    if (ref.current != null) {
+      ref.current.value = '';
+    }
+  };
 
 
   return (
   return (
     <Input
     <Input
@@ -37,7 +62,13 @@ const Search: React.FC<SearchProps> = ({
       defaultValue={value || searchParams.get('q') || ''}
       defaultValue={value || searchParams.get('q') || ''}
       inputSize="M"
       inputSize="M"
       disabled={disabled}
       disabled={disabled}
+      ref={ref}
       search
       search
+      clearIcon={
+        <IconButtonWrapper onClick={clearSearchValue}>
+          <CloseIcon />
+        </IconButtonWrapper>
+      }
     />
     />
   );
   );
 };
 };

+ 20 - 0
kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx

@@ -41,4 +41,24 @@ describe('Search', () => {
     render(<Search />);
     render(<Search />);
     expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
     expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
   });
   });
+
+  it('Clear button is visible', () => {
+    render(<Search placeholder={placeholder} />);
+
+    const clearButton = screen.getByRole('button');
+    expect(clearButton).toBeInTheDocument();
+  });
+
+  it('Clear button should clear text from input', async () => {
+    render(<Search placeholder={placeholder} />);
+
+    const searchField = screen.getAllByRole('textbox')[0];
+    await userEvent.type(searchField, 'some text');
+    expect(searchField).toHaveValue('some text');
+
+    const clearButton = screen.getByRole('button');
+    await userEvent.click(clearButton);
+
+    expect(searchField).toHaveValue('');
+  });
 });
 });