All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.components.SearchInput.SearchInput.tsx Maven / Gradle / Ivy

Go to download

This library provides a set of common React components for use with the PatternFly reference implementation.

The newest version!
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import { Button, ButtonVariant } from '../Button';
import { Badge } from '../Badge';
import { Icon } from '../Icon';
import AngleDownIcon from '@patternfly/react-icons/dist/esm/icons/angle-down-icon';
import AngleUpIcon from '@patternfly/react-icons/dist/esm/icons/angle-up-icon';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon';
import { AdvancedSearchMenu } from './AdvancedSearchMenu';
import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../TextInputGroup';
import { InputGroup, InputGroupItem } from '../InputGroup';
import { Popper } from '../../helpers';
import textInputGroupStyles from '@patternfly/react-styles/css/components/TextInputGroup/text-input-group';

/** Properties for adding search attributes to an advanced search input. These properties must
 * be passed in as an object within an array to the search input component's attribute properrty.
 */

export interface SearchInputSearchAttribute {
  /** The search attribute's value to be provided in the search input's query string.
   * It should have no spaces and be unique for every attribute.
   */
  attr: string;
  /** The search attribute's display name. It is used to label the field in the advanced
   * search menu.
   */
  display: React.ReactNode;
}

/** Properties for creating an expandable search input. These properties should be passed into
 * the search input component's expandableInput property.
 */

export interface SearchInputExpandable {
  /** Flag to indicate if the search input is expanded. */
  isExpanded: boolean;
  /** Callback function to toggle the expandable search input. */
  onToggleExpand: (event: React.SyntheticEvent, isExpanded: boolean) => void;
  /** An accessible label for the expandable search input toggle. */
  toggleAriaLabel: string;
}

/** The main search input component. */

export interface SearchInputProps extends Omit, 'onChange' | 'results' | 'ref'> {
  /** Delimiter in the query string for pairing attributes with search values.
   * Required whenever attributes are passed as props.
   */
  advancedSearchDelimiter?: string;
  /** The container to append the menu to.
   * If your menu is being cut off you can append it to an element higher up the DOM tree.
   * Some examples:
   * appendTo={() => document.body}
   * appendTo={document.getElementById('target')}
   */
  appendTo?: HTMLElement | (() => HTMLElement) | 'inline';
  /** An accessible label for the search input. */
  'aria-label'?: string;
  /** Flag to indicate utilities should be displayed. By default if this prop is undefined or false, utilities will only be displayed when the search input has a value. */

  areUtilitiesDisplayed?: boolean;
  /** Array of attribute values used for dynamically generated advanced search. */
  attributes?: string[] | SearchInputSearchAttribute[];
  /** Additional classes added to the search input. */
  className?: string;
  /** Object that makes the search input expandable/collapsible. */
  expandableInput?: SearchInputExpandable;
  /* Additional elements added after the attributes in the form.
   * The new form elements can be wrapped in a form group component for automatic formatting. */
  formAdditionalItems?: React.ReactNode;
  /** Attribute label for strings unassociated with one of the provided listed attributes. */
  hasWordsAttrLabel?: React.ReactNode;
  /** A suggestion for autocompleting. */
  hint?: string;
  /** @hide A reference object to attach to the input box. */
  innerRef?: React.RefObject;
  /** A flag for controlling the open state of a custom advanced search implementation. */
  isAdvancedSearchOpen?: boolean;
  /** Flag indicating if search input is disabled. */
  isDisabled?: boolean;
  /** Flag indicating if the next navigation button is disabled. */
  isNextNavigationButtonDisabled?: boolean;
  /** Flag indicating if the previous navigation button is disabled. */
  isPreviousNavigationButtonDisabled?: boolean;
  /** Accessible label for the button to navigate to next result. */
  nextNavigationButtonAriaLabel?: string;
  /** A callback for when the input value changes. */
  onChange?: (event: React.FormEvent, value: string) => void;
  /** A callback for when the user clicks the clear button. */
  onClear?: (event: React.SyntheticEvent) => void;
  /** A callback for when the user clicks to navigate to next result. */
  onNextClick?: (event: React.SyntheticEvent) => void;
  /** A callback for when the user clicks to navigate to previous result. */
  onPreviousClick?: (event: React.SyntheticEvent) => void;
  /** A callback for when the search button is clicked. */
  onSearch?: (
    event: React.SyntheticEvent,
    value: string,
    attrValueMap: { [key: string]: string }
  ) => void;
  /** A callback for when the open advanced search button is clicked. */
  onToggleAdvancedSearch?: (event: React.SyntheticEvent, isOpen?: boolean) => void;
  /** Accessible label for the button which opens the advanced search form menu. */
  openMenuButtonAriaLabel?: string;
  /** Placeholder text of the search input. */
  placeholder?: string;
  /** Accessible label for the button to navigate to previous result. */
  previousNavigationButtonAriaLabel?: string;
  /** z-index of the advanced search form when appendTo is not inline. */
  zIndex?: number;
  /** Label for the button which resets the advanced search form and clears the search input. */
  resetButtonLabel?: string;
  /** The number of search results returned. Either a total number of results,
   * or a string representing the current result over the total number of results. i.e. "1 / 5". */
  resultsCount?: number | string;
  /** Label for the button which calls the onSearch event handler. */
  submitSearchButtonLabel?: string;
  /** Value of the search input. */
  value?: string;
  /** Name attribue for the search input */
  name?: string;
}

