From 1ecaf0164ff476bcd4db54fdc9c2988f67274452 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 09:27:38 +0200 Subject: [PATCH 1/6] Fix openMenuOnFocus always false This was already false per default, but was set to false explicitly to fix a tab navigation issue in the metadata tab of events/series dialog. So this sets openMenuOnFocus to false in RenderFields, so that it may be true in other components again --- src/components/shared/DropDown.tsx | 1 - src/components/shared/wizard/RenderField.tsx | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 03dd4a8078..372c7a8b75 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -229,7 +229,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} /> From efc8d55c37676dac6b65c870f224a0ad6b05ec1d Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 09:44:38 +0200 Subject: [PATCH 2/6] Always copy options array Before we only made a copy if skipTranslate was true. This could have cause errors with immutable arrays, so it is probably better to copy everytime. --- src/components/shared/DropDown.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 372c7a8b75..2c7d1c0481 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -91,10 +91,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) { From 4fb640213db74cf32f81d91b8b39037e0c4b9897 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:08:38 +0200 Subject: [PATCH 3/6] Remove unused auto check menuPlacement defaults to this anyway, no need to check here again --- src/components/shared/DropDown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 2c7d1c0481..9de3f9300b 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -217,7 +217,7 @@ const DropDown = ({ onMenuClose: () => openMenu(false), isDisabled: disabled, openMenuOnFocus: openMenuOnFocus, - menuPlacement: menuPlacement ?? "auto", + menuPlacement: menuPlacement, components: { MenuList }, }; From 181ab39fb712254b7308e639dfeb82741bf33198 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:18:40 +0200 Subject: [PATCH 4/6] Make default Dropdown ref stable By using useRef instead of createRef we can avoid recreating the ref all the time. Should result in a very, very minor performance boost. --- src/components/shared/DropDown.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 9de3f9300b..41d5509283 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { dropDownSpacingTheme, @@ -20,7 +20,7 @@ export type DropDownOption = { * This component renders a dropdown menu using react-select */ const DropDown = ({ - ref = React.createRef, boolean, GroupBase>>>(), + ref, value, text, options, @@ -69,7 +69,8 @@ const DropDown = ({ }) => { const { t } = useTranslation(); - const selectRef = ref; + const internalRef = useRef, boolean, GroupBase>> | null>(null); + const selectRef = ref ?? internalRef; const style = dropDownStyle(customCSS ?? {}); From f8b467cad8824cc5c0a0fb9d9fb24ddecfaeb999 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:32:14 +0200 Subject: [PATCH 5/6] Keep virtualization components more stable Avoid potential rerenders for virtualized options which can end up quite costly for larger lists. Could also help with flickering and focus reset issues somewhat. --- src/components/shared/DropDown.tsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 41d5509283..823d536452 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { dropDownSpacingTheme, @@ -16,6 +16,17 @@ 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 */ @@ -132,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) ? ( @@ -151,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) { From b425501601aff451c0e0a6a67a942e9575de3bf4 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:47:04 +0200 Subject: [PATCH 6/6] Properly debounce async option load Previous code was just delaying the fetch by a second. It would still fire all accumulated request anyway (e.g. requests for "C", "Co, "Cou", "Cour", "Cours" and "Course", instead of just "Course". Implements debouncing, which should make for less load and cleaner rendering steps when searching in an async dropdown. --- src/components/shared/DropDown.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 823d536452..be062e5c5b 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -173,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 = (