From df3f50d10be3e66cb1960cc0af404a7c49c3c46d Mon Sep 17 00:00:00 2001 From: Maria Florencia Besteiro Gonzalez Date: Mon, 20 Apr 2026 13:48:43 +0200 Subject: [PATCH] Merge pull request #32825 from overleaf/mfb-autocomplete-component-design-review [web] Autocomplete component design review fixes GitOrigin-RevId: f598f46a770c94512de5beddb8ff1997df354fae --- .../shared/components/ol/ol-autocomplete.tsx | 142 ++++++++++-------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/services/web/frontend/js/shared/components/ol/ol-autocomplete.tsx b/services/web/frontend/js/shared/components/ol/ol-autocomplete.tsx index 45b0a97206..9c057874bd 100644 --- a/services/web/frontend/js/shared/components/ol/ol-autocomplete.tsx +++ b/services/web/frontend/js/shared/components/ol/ol-autocomplete.tsx @@ -107,22 +107,23 @@ function OLAutocompleteInternal({ const showCreateOption = allowCreateForInput && internalInputValue && !exactMatch + const createDisplayItem: OLAutocompleteDisplayItem[] = showCreateOption + ? [{ type: 'create' as const, inputValue: internalInputValue }] + : [] + const displayItems: OLAutocompleteDisplayItem[] = [ + ...(expandUp ? [] : createDisplayItem), ...inputItems.map(item => ({ type: 'item' as const, value: item.value, label: item.label, })), - ...(showCreateOption - ? [ - { - type: 'create' as const, - inputValue: internalInputValue, - }, - ] - : []), + ...(expandUp ? createDisplayItem : []), ] + const getDisplayIndex = (inputItemIndex: number) => + !expandUp && showCreateOption ? inputItemIndex + 1 : inputItemIndex + const hasGroupedItems = inputItems.some(item => Boolean(item.group)) const { @@ -141,6 +142,28 @@ function OLAutocompleteInternal({ if (!item) return '' return item.type === 'create' ? item.inputValue : item.label }, + stateReducer: (_state, { type, changes }) => { + if (type === useCombobox.stateChangeTypes.InputChange) { + const newInputValue = changes.inputValue || '' + const newAllowCreate = + typeof allowCreate === 'function' + ? allowCreate(newInputValue) + : allowCreate + const hasExactMatch = items.some( + item => item.label.toLowerCase() === newInputValue.toLowerCase() + ) + const hasMatchingItems = items.some(item => + item.label.toLowerCase().includes(newInputValue.toLowerCase()) + ) + const newShowCreate = newAllowCreate && newInputValue && !hasExactMatch + return { + ...changes, + highlightedIndex: + !expandUp && newShowCreate && hasMatchingItems ? 1 : 0, + } + } + return changes + }, onSelectedItemChange: ({ selectedItem }) => { if (selectedItem) { if (selectedItem.type === 'create') { @@ -159,13 +182,41 @@ function OLAutocompleteInternal({ const shouldShowDropdown = isOpen && displayItems.length > 0 + const renderCreateOption = (index: number) => ( + <> + {hasGroupedItems && expandUp && ( +
  • + )} +
  • + + {createOptionPrefix} + '{internalInputValue}' + +
  • + {hasGroupedItems && !expandUp && ( +
  • + )} + + ) + const handleClear = () => { selectItem(null) setInternalInputValue('') onChange('') } - const getSearchBar = () => ( + const renderSearchBar = () => (
    ) - const getResultsList = () => ( + const renderResultsList = () => ( <>
      {hasGroupedItems ? ( <> + {!expandUp && showCreateOption && <>{renderCreateOption(0)}} {inputItems.map((item, index) => { const previousItem = inputItems[index - 1] const hasGroupHeader = item.group && (!previousItem || previousItem.group !== item.group) + const displayIndex = getDisplayIndex(index) return ( @@ -230,14 +283,15 @@ function OLAutocompleteInternal({ value: item.value, label: item.label, }, - index, + index: displayIndex, })} > {item.label} @@ -246,49 +300,24 @@ function OLAutocompleteInternal({ ) })} - {showCreateOption && ( - <> -
    • -
    • - - {createOptionPrefix} - '{internalInputValue}' - -
    • - + {expandUp && showCreateOption && ( + <>{renderCreateOption(displayItems.length - 1)} )} ) : ( displayItems.map((item, index) => { - const isCreateOption = item.type === 'create' - const displayValue = isCreateOption ? item.inputValue : item.label + if (item.type === 'create') { + return ( + + {renderCreateOption(index)} + + ) + } return (
    • - {isCreateOption ? ( - <> - {createOptionPrefix} - '{displayValue}' - - ) : ( - displayValue - )} + {item.label}
    • ) @@ -318,13 +340,13 @@ function OLAutocompleteInternal({
      {expandUp ? ( <> - {getResultsList()} - {getSearchBar()} + {renderResultsList()} + {renderSearchBar()} ) : ( <> - {getSearchBar()} - {getResultsList()} + {renderSearchBar()} + {renderResultsList()} )}