diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 03dd4a8078..be062e5c5b 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { dropDownSpacingTheme, @@ -16,11 +16,22 @@ export type DropDownOption = { order?: number } +function MenuListRow({ + index, + names, + style, +}: RowComponentProps<{ + names: string[]; +}>) { + const name = names[index]; + return
{name}
; +} + /** * This component renders a dropdown menu using react-select */ const DropDown = ({ - ref = React.createRef, boolean, GroupBase>>>(), + ref, value, text, options, @@ -69,7 +80,8 @@ const DropDown = ({ }) => { const { t } = useTranslation(); - const selectRef = ref; + const internalRef = useRef, boolean, GroupBase>> | null>(null); + const selectRef = ref ?? internalRef; const style = dropDownStyle(customCSS ?? {}); @@ -91,10 +103,11 @@ const DropDown = ({ required: boolean, ) => { // Translate - // Translating is expensive, skip it if it is not required - if (!skipTranslate) { - unformattedOptions = unformattedOptions.map(option => ({ ...option, label: t(option.label as ParseKeys) })); - } + // Translating is expensive, skip it if it is not required. + // Either way, copy the array so the input is not transmuted later. + unformattedOptions = skipTranslate + ? [...unformattedOptions] + : unformattedOptions.map(option => ({ ...option, label: t(option.label as ParseKeys) })); // Add "No value" option if (!required) { @@ -130,7 +143,7 @@ const DropDown = ({ /** * Custom component for list virtualization */ - const MenuList = (props: MenuListProps, false>) => { + const MenuList = useCallback((props: MenuListProps, false>) => { const { children, maxHeight } = props; return Array.isArray(children) ? ( @@ -149,18 +162,7 @@ const DropDown = ({ /> ) : null; - }; - - function MenuListRow({ - index, - names, - style, - }: RowComponentProps<{ - names: string[]; - }>) { - const name = names[index]; - return
{name}
; - } + }, [itemHeight]); const filterOptions = (inputValue: string) => { if (options) { @@ -171,14 +173,23 @@ const DropDown = ({ return []; }; + const debounceTimeoutRef = useRef>(undefined); + + useEffect(() => { + return () => clearTimeout(debounceTimeoutRef.current); + }, []); + const loadOptionsAsync = (inputValue: string, callback: (options: DropDownOption[]) => void) => { - const timeout = async () => { - callback(formatOptions( - fetchOptions ? await fetchOptions(inputValue) : filterOptions(inputValue), - required, - )); - }; - setTimeout(() => { timeout(); }, 1000); + clearTimeout(debounceTimeoutRef.current); + debounceTimeoutRef.current = setTimeout(() => { + const timeout = async () => { + callback(formatOptions( + fetchOptions ? await fetchOptions(inputValue) : filterOptions(inputValue), + required, + )); + }; + void timeout(); + }, 1000); }; const loadOptions = ( @@ -216,7 +227,7 @@ const DropDown = ({ onMenuClose: () => openMenu(false), isDisabled: disabled, openMenuOnFocus: openMenuOnFocus, - menuPlacement: menuPlacement ?? "auto", + menuPlacement: menuPlacement, components: { MenuList }, }; @@ -229,7 +240,6 @@ const DropDown = ({ t("SELECT_NO_MATCHING_RESULTS")} /> ); diff --git a/src/components/shared/wizard/RenderField.tsx b/src/components/shared/wizard/RenderField.tsx index d2c72ea52f..eb6800cdae 100644 --- a/src/components/shared/wizard/RenderField.tsx +++ b/src/components/shared/wizard/RenderField.tsx @@ -422,7 +422,9 @@ const EditableSingleSelectDropDown = ({ } customCSS={{ isMetadataStyle: focused ? false : true, width: "100%" }} handleMenuIsOpen={(open: boolean) => setFocused(open)} - openMenuOnFocus + // Deliberately false: with the menu open, Tab breaks keyboard + // navigation through the metadata form fields (see f44c9b7). + openMenuOnFocus={false} autoFocus={isFirstField} skipTranslate={!metadataField.translatable} />