# Autocomplete

An Autocomplete component allows a user to quickly pick a preset value from a
larger list of possible options.

## Design & usage guidelines

The options in an Autocomplete component should be a list of possible values
that the user can select. These options should be presented in a way that makes
it easy for the user to find the value they are looking for. There are a few
ways to achieve this.

[Details](/storybook/web/?path=/story/components-forms-and-inputs-autocomplete--with-details)
can be added to an option, which will either appear below the option's label or
on the bottom right of the option. This can be used to provide more information
about the option, such as a description or a count.

[Section headings](/storybook/web/?path=/story/components-forms-and-inputs-autocomplete--section-heading)
can be added to an Autocomplete to break up the options into groups. This can be
useful if there are a lot of options or if the options are related to different
things.

You can
[set a value](/storybook/web/?path=/story/components-forms-and-inputs-autocomplete--set-a-value)
to the Autocomplete component by passing an active option. This will set the
`value` of the input.

Using the `getOptions` prop, you can pass a function that will be called when
the user types in the input. This async request should return a list of possible
options based on the user's input. This can be achieved by using either promises
or `await`.

## Related components

* If you want to present a list of predefined options without text input, or the
  number of options is smaller, use a [Select](../Select/Select.md)
* If autocompleted results are not required for the text input, use
  [InputText](../InputText/InputText.md)


## Component customization

### Custom rendering

If you need more control over the rendering of content in the menu, you can
provide a **custom render function** using `customRenderMenu`

This is useful if the default rendering of the Autocomplete Menu doesn’t meet
your requirements. If a custom render function is provided you will be able to
completely control how the menu is rendered.

This is achieved by providing a function to the `customRenderMenu` prop. This
function will receive the following props:

* `MenuWrapper` is the wrapper of the Autocomplete Menu, this is the element
  that will be visible when the menu is open. It handles the positioning of the
  Autocomplete Menu
* `inputFocused` is a boolean that indicates if the input is focused.
* `onOptionSelect` is a function that is called when an option is selected for
  updating the value of the Autocomplete.
* `options` is the list of options to render in the Autocomplete Menu. This is
  the options that are returned from the `getOptions` function.
* `selectedOption` is the option that is currently selected.
* `menuRef` is a ref that is passed to the Autocomplete Menu this is used by the
  `useKeyboardNavigation` hook to handle the keyboard navigation of the options.
* `inputRef` is a ref that is passed to the Autocomplete Menu to provide a ref
  to the `InputText` component.

There are also some helper functions that are used to handle the keyboard
navigation of the options and the base styling of the options.

* `useKeyboardNavigation` is used to handle the default keyboard navigation of
  the options. Use this hook if you are using only using MenuOption or
  BaseMenuOption components in the menu.
* `useCustomKeyboardNavigation` is used to handle the keyboard navigation of the
  options. Use this hook if you are using components in the menu that aren't
  MenuOption or BaseMenuOption such as custom elements. See the Advanced
  examples below for more information.
* `MenuOption` is the default rendering of the options in the Autocomplete Menu.
  The `UNSAFE_style` prop is used to add a custom styling to the options.
* `isOptionGroup` is a helper function that is used to determine if the option
  is a group.
* `isOptionSelected` is a helper function that is used to determine if the
  option is selected.
* `MenuOptionContent` is the default rendering of the content of the options in
  the Autocomplete Menu.
* `BaseMenuOption` is the base component for the options in the Autocomplete
  Menu. This component provides the base styling and functionality for the
  options such as the separators and the highlighting. This is used in the
  advanced example below.

### Basic Override

In this example we will override the default rendering of the Autocomplete Menu to add a custom styling to the options while keeping the default keyboard navigation.

```tsx
<AutocompleteBasicCustomRenderExample />
```

**Full Example (Click to Reveal Code)**

