Combo box with background autocomplete

This demonstrates the search working like the autocomplete on the Google Mail search bar.

Try typing in:, is:, from: or to: for example.

Pressing tab will complete the suggestion. A disabled 'ghost' input is placed behind the main input to show the autocomplete suggestion.

⚠️ Warning a screen-reader will not know there is an auto-selected suggestion, which might be confusing.

You can toggle between an theoretically accessible version by setting autoselect to "inline".

import { useEffect, useState } from 'react';
import { ComboBox } from '@citizensadvice/react-combo-boxes';

function searcher(value) {
  if (!value) {
    return [];
  }

  const match = value.match(/^.*\b(in|is|from|to):(\w*)$/);
  if (!match) {
    return [];
  }

  const found = {
    in: ['spam', 'bin', 'inbox', 'draft'],
    is: ['read', 'starred'],
    from: ['me', 'example@foo.com'],
    to: ['me', 'example@bar.com'],
  }[match[1]];

  if (!found) {
    return [];
  }

  return found
    .filter(
      (term) =>
        (!match[2] || term.startsWith(match[2].toLowerCase())) &&
        !new RegExp(`\\b${match[1]}:${term}\\b`).test(value),
    )
    .map((term) => `${value}${term.slice((match[2] || '').length)} `);
}

function renderInput(props, { expanded, suggestedOption }) {
  const suggestedValue = (expanded && suggestedOption?.label) || '';
  const { className } = props;

  return (
    <>
      <input
        {...props}
        style={{ background: 'transparent' }}
      />
      <input
        className={className}
        value={suggestedValue}
        disabled
        style={{
          position: 'absolute',
          left: 0,
          background: 'white',
          zIndex: -1,
          color: '#999',
        }}
      />
    </>
  );
}

export function Example() {
  const [value, setValue] = useState(null);
  const [options, setOptions] = useState(null);
  const [autoselect, setAutoselect] = useState(false);

  useEffect(() => {
    setOptions(searcher(value));
  }, [value]);

  return (
    <>
      <label
        id="select-label"
        htmlFor="select"
      >
        Select
      </label>
      <ComboBox
        id="select"
        aria-labelledby="select-label"
        value={value}
        size={100}
        onValue={setValue}
        onSearch={setValue}
        options={options}
        tabAutocomplete
        showSelectedLabel
        expandOnFocus={false}
        managedFocus={false}
        renderNotFound={() => null}
        renderClearButton={() => null}
        renderInput={renderInput}
        autoselect={autoselect}
        autoCapitalize="none"
        autoCorrect="off"
      />

      <fieldset>
        <legend>Autoselect</legend>
        <label>
          <input
            type="radio"
            name="autoselect"
            checked={autoselect === false}
            onChange={({ target: { checked } }) => {
              if (checked) {
                setAutoselect(false);
              }
            }}
          />{' '}
          <code>false</code>
        </label>
        <label>
          <input
            type="radio"
            name="autoselect"
            checked={autoselect === true}
            onChange={({ target: { checked } }) => {
              if (checked) {
                setAutoselect(true);
              }
            }}
          />{' '}
          <code>true</code>
        </label>
        <label>
          <input
            type="radio"
            name="autoselect"
            checked={autoselect === 'inline'}
            onChange={({ target: { checked } }) => {
              if (checked) {
                setAutoselect('inline');
              }
            }}
          />{' '}
          <code>&quot;inline&quot;</code>
        </label>
      </fieldset>

      <label htmlFor="output">Current value</label>
      <output
        htmlFor="select"
        id="output"
      >
        {JSON.stringify(value, undefined, ' ')}
      </output>
    </>
  );
}