---
id: Search input
section: components
---
import { useEffect, useRef, useState } from 'react';
import {
Button,
Card,
CardBody,
CardFooter,
DatePicker,
Form,
FormGroup,
Grid,
GridItem,
Menu,
MenuContent,
MenuItem,
MenuList,
MenuToggle,
Popper,
SearchInput,
TextInput
} from '@patternfly/react-core';
import { words } from './words.js';

## Demos

### Search with autocomplete

This demo handles building the advanced search form using the composable Menu, and the `SearchInput`'s `hint` prop.
It also demonstrates wiring up the appropriate keyboard interactions, focus management, and general event handling.

```js
import { useEffect, useRef, useState } from 'react';
import { Menu, MenuContent, MenuItem, MenuList, Popper, SearchInput } from '@patternfly/react-core';

import { words } from './words.js';

SearchAutocomplete = () => {
  const [value, setValue] = useState('');
  const [hint, setHint] = useState('');
  const [autocompleteOptions, setAutocompleteOptions] = useState([]);

  const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);

  const searchInputRef = useRef(null);
  const autocompleteRef = useRef(null);

  const onClear = () => {
    setValue('');
  };

  const onChange = (_event, newValue) => {
    if (
      newValue !== '' &&
      searchInputRef &&
      searchInputRef.current &&
      searchInputRef.current.contains(document.activeElement)
    ) {
      setIsAutocompleteOpen(true);

      // When the value of the search input changes, build a list of no more than 10 autocomplete options.
      // Options which start with the search input value are listed first, followed by options which contain
      // the search input value.
      let options = words
        .filter((option) => option.startsWith(newValue.toLowerCase()))
        .map((option) => (
          <MenuItem itemId={option} key={option}>
            {option}
          </MenuItem>
        ));
      if (options.length > 10) {
        options = options.slice(0, 10);
      } else {
        options = [
          ...options,
          ...words
            .filter((option) => !option.startsWith(newValue.toLowerCase()) && option.includes(newValue.toLowerCase()))
            .map((option) => (
              <MenuItem itemId={option} key={option}>
                {option}
              </MenuItem>
            ))
        ].slice(0, 10);
      }

      // The hint is set whenever there is only one autocomplete option left.
      setHint(options.length === 1 ? options[0].props.itemId : '');
      // The menu is hidden if there are no options
      setIsAutocompleteOpen(options.length > 0);
      setAutocompleteOptions(options);
    } else {
      setIsAutocompleteOpen(false);
    }
    setValue(newValue);
  };

  // Whenever an autocomplete option is selected, set the search input value, close the menu, and put the browser
  // focus back on the search input
  const onSelect = (e, itemId) => {
    e.stopPropagation();
    setValue(itemId);
    setIsAutocompleteOpen(false);
    searchInputRef.current.focus();
  };

  const handleMenuKeys = (event) => {
    // If there is a hint while the browser focus is on the search input, tab or right arrow will 'accept' the hint value
    // and set it as the search input value
    if (hint && (event.key === 'Tab' || event.key === 'ArrowRight') && searchInputRef.current === event.target) {
      setValue(hint);
      setHint('');
      setIsAutocompleteOpen(false);
      if (event.key === 'ArrowRight') {
        event.preventDefault();
      }
      // if the autocomplete is open and the browser focus is on the search input,
    } else if (isAutocompleteOpen && searchInputRef.current && searchInputRef.current === event.target) {
      // the escape key closes the autocomplete menu and keeps the focus on the search input.
      if (event.key === 'Escape') {
        setIsAutocompleteOpen(false);
        searchInputRef.current.focus();
        // the up and down arrow keys move browser focus into the autocomplete menu
      } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
        const firstElement = autocompleteRef.current.querySelector('li > button:not(:disabled)');
        firstElement && firstElement.focus();
        event.preventDefault(); // by default, the up and down arrow keys scroll the window
        // the tab, enter, and space keys will close the menu, and the tab key will move browser
        // focus forward one element (by default)
      } else if (event.key === 'Tab' || event.key === 'Enter' || event.key === ' ') {
        setIsAutocompleteOpen(false);
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
        }
      }
      // If the autocomplete is open and the browser focus is in the autocomplete menu
      // hitting tab will close the autocomplete and but browser focus back on the search input.
    } else if (isAutocompleteOpen && autocompleteRef.current.contains(event.target) && event.key === 'Tab') {
      event.preventDefault();
      setIsAutocompleteOpen(false);
      searchInputRef.current.focus();
    }
  };

  // The autocomplete menu should close if the user clicks outside the menu.
  const handleClickOutside = (event) => {
    if (
      isAutocompleteOpen &&
      autocompleteRef &&
      autocompleteRef.current &&
      !autocompleteRef.current.contains(event.target)
    ) {
      setIsAutocompleteOpen(false);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleMenuKeys);
    window.addEventListener('click', handleClickOutside);
    return () => {
      window.removeEventListener('keydown', handleMenuKeys);
      window.removeEventListener('click', handleClickOutside);
    };
  }, [isAutocompleteOpen, hint, searchInputRef.current]);

  const searchInput = (
    <SearchInput
      value={value}
      onChange={onChange}
      onClear={onClear}
      ref={searchInputRef}
      hint={hint}
      id="autocomplete-search"
      aria-label='Search with autocomplete'
    />
  );

  const autocomplete = (
    <Menu ref={autocompleteRef} onSelect={onSelect}>
      <MenuContent>
        <MenuList>{autocompleteOptions}</MenuList>
      </MenuContent>
    </Menu>
  );

  return (
    <Popper
      trigger={searchInput}
      triggerRef={searchInputRef}
      popper={autocomplete}
      popperRef={autocompleteRef}
      isVisible={isAutocompleteOpen}
      enableFlip={false}
      // append the autocomplete menu to the search input in the DOM for the sake of the keyboard navigation experience
      appendTo={() => document.querySelector('#autocomplete-search')}
    />
  );
};
```