```tsx
export const BasicCustomTemplate = () => {
  const [detailsValue, setDetailsValue] = useState<Option | undefined>();
  const planetSection = SectionHeadingOptions.find(
    ({ label }) => label === "Planets",
  )?.options;

  return (
    <Content>
      <Autocomplete
        placeholder="Search for an option with section headings"
        initialOptions={SectionHeadingOptions}
        value={detailsValue}
        onChange={newValue => setDetailsValue(newValue)}
        customRenderMenu={({
          MenuWrapper,
          inputFocused,
          onOptionSelect,
          options,
          selectedOption,
          menuRef,
        }) => {
          const { highlightedIndex } = useKeyboardNavigation({
            visible: inputFocused,
            options,
            onOptionSelect,
            menuRef,
          });

          return (
            <MenuWrapper visible={inputFocused}>
              {options.map((option, index) => {
                const optionStyle = getOptionStyling({
                  option,
                  highlightedIndex,
                  options,
                });

                return (
                  <MenuOption
                    key={option.label}
                    option={option}
                    addSeparators={isOptionGroup(option)}
                    isHighlighted={index === highlightedIndex}
                    onOptionSelect={onOptionSelect}
                    isSelected={isOptionSelected(selectedOption, option)}
                    UNSAFE_style={optionStyle}
                  />
                );
              })}
            </MenuWrapper>
          );
        }}
        getOptions={getHeadingsOption}
      />
    </Content>
  );

  // eslint-disable-next-line max-statements
  function getOptionStyling({
    option,
    highlightedIndex,
    options,
  }: {
    option: AnyOption;
    highlightedIndex: number;
    options: AnyOption[];
  }) {
    const isSectionLabel = "options" in option;
    const isPlanetsLabel = option.label === "Planets";

    const inPlanetSection = planetSection?.find?.(
      ({ label }) => label === option.label,
    );
    const activeOption = options[highlightedIndex];

    const activeOptionInSection =
      isSectionLabel &&
      option?.options?.find?.(({ label }) => label === activeOption?.label);
    let optionColor = "var(--color-success--surface)";
    let groupColor = "var(--color-success)";

    if (isSectionLabel && activeOptionInSection) {
      groupColor = "var(--color-informative)";
      optionColor = "var(--color-informative--surface)";
    } else if (!isSectionLabel && inPlanetSection) {
      groupColor = "var(--color-warning)";
      optionColor = "var(--color-warning--surface)";
    } else if (isSectionLabel && isPlanetsLabel) {
      groupColor = "var(--color-critical)";
      optionColor = "var(--color-critical--surface)";
    }
    const isActive = activeOption === option ? 0.5 : 1;

    return {
      groupOption: {
        heading: {
          backgroundColor: groupColor,
        },
      },
      option: {
        backgroundColor: optionColor,
        opacity: isActive,
      },
    };
  }

  function getHeadingsOption(text: string) {
    if (text === "") {
      return SectionHeadingOptions;
    }
    const filterRegex = new RegExp(text, "i");

    return SectionHeadingOptions.map(section => ({
      ...section,
      options: section.options.filter(option =>
        option.label.match(filterRegex),
      ),
    }));
  }
};
```

### Advanced Menu Override

In this example we will override the rendering of the menu to render the Options
in a completely custom way. This example will also include a footer that is
rendered at the bottom of the menu with sticky positioning.

**Note:** Due to the complexity of the example, we will not be using the
`useKeyboardNavigation` hook to handle the keyboard navigation of the options.

Instead we will be handling the keyboard navigation manually with the
`useCustomKeyboardNavigation` hook. This requires us to manually keep track of
the highlighted option index and update it when the user navigates the menu.

```tsx
<AutocompleteAdvancedCustomRenderExample />
```

**Full Example (Click to Reveal Code)**