const SearchInputBase: React.FunctionComponent = ({
  className,
  value = '',
  attributes = [] as string[],
  formAdditionalItems,
  hasWordsAttrLabel = 'Has words',
  advancedSearchDelimiter,
  placeholder,
  hint,
  onChange,
  onSearch,
  onClear,
  onToggleAdvancedSearch,
  isAdvancedSearchOpen,
  resultsCount,
  onNextClick,
  onPreviousClick,
  innerRef,
  expandableInput,
  'aria-label': ariaLabel = 'Search input',
  resetButtonLabel = 'Reset',
  openMenuButtonAriaLabel = 'Open advanced search',
  previousNavigationButtonAriaLabel = 'Previous',
  isPreviousNavigationButtonDisabled = false,
  isNextNavigationButtonDisabled = false,
  nextNavigationButtonAriaLabel = 'Next',
  submitSearchButtonLabel = 'Search',
  isDisabled = false,
  appendTo,
  zIndex = 9999,
  name,
  areUtilitiesDisplayed,
  ...props
}: SearchInputProps) => {
  const [isSearchMenuOpen, setIsSearchMenuOpen] = React.useState(false);
  const [searchValue, setSearchValue] = React.useState(value);
  const searchInputRef = React.useRef(null);
  const ref = React.useRef(null);
  const searchInputInputRef = innerRef || ref;
  const searchInputExpandableToggleRef = React.useRef(null);
  const triggerRef = React.useRef(null);
  const popperRef = React.useRef(null);
  const [focusAfterExpandChange, setFocusAfterExpandChange] = React.useState(false);

  const { isExpanded, onToggleExpand, toggleAriaLabel } = expandableInput || {};

  React.useEffect(() => {
    // this effect and the focusAfterExpandChange variable are needed to focus the input/toggle as needed when the
    // expansion toggle is fired without focusing on mount
    if (!focusAfterExpandChange) {
      return;
    } else if (isExpanded) {
      searchInputInputRef?.current?.focus();
    } else {
      searchInputExpandableToggleRef?.current?.focus();
    }
    setFocusAfterExpandChange(false);
  }, [focusAfterExpandChange, isExpanded, searchInputInputRef, searchInputExpandableToggleRef]);

  React.useEffect(() => {
    setSearchValue(value);
  }, [value]);

  React.useEffect(() => {
    if (attributes.length > 0 && !advancedSearchDelimiter) {
      // eslint-disable-next-line no-console
      console.error(
        'An advancedSearchDelimiter prop is required when advanced search attributes are provided using the attributes prop'
      );
    }
  });

  React.useEffect(() => {
    setIsSearchMenuOpen(isAdvancedSearchOpen);
  }, [isAdvancedSearchOpen]);

  const onChangeHandler = (event: React.FormEvent, value: string) => {
    if (onChange) {
      onChange(event, value);
    }
    setSearchValue(value);
  };

  const onToggle = (e: React.SyntheticEvent) => {
    const isOpen = !isSearchMenuOpen;
    setIsSearchMenuOpen(isOpen);
    if (onToggleAdvancedSearch) {
      onToggleAdvancedSearch(e, isOpen);
    }
  };

  const onSearchHandler = (event: React.SyntheticEvent) => {
    event.preventDefault();
    if (onSearch) {
      onSearch(event, value, getAttrValueMap());
    }
    setIsSearchMenuOpen(false);
  };

  const splitStringExceptInQuotes = (str: string) => {
    let quoteType: string;

    return str.match(/\\?.|^$/g).reduce(
      (p: any, c: string) => {
        if (c === "'" || c === '"') {
          if (!quoteType) {
            quoteType = c;
          }
          if (c === quoteType) {
            p.quote = !p.quote;
          }
        } else if (!p.quote && c === ' ') {
          p.a.push('');
        } else {
          p.a[p.a.length - 1] += c.replace(/\\(.)/, '$1');
        }
        return p;
      },
      { a: [''] }
    ).a;
  };

  const getAttrValueMap = () => {
    const attrValue: { [key: string]: string } = {};
    const pairs = splitStringExceptInQuotes(searchValue);
    pairs.map((pair: string) => {
      const splitPair = pair.split(advancedSearchDelimiter);
      if (splitPair.length === 2) {
        attrValue[splitPair[0]] = splitPair[1].replace(/(^'|'$)/g, '');
      } else if (splitPair.length === 1) {
        attrValue.haswords = attrValue.hasOwnProperty('haswords')
          ? `${attrValue.haswords} ${splitPair[0]}`
          : splitPair[0];
      }
    });
    return attrValue;
  };

  const onEnter = (event: React.KeyboardEvent) => {
    if (event.key === 'Enter') {
      onSearchHandler(event);
    }
  };

  const onClearInput = (e: React.SyntheticEvent) => {
    if (onClear) {
      onClear(e);
    }
    if (searchInputInputRef && searchInputInputRef.current) {
      searchInputInputRef.current.focus();
    }
  };

  const onExpandHandler = (event: React.SyntheticEvent) => {
    setSearchValue('');
    onToggleExpand(event, isExpanded);
    setFocusAfterExpandChange(true);
  };

  const renderUtilities =
    value && (resultsCount || (!!onNextClick && !!onPreviousClick) || (!!onClear && !expandableInput));

  const buildTextInputGroup = ({ ...searchInputProps } = {}) => (
    
      }
        innerRef={searchInputInputRef}
        value={searchValue}
        placeholder={placeholder}
        aria-label={ariaLabel}
        onKeyDown={onEnter}
        onChange={onChangeHandler}
        name={name}
      />
      {(renderUtilities || areUtilitiesDisplayed) && (
        
          {resultsCount && {resultsCount}}
          {!!onNextClick && !!onPreviousClick && (
            
)} {!!onClear && !expandableInput && ( )}
)}
); const expandableToggle = ( )} {!!onSearch && ( )} {expandableInput && {expandableToggle}} ); const searchInputProps = { ...props, className: className && css(className), innerRef: searchInputRef }; if (!!expandableInput && !isExpanded) { return ( {expandableToggle} ); } if (!!onSearch || attributes.length > 0 || !!onToggleAdvancedSearch) { if (attributes.length > 0) { const AdvancedSearch = (
); const AdvancedSearchWithPopper = (
appendTo || searchInputRef.current} zIndex={zIndex} />
); const AdvancedSearchInline = (
{buildSearchTextInputGroupWithExtraButtons()} {AdvancedSearch}
); return appendTo !== 'inline' ? AdvancedSearchWithPopper : AdvancedSearchInline; } return buildSearchTextInputGroupWithExtraButtons({ ...searchInputProps }); } return buildSearchTextInputGroup(searchInputProps); }; SearchInputBase.displayName = 'SearchInputBase'; export const SearchInput = React.forwardRef((props: SearchInputProps, ref: React.Ref) => ( } /> )); SearchInput.displayName = 'SearchInput';




© 2015 - 2024 Weber Informatics LLC | Privacy Policy