### Composable advanced search

This demo handles building the advanced search form using the composable Menu, as well as wiring up a
select using the composable Menu and MenuToggle components. This demo also demonstrates wiring up the appropriate
keyboard interactions, focus management, and general event handling.

Note: This demo and its handling of 'date within' and a date picker is modeled after the gmail advanced search form.

```js
import { useEffect, useRef, useState } from 'react';
import {
  ActionGroup,
  Button,
  DatePicker,
  Form,
  FormGroup,
  Grid,
  GridItem,
  isValidDate,
  Menu,
  MenuContent,
  MenuItem,
  MenuList,
  MenuToggle,
  Panel,
  PanelMain,
  PanelMainBody,
  Popper,
  SearchInput,
  TextInput,
  yyyyMMddFormat
} from '@patternfly/react-core';

AdvancedComposableSearchInput = () => {
  const [value, setValue] = useState('');
  const [hasWords, setHasWords] = useState('');
  const [dateWithin, setDateWithin] = useState('1 day');
  const [date, setDate] = useState();

  const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(false);
  const [isDateWithinOpen, setIsDateWithinOpen] = useState(false);

  const isInitialMount = useRef(true);
  const firstAttrRef = useRef(null);
  const searchInputRef = useRef(null);
  const advancedSearchPaneRef = useRef(null);
  const dateWithinToggleRef = useRef(undefined);
  const dateWithinMenuRef = useRef(undefined);

  const onClear = () => {
    setValue('');
    setHasWords('');
    setDateWithin('');
    setDate('');
  };

  const onChange = (_event, value) => {
    if (value.length <= hasWords.length + 1) {
      setValue(value);
      setHasWords(value);
    } else {
      setValue(hasWords);
    }
  };

  // After initial page load, whenever the advanced search menu is opened, the browser focus should be placed on the
  // first advanced search form input. Whenever the advanced search menu is closed, the browser focus should
  // be returned to the search input.
  useEffect(() => {
    if (isInitialMount.current) {
      isInitialMount.current = false;
    } else {
      if (isAdvancedSearchOpen && firstAttrRef && firstAttrRef.current) {
        firstAttrRef.current.focus();
      } else if (!isAdvancedSearchOpen && searchInputRef) {
        searchInputRef.current.focus();
      }
    }
  }, [isAdvancedSearchOpen]);

  // If a menu is open and has browser focus, then the escape key closes them and puts the browser focus onto their
  // respective toggle. The 'date within' menu also needs to close when the 'tab' key is hit. However, hitting tab while
  // focus is in the advanced search form should move the focus to the next form input, not close the advanced search
  // menu.
  const handleMenuKeys = (event) => {
    if (isDateWithinOpen && dateWithinMenuRef.current && dateWithinMenuRef.current.contains(event.target)) {
      if (event.key === 'Escape' || event.key === 'Tab') {
        setIsDateWithinOpen(!isDateWithinOpen);
        dateWithinToggleRef.current.focus();
      }
    }
    if (isAdvancedSearchOpen && advancedSearchPaneRef.current && advancedSearchPaneRef.current.contains(event.target)) {
      if (
        event.key === 'Escape' ||
        (event.key === 'Tab' &&
          !event.shiftKey &&
          advancedSearchPaneRef.current.querySelector('button[type=reset]') === event.target)
      ) {
        setIsAdvancedSearchOpen(!isAdvancedSearchOpen);
        searchInputRef.current.focus();
      }
    }
  };

  // If a menu is open and has browser focus, then clicking outside the menu should close it.
  const handleClickOutside = (event) => {
    if (
      isDateWithinOpen &&
      dateWithinMenuRef &&
      dateWithinMenuRef.current &&
      !dateWithinMenuRef.current.contains(event.target)
    ) {
      setIsDateWithinOpen(false);
    }
    if (
      isAdvancedSearchOpen &&
      advancedSearchPaneRef &&
      advancedSearchPaneRef.current &&
      !advancedSearchPaneRef.current.contains(event.target)
    ) {
      setIsAdvancedSearchOpen(false);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleMenuKeys);
    window.addEventListener('click', handleClickOutside);
    return () => {
      window.removeEventListener('keydown', handleMenuKeys);
      window.removeEventListener('click', handleClickOutside);
    };
  }, [dateWithinMenuRef.current, advancedSearchPaneRef.current, isAdvancedSearchOpen, isDateWithinOpen]);

  // This demo and its handling of 'date within' and a date picker is modeled after the gmail advanced search form.
  const onSubmit = (event, value) => {
    event.preventDefault();

    if (isValidDate(new Date(date)) && dateWithin) {
      let afterDate = new Date(date);
      let toDate = new Date(date);
      switch (dateWithin) {
        case '1 day':
          afterDate.setDate(afterDate.getDate());
          toDate.setDate(toDate.getDate() + 2);
          break;
        case '3 days':
          afterDate.setDate(afterDate.getDate() - 2);
          toDate.setDate(toDate.getDate() + 4);
          break;
        case '1 week':
          afterDate.setDate(afterDate.getDate() - 6);
          toDate.setDate(toDate.getDate() + 8);
          break;
        case '2 weeks':
          afterDate.setDate(afterDate.getDate() - 13);
          toDate.setDate(toDate.getDate() + 15);
          break;
        case '1 month':
          afterDate.setMonth(afterDate.getMonth() - 1);
          afterDate.setDate(afterDate.getDate() + 1);
          toDate.setMonth(toDate.getMonth() + 1);
          toDate.setDate(toDate.getDate() + 1);
          break;
        case '2 months':
          afterDate.setMonth(afterDate.getMonth() - 2);
          afterDate.setDate(afterDate.getDate() + 1);
          toDate.setMonth(toDate.getMonth() + 2);
          toDate.setDate(toDate.getDate() + 1);
          break;
        case '6 months':
          afterDate.setMonth(afterDate.getMonth() - 6);
          afterDate.setDate(afterDate.getDate() + 1);
          toDate.setMonth(toDate.getMonth() + 6);
          toDate.setDate(toDate.getDate() + 1);
          break;
        case '1 year':
          afterDate.setFullYear(afterDate.getFullYear() - 1);
          afterDate.setDate(afterDate.getDate() + 1);
          toDate.setFullYear(toDate.getFullYear() + 1);
          toDate.setDate(toDate.getDate() + 1);
          break;
      }
      setValue(`${hasWords && hasWords + ' '}after:${yyyyMMddFormat(afterDate)} to:${yyyyMMddFormat(toDate)}`);
    } else {
      setValue(hasWords);
    }

    setIsAdvancedSearchOpen(false);
  };

  const searchInput = (
    <SearchInput
      value={value}
      onChange={onChange}
      onToggleAdvancedSearch={(e, isOpen) => {
        e.stopPropagation();
        setIsAdvancedSearchOpen(isOpen);
      }}
      isAdvancedSearchOpen={isAdvancedSearchOpen}
      onClear={onClear}
      onSearch={onSubmit}
      ref={searchInputRef}
      id="custom-advanced-search"
      aria-label='Composable advanced search'
    />
  );

  // Clicking the 'date within' toggle should open its associated menu and then place the browser
  // focus on the first menu item.
  const toggleDateWithinMenu = (ev) => {
    ev.stopPropagation(); // Stop handleClickOutside from handling
    setTimeout(() => {
      if (dateWithinMenuRef.current) {
        const firstElement = dateWithinMenuRef.current.querySelector('li > button:not(:disabled)');
        firstElement && firstElement.focus();
      }
    }, 0);
    setIsDateWithinOpen(!isDateWithinOpen);
  };

  // Selecting a date within option closes the menu, sets the value of date within, and puts browser focus back
  // on the date within toggle.
  const onDateWithinSelect = (e, itemId) => {
    e.stopPropagation();
    setIsDateWithinOpen(false);
    setDateWithin(itemId);
    if (dateWithinToggleRef && dateWithinToggleRef.current) {
      dateWithinToggleRef.current.focus();
    }
  };

  const dateWithinOptions = (
    <Menu ref={dateWithinMenuRef} selected={dateWithin} onSelect={onDateWithinSelect}>
      <MenuContent>
        <MenuList>
          <MenuItem itemId="1 day">1 day</MenuItem>
          <MenuItem itemId="3 days">3 days</MenuItem>
          <MenuItem itemId="1 week">1 week</MenuItem>
          <MenuItem itemId="2 weeks">2 weeks</MenuItem>
          <MenuItem itemId="1 month">1 month</MenuItem>
          <MenuItem itemId="2 months">2 months</MenuItem>
          <MenuItem itemId="6 months">6 months</MenuItem>
          <MenuItem itemId="1 year">1 year</MenuItem>
        </MenuList>
      </MenuContent>
    </Menu>
  );

  const dateWithinToggle = (
    <MenuToggle
      ref={dateWithinToggleRef}
      onClick={toggleDateWithinMenu}
      isExpanded={isDateWithinOpen}
      style={{ width: '100%' }}
    >
      {dateWithin}
    </MenuToggle>
  );

  const advancedForm = (
    <div ref={advancedSearchPaneRef} role="dialog" aria-label="Advanced search form">
      <Panel variant="raised">
        <PanelMain>
          <PanelMainBody>
            <Form>
              <FormGroup label="Has the words" fieldId="has-words" key="has-words">
                <TextInput
                  type="text"
                  id="has-words"
                  value={hasWords}
                  onChange={(_event, value) => {
                    setHasWords(value);
                    setValue(value);
                  }}
                  ref={firstAttrRef}
                />
              </FormGroup>
              <Grid hasGutter md={6}>
                <GridItem>
                  <FormGroup label="Date within" fieldId="date-within" key="date-within">
                    <Popper
                      trigger={dateWithinToggle}
                      triggerRef={dateWithinToggleRef}
                      popper={dateWithinOptions}
                      popperRef={dateWithinMenuRef}
                      isVisible={isDateWithinOpen}
                    />
                  </FormGroup>
                </GridItem>
                <GridItem>
                  <FormGroup label="Of date" fieldId="date" key="date">
                    <DatePicker
                      id="datePicker"
                      style={{ width: '100%' }}
                      value={date}
                      onChange={(_e, newValue) => setDate(newValue)}
                      appendTo={() => document.querySelector('#datePicker')}
                    />
                  </FormGroup>
                </GridItem>
              </Grid>
              <ActionGroup>
                <Button variant="primary" type="submit" onClick={(e) => onSubmit(null, e)}>
                  Submit
                </Button>
                {!!onClear && (
                  <Button variant="link" type="reset" onClick={onClear}>
                    Reset
                  </Button>
                )}
              </ActionGroup>
            </Form>
          </PanelMainBody>
        </PanelMain>
      </Panel>
    </div>
  );

  // Popper is just one way to build a relationship between a toggle and a menu.
  return (
    <Popper
      trigger={searchInput}
      triggerRef={searchInputRef}
      popper={advancedForm}
      popperRef={advancedSearchPaneRef}
      isVisible={isAdvancedSearchOpen}
      enableFlip={false}
      appendTo={() => document.querySelector('#custom-advanced-search')}
    />
  );
};
```