```tsx
interface CustomOption {
    value: number;
    label: string;
    address: string;
    contact: string;
    status: string;
  }
  const AdvancedOptions: CustomOption[] = [
    {
      value: 1,
      label: "Nostromo",
      address: "4517 Washington Ave. Manchester, Kentucky 39495",
      contact: "(406) 555-4145 · darlene.r@gmail.com",
      status: "Lead",
    },
    {
      value: 2,
      label: "Rodger Young",
      address: "8502 Preston Rd. Inglewood, Maine 98380",
      contact: "(406) 555-0120 · marvin.m@gmail.com",
      status: "Active",
    },
    {
      value: 3,
      label: "Serenity",
      address: "3 Properties",
      contact: "(406) 555-0120 · seemore.b@gmail.com",
      status: "Active",
    },
    {
      value: 4,
      label: "Sleeper Service",
      address: "3891 Ranchview Dr. Richardson, California 62639",
      contact: "(406) 555-0120",
      status: "Active",
    },
  ];
  
  export const AdvancedCustomTemplate = () => {
    const [value, setValue] = useState<CustomOption | undefined>();
  
    function getOptions(text: string) {
      if (text === "") {
        return AdvancedOptions;
      }
      const filterRegex = new RegExp(text, "i");
  
      return AdvancedOptions.filter(
        option =>
          option.label.match(filterRegex) ||
          option.address.match(filterRegex) ||
          option.contact.match(filterRegex),
      );
    }
  
    return (
      <Autocomplete
        placeholder="Search for something"
        initialOptions={AdvancedOptions}
        value={value}
        onChange={newValue => setValue(newValue)}
        customRenderMenu={props => <CustomMenuContent {...props} />}
        getOptions={getOptions}
      />
    );
  };
  
  function CustomMenuContent({
    options,
    selectedOption,
    onOptionSelect,
    inputFocused,
    inputRef,
    MenuWrapper,
    menuRef,
  }: CustomOptionsMenuProp<CustomOption, CustomOption>) {
    // Set to -1 to account for the footer being the first option when options are being initialized
    const INITIAL_HIGHLIGHTED_OPTION_INDEX = -1;

    const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(
      INITIAL_HIGHLIGHTED_OPTION_INDEX,
    );
    // Length of options + 1 to account for the footer
    const maxIndex = options.length - 1 + 1;

    // We need to track the footer focus state because it can be focused instead of just the Input
    const [footerFocused, setFooterFocused] = useState(false);
    const footerElement = document.querySelector(
      "#footerElement",
    ) as HTMLElement;

    const footerFocusedCallback = useCallbackRef(() => {
      setFooterFocused(true);
    });
    const footerBlurCallback = useCallbackRef(() => {
      setFooterFocused(false);
    });

    useEffect(() => {
      footerElement?.addEventListener("focus", footerFocusedCallback);
      footerElement?.addEventListener("blur", footerBlurCallback);

      return () => {
        footerElement?.removeEventListener("focus", footerFocusedCallback);
        footerElement?.removeEventListener("blur", footerBlurCallback);
      };
    }, [footerElement, footerFocusedCallback, footerBlurCallback]);

    const menuVisible = useMemo(
      () => inputFocused || footerFocused,
      [inputFocused, footerFocused],
    );
  
    const onRequestHighlightChange = useCallback(
      (event: KeyboardEvent, direction: KeyboardAction) => {
        const indexChange = getRequestedIndexChange({
          event,
          options,
          direction,
          highlightedIndex: highlightedOptionIndex,
        });
        const newPreviousIndex = Math.max(
          0,
          highlightedOptionIndex + indexChange,
        );
        const newNextIndex = Math.min(
          maxIndex,
          highlightedOptionIndex + indexChange,
        );
        if (!menuVisible) return;
  
        switch (direction) {
          case KeyboardAction.Previous:
            // If the footer is focused, focus the input
            if (highlightedOptionIndex === maxIndex) {
              inputRef?.current?.focus();
            }
            setHighlightedOptionIndex(newPreviousIndex);
            menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
              behavior: "smooth",
              block: "nearest",
              inline: "start",
            });
  
            break;
          case KeyboardAction.Next: {
            setHighlightedOptionIndex(newNextIndex);
            const nextElement = menuRef?.children[newNextIndex];
            const footerHeight = footerElement?.offsetHeight || 0;

            if (nextElement) {
              const rect = nextElement.getBoundingClientRect();
              const menuRect = menuRef.getBoundingClientRect();

              // If element is hidden behind footer
              if (rect.bottom > menuRect.bottom - footerHeight) {
                // Calculate exact scroll position needed
                const scrollOffset =
                  rect.bottom - (menuRect.bottom - footerHeight);
                menuRef.scrollTop += scrollOffset;
              } else {
                nextElement.scrollIntoView({
                  behavior: "smooth",
                  block: "nearest",
                  inline: "start",
                });
              }
            }

            if (newNextIndex === maxIndex) {
              footerElement?.focus();
            }
            break;
          }
  
          case KeyboardAction.Select:
            // Don't select the footer
            highlightedOptionIndex < maxIndex &&
              onOptionSelect(options[highlightedOptionIndex]);
  
            menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
              behavior: "smooth",
              block: "nearest",
              inline: "start",
            });
            if (highlightedOptionIndex === maxIndex) {
              footerElement?.click();
            }
            break;
        }
      },
      [highlightedOptionIndex, options, onOptionSelect],
    );
    useCustomKeyboardNavigation({ onRequestHighlightChange });
  
    return (
      <MenuWrapper visible={menuVisible}>
        {options.map((option, index) => {
          const label = option.status;
          const status = option.status === "Active" ? "success" : "informative";
          return (
            <BaseMenuOption
              addSeparators={true}
              isHighlighted={index === highlightedOptionIndex}
              onOptionSelect={onOptionSelect}
              option={option}
              key={option.label}
            >
              <Flex template={["grow", "shrink"]}>
                <Flex align="start" template={["shrink", "grow"]}>
                  <Content spacing="minuscule">
                    {selectedOption === option && (
                      <Icon name="checkmark" size="small" />
                    )}
                    <Text>{option.label}</Text>
                    <Text variation="subdued">{option.address}</Text>
                    <Text variation="subdued">{option.contact}</Text>
                  </Content>
                </Flex>
                <StatusLabel status={status} label={label} />
              </Flex>
            </BaseMenuOption>
          );
        })}
        {footer}
      </MenuWrapper>
    );
  }
```

#### Rendering the Menu Content

The content in the Menu is wrapped by the `<MenuWrapper>` component which
handles attaching the menu to the input. Our custom options are wrapped by the
`BaseMenuOption` component which handles the base styling and functionality for
the options. Inside the `BaseMenuOption` component the content that we want to
render is provided as children.

**Note:** For the footer to be sticky at the bottom of the menu without being
scrollable, the footer needs to be wrapped in a `div` with `position: "sticky"`
and `bottom: 0`.

**Menu Option Content (Click to Reveal Code)**

