<commit_before>import React, { useState, useEffect, useRef, useCallback } from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
import SelectTextbox, {
  formInputPropTypes,
} from "../select-textbox/select-textbox.component";
import guid from "../../../utils/helpers/guid";
import withFilter from "../utils/with-filter.hoc";
import StyledSelect from "../select.style";
import SelectList from "../select-list/select-list.component";
import isExpectedOption from "../utils/is-expected-option";

const FilterableSelectList = withFilter(SelectList);

const FilterableSelect = React.forwardRef(
  (
    {
      value,
      defaultValue,
      id,
      name,
      children,
      onOpen,
      onChange,
      onClick,
      onKeyDown,
      onFocus,
      onBlur,
      openOnFocus,
      noResultsMessage,
      disablePortal,
      listActionButton,
      onListAction,
      isLoading,
      disabled,
      readOnly,
      onListScrollBottom,
      tableHeader,
      multiColumn,
      ...textboxProps
    },
    inputRef
  ) => {
    const selectListId = useRef(guid());
    const labelId = useRef(guid());
    const containerRef = useRef();
    const listboxRef = useRef();
    const isControlled = useRef(value !== undefined);
    const isMouseDownReported = useRef(false);
    const isInputFocused = useRef(false);
    const isMouseDownOnInput = useRef(false);
    const [textboxRef, setTextboxRef] = useState();
    const [isOpen, setOpen] = useState(false);
    const [textValue, setTextValue] = useState("");
    const [selectedValue, setSelectedValue] = useState("");
    const [highlightedValue, setHighlightedValue] = useState("");
    const [filterText, setFilterText] = useState("");

    const createCustomEvent = useCallback(
      (newValue) => {
        const customEvent = {
          target: {
            ...(name && { name }),
            ...(id && { id }),
            value: newValue,
          },
        };

        return customEvent;
      },
      [name, id]
    );

    const triggerChange = useCallback(
      (newValue) => {
        if (onChange) {
          onChange(createCustomEvent(newValue));
        }
      },
      [onChange, createCustomEvent]
    );

    const updateValues = useCallback(
      (newFilterText, isDeleteEvent) => {
        setSelectedValue((previousValue) => {
          const match = findElementWithMatchingText(newFilterText, children);
          const isFilterCleared = isDeleteEvent && newFilterText === "";

          if (!match || isFilterCleared) {
            setTextValue(newFilterText);
            triggerChange("");

            return "";
          }

          if (isDeleteEvent) {
            setTextValue(newFilterText);

            return match.props.value;
          }

          triggerChange(match.props.value);

          if (
            match.props.text
              .toLowerCase()
              .startsWith(newFilterText.toLowerCase())
          ) {
            setTextValue(match.props.text);
          } else {
            setTextValue(newFilterText);
          }

          if (isControlled.current) {
            return previousValue;
          }

          setHighlightedValue(match.props.value);

          return match.props.value;
        });
      },
      [children, triggerChange]
    );

    const setMatchingText = useCallback(
      (newValue, isClosing) => {
        const matchingOption = React.Children.toArray(children).find((child) =>
          isExpectedOption(child, newValue)
        );

        if (!matchingOption || matchingOption.props.text === undefined) {
          setTextValue("");
        } else if (
          isClosing ||
          matchingOption.props.text
            .toLowerCase()
            .startsWith(filterText.toLowerCase())
        ) {
          setTextValue(matchingOption.props.text);
        }
      },
      [children, filterText]
    );

    const handleTextboxChange = useCallback(
      (event) => {
        const newValue = event.target.value;
        const isDeleteEvent =
          event.nativeEvent.inputType === "deleteContentBackward" ||
          event.nativeEvent.inputType === "deleteContentForward" ||
          event.nativeEvent.inputType === "delete";

        updateValues(newValue, isDeleteEvent);
        setFilterText(newValue);
        setOpen(true);
      },
      [updateValues]
    );

    const fillLastFilterCharacter = useCallback(
      (key) => {
        setFilterText((previousFilterText) => {
          if (
            previousFilterText.length === textValue.length - 1 &&
            key === textValue.slice(-1)
          ) {
            return textValue;
          }

          return previousFilterText;
        });
      },
      [textValue]
    );

    const handleTextboxKeydown = useCallback(
      (event) => {
        const { key } = event;

        if (onKeyDown) {
          onKeyDown(event);
        }

        if (readOnly) {
          return;
        }

        if (!event.defaultPrevented && isNavigationKey(key)) {
          event.preventDefault();
          setOpen(true);
        }

        fillLastFilterCharacter(key);
      },
      [fillLastFilterCharacter, onKeyDown, readOnly]
    );

    const handleGlobalClick = useCallback(
      (event) => {
        const notInContainer =
          containerRef.current && !containerRef.current.contains(event.target);
        const notInList =
          listboxRef.current && !listboxRef.current.contains(event.target);

        isMouseDownReported.current = false;

        if (notInContainer && notInList) {
          setMatchingText(selectedValue, true);
          setOpen(false);
        }
      },
      [setMatchingText, selectedValue]
    );

    useEffect(() => {
      const newValue = value || defaultValue;
      const modeSwitchedMessage =
        "Input elements should not switch from uncontrolled to controlled (or vice versa). " +
        "Decide between using a controlled or uncontrolled input element for the lifetime of the component";
      const onChangeMissingMessage =
        "onChange prop required when using a controlled input element";

      invariant(
        isControlled.current === (value !== undefined),
        modeSwitchedMessage
      );
      invariant(
        !isControlled.current || (isControlled.current && onChange),
        onChangeMissingMessage
      );

      setSelectedValue((prevValue) => {
        if (isControlled.current && prevValue !== newValue) {
          setMatchingText(newValue);
        }

        return newValue;
      });

      setHighlightedValue(newValue);
      // prevent value update on filter change
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [value, defaultValue, onChange, children]);

    useEffect(() => {
      if (!isOpen) {
        setFilterText("");
      } else if (onOpen) {
        onOpen();<commit_msg>fix(filterable-select): text value cleared when option list changes<commit_after>import React, { useState, useEffect, useRef, useCallback } from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
import SelectTextbox, {
  formInputPropTypes,
} from "../select-textbox/select-textbox.component";
import guid from "../../../utils/helpers/guid";
import withFilter from "../utils/with-filter.hoc";
import StyledSelect from "../select.style";
import SelectList from "../select-list/select-list.component";
import isExpectedOption from "../utils/is-expected-option";

const FilterableSelectList = withFilter(SelectList);

const FilterableSelect = React.forwardRef(
  (
    {
      value,
      defaultValue,
      id,
      name,
      children,
      onOpen,
      onChange,
      onClick,
      onKeyDown,
      onFocus,
      onBlur,
      openOnFocus,
      noResultsMessage,
      disablePortal,
      listActionButton,
      onListAction,
      isLoading,
      disabled,
      readOnly,
      onListScrollBottom,
      tableHeader,
      multiColumn,
      ...textboxProps
    },
    inputRef
  ) => {
    const selectListId = useRef(guid());
    const labelId = useRef(guid());
    const containerRef = useRef();
    const listboxRef = useRef();
    const isControlled = useRef(value !== undefined);
    const isMouseDownReported = useRef(false);
    const isInputFocused = useRef(false);
    const isMouseDownOnInput = useRef(false);
    const [textboxRef, setTextboxRef] = useState();
    const [isOpen, setOpen] = useState(false);
    const [textValue, setTextValue] = useState("");
    const [selectedValue, setSelectedValue] = useState("");
    const [highlightedValue, setHighlightedValue] = useState("");
    const [filterText, setFilterText] = useState("");

    const createCustomEvent = useCallback(
      (newValue) => {
        const customEvent = {
          target: {
            ...(name && { name }),
            ...(id && { id }),
            value: newValue,
          },
        };

        return customEvent;
      },
      [name, id]
    );

    const triggerChange = useCallback(
      (newValue) => {
        if (onChange) {
          onChange(createCustomEvent(newValue));
        }
      },
      [onChange, createCustomEvent]
    );

    const updateValues = useCallback(
      (newFilterText, isDeleteEvent) => {
        setSelectedValue((previousValue) => {
          const match = findElementWithMatchingText(newFilterText, children);
          const isFilterCleared = isDeleteEvent && newFilterText === "";

          if (!match || isFilterCleared) {
            setTextValue(newFilterText);
            triggerChange("");

            return "";
          }

          if (isDeleteEvent) {
            setTextValue(newFilterText);

            return match.props.value;
          }

          triggerChange(match.props.value);

          if (
            match.props.text
              .toLowerCase()
              .startsWith(newFilterText.toLowerCase())
          ) {
            setTextValue(match.props.text);
          } else {
            setTextValue(newFilterText);
          }

          if (isControlled.current) {
            return previousValue;
          }

          setHighlightedValue(match.props.value);

          return match.props.value;
        });
      },
      [children, triggerChange]
    );

    const setMatchingText = useCallback(
      (newValue, isClosing) => {
        const matchingOption = React.Children.toArray(children).find((child) =>
          isExpectedOption(child, newValue)
        );

        if (!matchingOption || matchingOption.props.text === undefined) {
          setTextValue("");
        } else if (
          isClosing ||
          matchingOption.props.text
            .toLowerCase()
            .startsWith(filterText.toLowerCase())
        ) {
          setTextValue(matchingOption.props.text);
        }
      },
      [children, filterText]
    );

    const handleTextboxChange = useCallback(
      (event) => {
        const newValue = event.target.value;
        const isDeleteEvent =
          event.nativeEvent.inputType === "deleteContentBackward" ||
          event.nativeEvent.inputType === "deleteContentForward" ||
          event.nativeEvent.inputType === "delete";

        updateValues(newValue, isDeleteEvent);
        setFilterText(newValue);
        setOpen(true);
      },
      [updateValues]
    );

    const fillLastFilterCharacter = useCallback(
      (key) => {
        setFilterText((previousFilterText) => {
          if (
            previousFilterText.length === textValue.length - 1 &&
            key === textValue.slice(-1)
          ) {
            return textValue;
          }

          return previousFilterText;
        });
      },
      [textValue]
    );

    const handleTextboxKeydown = useCallback(
      (event) => {
        const { key } = event;

        if (onKeyDown) {
          onKeyDown(event);
        }

        if (readOnly) {
          return;
        }

        if (!event.defaultPrevented && isNavigationKey(key)) {
          event.preventDefault();
          setOpen(true);
        }

        fillLastFilterCharacter(key);
      },
      [fillLastFilterCharacter, onKeyDown, readOnly]
    );

    const handleGlobalClick = useCallback(
      (event) => {
        const notInContainer =
          containerRef.current && !containerRef.current.contains(event.target);
        const notInList =
          listboxRef.current && !listboxRef.current.contains(event.target);

        isMouseDownReported.current = false;

        if (notInContainer && notInList) {
          setMatchingText(selectedValue, true);
          setOpen(false);
        }
      },
      [setMatchingText, selectedValue]
    );

    useEffect(() => {
      const newValue = value || defaultValue;
      const modeSwitchedMessage =
        "Input elements should not switch from uncontrolled to controlled (or vice versa). " +
        "Decide between using a controlled or uncontrolled input element for the lifetime of the component";
      const onChangeMissingMessage =
        "onChange prop required when using a controlled input element";

      invariant(
        isControlled.current === (value !== undefined),
        modeSwitchedMessage
      );
      invariant(
        !isControlled.current || (isControlled.current && onChange),
        onChangeMissingMessage
      );

      setSelectedValue((prevValue) => {
        if (value && isControlled.current && prevValue !== newValue) {
          setMatchingText(newValue);
        }

        return newValue;
      });

      setHighlightedValue(newValue);
      // prevent value update on filter change
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [value, defaultValue, onChange, children]);

    useEffect(() => {
      if (!isOpen) {
        setFilterText("");
      } else if (onOpen) {
        onOpen();
{'fraction_non_alphanumeric': 0.11282496922297053, 'fraction_numerical': 0.0002896661597508871, 'mean_word_length': 2.122315170698621, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3404112', 'n_tokens_mistral': 3716, 'n_tokens_neox': 3679, 'n_words': 759}