All files / src/components main-search.tsx

70.58% Statements 12/17
62.5% Branches 10/16
54.54% Functions 6/11
75% Lines 12/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                                                                                                    8x     8x                   6x 6x           6x   6x   6x     6x 1x                   6x                 3x                                       1x                                                          
import {
  type CSSProperties,
  Fragment,
  type InputHTMLAttributes,
  type SyntheticEvent,
  useMemo,
} from 'react';
 
import { Dropdown } from './dropdown-button';
import Button from './button';
 
import type { FranklinStyle } from '../types/common';
 
import '../styles/components/main-search.scss';
 
interface InputInternalStyle extends CSSProperties {
  '--input-padding': string;
}
 
export type MainSearchProps = {
  /**
   * Action performed upon search submission
   */
  onSubmit: (e: SyntheticEvent) => void;
  /**
   * What happens when text is typed in the search box
   */
  onTextChange: (selectedValue: string) => void;
  /**
   * The search term
   */
  searchTerm?: string;
  /**
   * An object representing the different namespace options
   */
  namespaces?: Record<string, string>;
  /**
   * What happens when a namespace is changed in the dropdown
   */
  onNamespaceChange?: (namespace: string) => void;
  /**
   * The selected namespace in the dropdown
   */
  selectedNamespace?: string;
  /**
   * A list of objects representing secondary buttons and their actions
   */
  secondaryButtons?: { label: string; action: () => void }[];
} & InputHTMLAttributes<HTMLInputElement>;
 
const countCharacters = (items: string[]) =>
  items.reduce((prev, curr) => prev + curr.length, 0);
 
const MainSearch = ({
  searchTerm,
  namespaces = {},
  onTextChange,
  onSubmit,
  onNamespaceChange,
  selectedNamespace,
  secondaryButtons,
  ...props
}: MainSearchProps) => {
  const style = useMemo<FranklinStyle>(
    () => ({
      '--main-button-color': `var(--fr--color-${selectedNamespace}, var(--fr--color-sea-blue))`,
    }),
    [selectedNamespace]
  );
 
  const inputStyle = useMemo<InputInternalStyle | undefined>(() => {
    const count =
      secondaryButtons &&
      countCharacters(secondaryButtons.map(({ label }) => label));
    return count ? { '--input-padding': `${count}ch` } : undefined;
  }, [secondaryButtons]);
 
  const visibleElement = (onClick: () => unknown) => (
    <Button variant="primary" style={style} onClick={onClick}>
      {
        namespaces[
          // Pick the first item if nothing defined
          selectedNamespace || Object.keys(namespaces)[0]
        ]
      }
    </Button>
  );
 
  return (
    <form onSubmit={onSubmit} aria-label="Main search" className="main-search">
      {Object.keys(namespaces).length > 0 && onNamespaceChange && (
        <Dropdown
          visibleElement={visibleElement}
          propChangeToClose={selectedNamespace}
        >
          <ul className="no-bullet">
            {Object.keys(namespaces).map((key) => (
              <li key={key}>
                <Button
                  variant="tertiary"
                  onClick={() => {
                    onNamespaceChange(key);
                  }}
                >
                  {namespaces[key]}
                </Button>
              </li>
            ))}
          </ul>
        </Dropdown>
      )}
      <div className="main-search__input-container">
        <input
          type="search"
          aria-label={`Text query${
            selectedNamespace ? ` in ${selectedNamespace}` : ''
          }`}
          onChange={(e) => onTextChange(e.target.value)}
          value={searchTerm}
          style={inputStyle}
          {...props}
        />
        {secondaryButtons && (
          <div className="main-search__secondary-container">
            {secondaryButtons.map(({ label, action }, index) => (
              <Fragment key={index}>
                {index > 0 && <small> | </small>}
                <button
                  type="button"
                  onClick={action}
                  key={label}
                  className="main-search--secondary"
                >
                  <small>{label}</small>
                </button>
              </Fragment>
            ))}
          </div>
        )}
      </div>
      <Button type="submit">Search</Button>
    </form>
  );
};
 
export default MainSearch;