```tsx
function addNewClient() {
  window.alert("Add new client");
}
const footer = (
  <div style={{ position: "sticky", bottom: 0 }}>
    <Button
      label="+ Add new client"
      onClick={addNewClient}
      id="footerElement"
      size="small"
      fullWidth
      type="tertiary"
    />
  </div>
);
<MenuWrapper visible={menuVisible}>
  {options.map((option, index) => {
    const label = option.status;
    const status = option.status === "Active" ? "success" : "informative";

    return (
      <BaseMenuOption
        addSeparators={true}
        isHighlighted={index === highlightedOptionIndex}
        onOptionSelect={onOptionSelect}
        option={option}
        key={option.label}
      >
        <Flex template={["grow", "shrink"]}>
          <Flex align="start" template={["shrink", "grow"]}>
            <Content spacing="minuscule">
              {selectedOption === option && (
                <Icon name="checkmark" size="small" />
              )}
              <Text>{option.label}</Text>
              <Text variation="subdued">{option.address}</Text>
              <Text variation="subdued">{option.contact}</Text>
            </Content>
          </Flex>
          <StatusLabel status={status} label={label} />
        </Flex>
      </BaseMenuOption>
    );
  })}
  {footer}
</MenuWrapper>
```

#### Keyboard Navigation

The following code is the keyboard navigation logic for the menu. It is used to
handle the keyboard navigation of the options and the footer.

* `highlightedOptionIndex` is the index of the option that is currently
  highlighted. It is initially set to -1 to account for the footer being the
  first option when options are being initialized.
* `maxIndex` is the index of the footer.
* `footerFocused` is a boolean that indicates if the footer is focused. We need
  to track this because it can be focused instead of just the Input and if we
  don't then it is possible for the Menu to remain opened
* `menuVisible` is a boolean that indicates if the menu is visible.
* `onRequestHighlightChange` is a function that is called when the user
  navigates the menu.
* `getRequestedIndexChange` is a helper function that is used to get the index
  change based on the keyboard event, direction and if the option is a group.
* `KeyboardAction.Next` case should calculate the exact scroll position needed
  with the footer height offset in mind, when the next element is hidden behind
  the footer.

**Keyboard Navigation (Click to Reveal Code)**

```tsx
// Set to -1 to account for the footer being the first option when options are being initialized
    const INITIAL_HIGHLIGHTED_OPTION_INDEX = -1;

    const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(
      INITIAL_HIGHLIGHTED_OPTION_INDEX,
    );
    // Length of options + 1 to account for the footer
    const maxIndex = options.length - 1 + 1;

    // We need to track the footer focus state because it can be focused instead of just the Input
    const [footerFocused, setFooterFocused] = useState(false);
    const footerElement = document.querySelector(
      "#footerElement",
    ) as HTMLElement;

    const footerFocusedCallback = useCallbackRef(() => {
      setFooterFocused(true);
    });
    const footerBlurCallback = useCallbackRef(() => {
      setFooterFocused(false);
    });

    useEffect(() => {
      footerElement?.addEventListener("focus", footerFocusedCallback);
      footerElement?.addEventListener("blur", footerBlurCallback);

      return () => {
        footerElement?.removeEventListener("focus", footerFocusedCallback);
        footerElement?.removeEventListener("blur", footerBlurCallback);
      };
    }, [footerElement, footerFocusedCallback, footerBlurCallback]);

    const menuVisible = useMemo(
      () => inputFocused || footerFocused,
      [inputFocused, footerFocused],
    );
  
    const onRequestHighlightChange = useCallback(
      (event: KeyboardEvent, direction: KeyboardAction) => {
        const indexChange = getRequestedIndexChange({
          event,
          options,
          direction,
          highlightedIndex: highlightedOptionIndex,
        });
        const newPreviousIndex = Math.max(
          0,
          highlightedOptionIndex + indexChange,
        );
        const newNextIndex = Math.min(
          maxIndex,
          highlightedOptionIndex + indexChange,
        );
        if (!menuVisible) return;
  
        switch (direction) {
          case KeyboardAction.Previous:
            // If the footer is focused, focus the input
            if (highlightedOptionIndex === maxIndex) {
              inputRef?.current?.focus();
            }
            setHighlightedOptionIndex(newPreviousIndex);
            menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
              behavior: "smooth",
              block: "nearest",
              inline: "start",
            });
  
            break;
          case KeyboardAction.Next: {
            setHighlightedOptionIndex(newNextIndex);
            const nextElement = menuRef?.children[newNextIndex];
            const footerHeight = footerElement?.offsetHeight || 0;

            if (nextElement) {
              const rect = nextElement.getBoundingClientRect();
              const menuRect = menuRef.getBoundingClientRect();

              // If element is hidden behind footer
              if (rect.bottom > menuRect.bottom - footerHeight) {
                // Calculate exact scroll position needed
                const scrollOffset =
                  rect.bottom - (menuRect.bottom - footerHeight);
                menuRef.scrollTop += scrollOffset;
              } else {
                nextElement.scrollIntoView({
                  behavior: "smooth",
                  block: "nearest",
                  inline: "start",
                });
              }
            }

            if (newNextIndex === maxIndex) {
              footerElement?.focus();
            }
            break;
          }
  
          case KeyboardAction.Select:
            // Don't select the footer
            highlightedOptionIndex < maxIndex &&
              onOptionSelect(options[highlightedOptionIndex]);
  
            menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
              behavior: "smooth",
              block: "nearest",
              inline: "start",
            });
            if (highlightedOptionIndex === maxIndex) {
              footerElement?.click();
            }
            break;
        }
      },
      [highlightedOptionIndex, options, onOptionSelect],
    );
    useCustomKeyboardNavigation({ onRequestHighlightChange });
```

### Advanced Custom Section Headings

In this example we will override the rendering of the menu to render the an
Autocomplete with Section Headings and Custom Elements in the Section Headings.

**Note:** Similar to the Advanced Custom Menu Override, due to the complexity of
the example, we will not be using the `useKeyboardNavigation` hook to handle the
keyboard navigation of the options.

```tsx
<AutocompleteAdvancedSectionHeadingExample />
```

**Full Example (Click to Reveal Code)**

```tsx
interface CustomOptionForGroup {
  value: number;
  label: string;
  status: string;
}
interface CustomGroupOption {
  label: string;
  icon: IconNames;
  options: CustomOptionForGroup[];
}

const SectionHeadingOptionsCustom: CustomGroupOption[] = [
  {
    label: "Quotes",
    icon: "quote",
    options: [
      { value: 1, label: "Quote for Acme Corp", status: "Draft" },
      { value: 2, label: "Quote for Stark Industries", status: "Approved" },
      {
        value: 3,
        label: "Quote for Wayne Enterprises",
        status: "Awaiting approval",
      },
      { value: 4, label: "Quote for Umbrella Corp", status: "Paid" },
    ],
  },
  {
    icon: "invoice",
    label: "Invoices",
    options: [
      { value: 5, label: "Invoice for Acme Corp", status: "Draft" },
      {
        value: 6,
        label: "Invoice for Stark Industries",
        status: "Awaiting payment",
      },
      {
        value: 7,
        label: "Invoice for Wayne Enterprises",
        status: "Awaiting payment",
      },
      { value: 8, label: "Invoice for Umbrella Corp", status: "Paid" },
    ],
  },
];

interface CustomElementOption {
  sectionLabel: string;
  indexToInsertAfter: number;
}
type OptionsWithExtraElements =
  | CustomGroupOption
  | CustomOptionForGroup
  | CustomElementOption;

export function AdvancedSectionHeadingTemplate() {
  const [value, setValue] = useState<CustomOptionForGroup | undefined>();

  return (
    <Autocomplete
      placeholder="Search for something under a section heading"
      initialOptions={SectionHeadingOptionsCustom}
      value={value}
      onChange={newValue => setValue(newValue)}
      getOptions={() => SectionHeadingOptionsCustom}
      customRenderMenu={props => (
        <AdvancedSectionHeadingCustomMenuContent {...props} />
      )}
    />
  );
}

function AdvancedSectionHeadingCustomMenuContent({
  options,
  selectedOption,
  onOptionSelect,
  inputFocused,
  inputRef,
  MenuWrapper,
  menuRef,
}: CustomOptionsMenuProp<CustomGroupOption, CustomOptionForGroup>) {
  const { optionsWithExtraElements, extraElementFocused } =
    useCustomSectionOptions(options);
  // Set to 1 to account for the first option being the section heading
  const initialHighlight = 1

  const [highlightedOptionIndex, setHighlightedOptionIndex] =
    useState(initialHighlight);
  // Length of options -1 and the number of extra elements

  const maxIndex = optionsWithExtraElements.length - 1;

  const menuVisible = useMemo(
    () => extraElementFocused || inputFocused,
    [extraElementFocused, inputFocused],
  );
  const onRequestHighlightChange = useCallback(
    // eslint-disable-next-line max-statements
    (_event: KeyboardEvent, direction: KeyboardAction) => {
      const requestedIndex =
        optionsWithExtraElements[highlightedOptionIndex + direction];
      // Default index change to the direction. If the requested index is not an extra element,
      // then we need to adjust the index change to account for the section heading
      let indexChange = direction;

      if (requestedIndex && !isExtraElement(requestedIndex)) {
        indexChange =
          requestedIndex && isOptionGroup(requestedIndex)
            ? direction + direction
            : direction;
      }

      const newPreviousIndex = Math.max(
        0,
        highlightedOptionIndex + indexChange,
      );
      const newNextIndex = Math.min(
        maxIndex,
        highlightedOptionIndex + indexChange,
      );

      if (!menuVisible) return;

            switch (direction) {
        case KeyboardAction.Previous:
          // If the option is an extra element, focus the extra element. Else focus the input
          if (isExtraElement(optionsWithExtraElements[newPreviousIndex])) {
            const element = document.getElementById(
              optionsWithExtraElements[newPreviousIndex].sectionLabel,
            ) as HTMLElement;
            element?.focus();
          } else {
            inputRef?.current?.focus();
          }

          setHighlightedOptionIndex(newPreviousIndex);
          menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
            behavior: "smooth",
            block: "nearest",
            inline: "start",
          });

          break;
        case KeyboardAction.Next:
          setHighlightedOptionIndex(newNextIndex);
          menuRef?.children[newNextIndex]?.scrollIntoView?.({
            behavior: "smooth",
            block: "nearest",
            inline: "start",
          });

          // If the option is an extra element, focus the extra element. Else focus the input
          if (isExtraElement(optionsWithExtraElements[newNextIndex])) {
            const element = document.getElementById(
              optionsWithExtraElements[newNextIndex].sectionLabel,
            ) as HTMLElement;
            element?.focus();
          } else {
            inputRef?.current?.focus();
          }
          break;

        case KeyboardAction.Select:
          menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
            behavior: "smooth",
            block: "nearest",
            inline: "start",
          });

          // If the option is an extra element, click the extra element. Else check it is not an option group and select the option
          if (
            isExtraElement(optionsWithExtraElements[highlightedOptionIndex])
          ) {
            const element = document.getElementById(
              optionsWithExtraElements[highlightedOptionIndex].sectionLabel,
            ) as HTMLElement;
            element?.click();
          } else if (
            !isOptionGroup(optionsWithExtraElements[highlightedOptionIndex])
          ) {
            onOptionSelect(optionsWithExtraElements[highlightedOptionIndex]);
          }

          break;
      }
    },
    [
      highlightedOptionIndex,
      options,
      onOptionSelect,
      extraElementFocused,
      maxIndex,
      optionsWithExtraElements,
    ],
  );
  useCustomKeyboardNavigation({ onRequestHighlightChange });

  ${AdvancedSectionMenuRenderingCode}
}

function AdvancedSectionFooterButton({
  label,
  id,
}: {
  readonly label: string;
  readonly id: string;
}) {
  return (
    <Button
      label={\`+ Add new \${label}\`}
      onClick={() => alert(\`Add new \${label}\`)}
      size="small"
      id={id}
      fullWidth
      type="tertiary"
    />
  );
}

${AdvancedSectionSelectableOptionCode}
${useCustomSectionOptionsCode}
${isExtraElementCode}
```

#### Rendering the Menu Content

The content in the Menu is wrapped by the `<MenuWrapper>` component which
handles attaching the menu to the input. We use the `optionsWithExtraElements`
to render the options. The `optionsWithExtraElements` is the options with the
extra elements inserted in the correct positions. This array is generated using
a custom hook called `useCustomSectionOptions`. (explanation of the hook below).

**Menu Content Rendering (Click to Reveal Code)**

```tsx
<MenuWrapper visible={menuVisible}>
  {optionsWithExtraElements.map((option, index) => {
    if (isExtraElement(option)) {
      return (
        <AdvancedSectionFooterButton
          label={option.sectionLabel}
          id={option.sectionLabel}
          key={\`\${option.sectionLabel}-create-new\`}
        />
      );
    }

    if (isOptionGroup(option)) {
      return (
        <BaseMenuGroupOption key={option.label}>
          <Flex template={["grow", "shrink"]}>
            <Heading level={5}>{option.label}</Heading>
            <Icon name={option.icon} />
          </Flex>
        </BaseMenuGroupOption>
      );
    }

    return (
        <SelectableOption
          isSelected={isOptionSelected(selectedOption, option)}
          key={option.value}
          option={option}
          index={index}
          highlightedOptionIndex={highlightedOptionIndex}
          onOptionSelect={onOptionSelect}
      />
    );
  })}
</MenuWrapper>
```

#### Utilities used for Custom Rendering

The following utilities were created in this example to help with custom
rendering:

`useCustomSectionOptions` determines the indices of the options that should have
extra elements inserted after them. It then inserts the extra elements into an
array and returns it with the extra elements inserted. This is used when the
options are being rendered.

**useCustomSectionOptions (Click to Reveal Code)**

```tsx
/**
 * Get the indices of the options that should have extra elements inserted after them
 * @param options - The options to get the extra element indices for
 * @returns The indices of the options that should have extra elements inserted after them
 */
function getExtraElementIndices(
  options: (CustomOptionForGroup | CustomGroupOption)[],
) {
  const sectionsOptions = options.filter(isOptionGroup);

  return sectionsOptions.map(({ label, options: optionsInSection }) => {
    const indexToInsertAfter = options.findIndex(
      opt => opt === optionsInSection[optionsInSection.length - 1],
    );

    return {
      sectionLabel: label,
      indexToInsertAfter,
    };
  });
}

/**
 * Creates a new array with the extra elements inserted into the options array
 * @param options - The options to insert the extra elements into
 * @param extraElements - The extra elements to insert into the options
 * @returns The options with the extra elements inserted
 */
function insertExtraElements(
  options: (CustomOptionForGroup | CustomGroupOption)[],
  extraElements: CustomElementOption[],
) {
  return options.reduce<Array<OptionsWithExtraElements>>(
    (acc, option, index) => {
      const shouldInsertExtraElement = extraElements.find(
        ({ indexToInsertAfter }) => indexToInsertAfter === index,
      );

      if (shouldInsertExtraElement) {
        return [
          ...acc,
          option,
          {
            sectionLabel: shouldInsertExtraElement.sectionLabel,
            indexToInsertAfter: shouldInsertExtraElement.indexToInsertAfter,
          },
        ];
      }

      return [...acc, option];
    },
    [],
  );
}

/**
 * Custom hook to handle the section options with extra elements
 * @param options - The options to handle
 * @returns The options with the extra elements inserted and the focus state for the extra elements
 */
function useCustomSectionOptions(
  options: (CustomGroupOption | CustomOptionForGroup)[],
) {
  const extraElements = useMemo(
    () => getExtraElementIndices(options),
    [options],
  );
  const optionsWithExtraElements = useMemo(
    () => insertExtraElements(options, extraElements),
    [options, extraElements],
  );
  // We need to track the focus state for the extra elements because it can be focused instead of just the Input
  const [extraElementFocused, setExtraElementFocused] = useState(false);

  const extraElementFocusCallback = useCallbackRef(() => {
    setExtraElementFocused(true);
  });
  const extraElementBlurCallback = useCallbackRef(() => {
    setExtraElementFocused(false);
  });
  // Add event listeners to the extra elements for focus and blur
  useEffect(() => {
    extraElements.forEach(({ sectionLabel }) => {
      const element = document.getElementById(sectionLabel) as HTMLElement;
      element?.addEventListener("focus", extraElementFocusCallback);
      element?.addEventListener("blur", extraElementBlurCallback);
    });

    return () => {
      extraElements.forEach(({ sectionLabel }) => {
        const element = document.getElementById(sectionLabel) as HTMLElement;
        element?.removeEventListener("focus", extraElementFocusCallback);
        element?.removeEventListener("blur", extraElementBlurCallback);
      });
    };
  }, [extraElements]);

  return {
    optionsWithExtraElements,
    extraElementFocused,
  };
}
```

`isExtraElement` is a helper function that is used to determine if the option is
an extra element.

**isExtraElement (Click to Reveal Code)**

```tsx
/**
 * Checks if the option is an extra element
 * @param option - The option to check
 * @returns Whether the option is an extra element
 */
function isExtraElement(option: object): option is CustomElementOption {
  return option && "sectionLabel" in option && "indexToInsertAfter" in option;
}
```

`SelectableOption` is a component that is used to render the selectable options
in the menu.

**SelectableOption (Click to Reveal Code)**

```tsx
/**
 * Renders the selectable option
 * @param option - The option to render
 * @param index - The index of the option
 * @param highlightedOptionIndex - The index of the highlighted option
 * @param onOptionSelect - The function to call when the option is selected
 * @param isSelected - Whether the option is selected
 */
function SelectableOption({
  option,
  index,
  highlightedOptionIndex,
  onOptionSelect,
  isSelected,
}: {
  readonly option: CustomOptionForGroup;
  readonly index: number;
  readonly highlightedOptionIndex: number;
  readonly onOptionSelect: (option?: CustomOptionForGroup) => void;
  readonly isSelected: boolean;
}) {
  let status: StatusIndicatorType = "informative";

  switch (option.status) {
    case "Paid":
    case "Approved":
      status = "success";
      break;
    case "Awaiting payment":
    case "Awaiting approval":
      status = "warning";
      break;
    case "Draft":
      status = "inactive";
      break;
  }

  return (
    <BaseMenuOption
      addSeparators={true}
      isHighlighted={highlightedOptionIndex === index}
      onOptionSelect={onOptionSelect}
      option={option}
    >
      <Flex template={["grow", "shrink"]}>
        <Text>
          {isSelected && <Icon name="checkmark" size="small" />} {option.label}
        </Text>
        <StatusLabel label={option.status} status={status} />
      </Flex>
    </BaseMenuOption>
  );
}
```

#### Keyboard Navigation

The following code is the keyboard navigation logic for the menu. It is used to
handle the keyboard navigation of the options and the footers in each of the
sections.

The keyboard navigation logic is similar to the Advanced Custom Menu Override.
However, we need to account for the extra elements in each of the sections
instead of just a single footer.

`useCustomSectionOptions` returns the options with the extra elements inserted
and the `extraElementFocused` state. It provides event listeners for keeping
track of the focused state for if an extra element is focused. There are
additional checks to see if the requested index is an extra element and if it
is, focus the extra element and if not it will focus the input.

**Keyboard Navigation (Click to Reveal Code)**

```tsx
... 
  // Other code
  const { optionsWithExtraElements, extraElementFocused } =
    useCustomSectionOptions(options);
  // Set to 1 to account for the first option being the section heading
  const initialHighlight = 1

  const [highlightedOptionIndex, setHighlightedOptionIndex] =
    useState(initialHighlight);
  // Length of options -1 and the number of extra elements

  const maxIndex = optionsWithExtraElements.length - 1;

  const menuVisible = useMemo(
    () => extraElementFocused || inputFocused,
    [extraElementFocused, inputFocused],
  );
  const onRequestHighlightChange = useCallback(
    // eslint-disable-next-line max-statements
    (_event: KeyboardEvent, direction: KeyboardAction) => {
      const requestedIndex =
        optionsWithExtraElements[highlightedOptionIndex + direction];
      // Default index change to the direction. If the requested index is not an extra element,
      // then we need to adjust the index change to account for the section heading
      let indexChange = direction;

      if (requestedIndex && !isExtraElement(requestedIndex)) {
        indexChange =
          requestedIndex && isOptionGroup(requestedIndex)
            ? direction + direction
            : direction;
      }

      const newPreviousIndex = Math.max(
        0,
        highlightedOptionIndex + indexChange,
      );
      const newNextIndex = Math.min(
        maxIndex,
        highlightedOptionIndex + indexChange,
      );

      if (!menuVisible) return;

            switch (direction) {
        case KeyboardAction.Previous:
          // If the option is an extra element, focus the extra element. Else focus the input
          if (isExtraElement(optionsWithExtraElements[newPreviousIndex])) {
            const element = document.getElementById(
              optionsWithExtraElements[newPreviousIndex].sectionLabel,
            ) as HTMLElement;
            element?.focus();
          } else {
            inputRef?.current?.focus();
          }

          setHighlightedOptionIndex(newPreviousIndex);
          menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
            behavior: "smooth",
            block: "nearest",
            inline: "start",
          });

          break;
        case KeyboardAction.Next:
          setHighlightedOptionIndex(newNextIndex);
          menuRef?.children[newNextIndex]?.scrollIntoView?.({
            behavior: "smooth",
            block: "nearest",
            inline: "start",
          });

          // If the option is an extra element, focus the extra element. Else focus the input
          if (isExtraElement(optionsWithExtraElements[newNextIndex])) {
            const element = document.getElementById(
              optionsWithExtraElements[newNextIndex].sectionLabel,
            ) as HTMLElement;
            element?.focus();
          } else {
            inputRef?.current?.focus();
          }
          break;

        case KeyboardAction.Select:
          menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
            behavior: "smooth",
            block: "nearest",
            inline: "start",
          });

          // If the option is an extra element, click the extra element. Else check it is not an option group and select the option
          if (
            isExtraElement(optionsWithExtraElements[highlightedOptionIndex])
          ) {
            const element = document.getElementById(
              optionsWithExtraElements[highlightedOptionIndex].sectionLabel,
            ) as HTMLElement;
            element?.click();
          } else if (
            !isOptionGroup(optionsWithExtraElements[highlightedOptionIndex])
          ) {
            onOptionSelect(optionsWithExtraElements[highlightedOptionIndex]);
          }

          break;
      }
    },
    [
      highlightedOptionIndex,
      options,
      onOptionSelect,
      extraElementFocused,
      maxIndex,
      optionsWithExtraElements,
    ],
  );
  useCustomKeyboardNavigation({ onRequestHighlightChange });
```


## Props

### Web

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `getOptions` | `(newInputText: string) => (GenericOption | GenericGetOptionsValue)[] | Promise<(GenericOption | GenericGetOptionsValue)[]>` | Yes | — | Called as the user types in the input. The autocomplete will display what is returned from this method to the user as... |
| `onChange` | `(newValue?: GenericOptionValue) => void` | Yes | — | Simplified onChange handler that only provides the new value. @param newValue |
| `placeholder` | `string` | Yes | — | Hint text that goes above the value once the form is filled out. |
| `value` | `Option` | Yes | — | Set Autocomplete value. |
| `allowFreeForm` | `boolean` | No | `true` | Allow the autocomplete to use values not from the drop down menu. |
| `clearable` | `Clearable` | No | — | Add a clear action on the input that clears the value.  You should always use `while-editing` if you want the input t... |
| `customRenderMenu` | `(props: CustomOptionsMenuProp<GenericOption | GenericGetOptionsValue, GenericOptionValue>) => ReactElement<...>` | No | — | Override the content rendered in the menu. |
| `debounce` | `number` | No | `300` | Debounce in milliseconds for getOptions |
| `description` | `ReactNode` | No | — | Further description of the input, can be used for a hint. |
| `initialOptions` | `GenericOption[]` | No | — | Initial options to display in the autocomplete. |
| `inputRef` | `RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>` | No | — | @deprecated Use `ref` instead. |
| `invalid` | `boolean` | No | — | Highlights the field red to indicate an error. |
| `name` | `string` | No | — | Name of the input. |
| `onBlur` | `(event?: FocusEvent<Element, Element>) => void` | No | — | Blur callback. |
| `onFocus` | `(event?: FocusEvent<Element, Element>) => void` | No | — | Focus callback. |
| `prefix` | `Affix` | No | — | Adds a prefix label and icon to the field |
| `ref` | `Ref<InputTextRef>` | No | — | Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (... |
| `size` | `"small" | "large"` | No | — | Adjusts the interface to either have small or large spacing. |
| `suffix` | `{ onClick: () => void; readonly ariaLabel: string; readonly icon: IconNames; readonly label?: string; } | { onClick?: never; ariaLabel?: never; readonly label?: string; readonly icon?: IconNames; }` | No | — | Adds a suffix label and icon with an optional action to the field |
| `validations` | `RegisterOptions` | No | — | Validations to run on the input. |
| `version` | `1` | No | `1` | Version of the component to use. |
