ソースを参照

Merge pull request #32825 from overleaf/mfb-autocomplete-component-design-review

[web] Autocomplete component design review fixes

GitOrigin-RevId: f598f46a770c94512de5beddb8ff1997df354fae
Maria Florencia Besteiro Gonzalez 3 ヶ月 前
コミット
df3f50d10b

+ 82 - 60
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 && (
+        <li role="separator" className="dropdown-divider" />
+      )}
+      <li
+        {...getItemProps({
+          item: { type: 'create', inputValue: internalInputValue },
+          index,
+        })}
+      >
+        <OLButton
+          variant="ghost"
+          size="sm"
+          className={classnames('w-100', 'justify-content-start', {
+            'dropdown-item-highlighted': highlightedIndex === index,
+          })}
+        >
+          <span className="text-muted">{createOptionPrefix} </span>
+          <strong>'{internalInputValue}'</strong>
+        </OLButton>
+      </li>
+      {hasGroupedItems && !expandUp && (
+        <li role="separator" className="dropdown-divider" />
+      )}
+    </>
+  )
+
   const handleClear = () => {
     selectItem(null)
     setInternalInputValue('')
     onChange('')
   }
 
-  const getSearchBar = () => (
+  const renderSearchBar = () => (
     <div className={classnames({ 'mb-3': !expandUp, 'mt-3': expandUp })}>
       <OLFormLabel
         {...getLabelProps()}
@@ -196,7 +247,7 @@ function OLAutocompleteInternal({
     </div>
   )
 
-  const getResultsList = () => (
+  const renderResultsList = () => (
     <>
       <ul
         {...getMenuProps()}
@@ -207,11 +258,13 @@ function OLAutocompleteInternal({
       >
         {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 (
                 <Fragment key={`${item.value}${index}`}>
@@ -230,14 +283,15 @@ function OLAutocompleteInternal({
                         value: item.value,
                         label: item.label,
                       },
-                      index,
+                      index: displayIndex,
                     })}
                   >
                     <DropdownItem
                       as="span"
                       role={undefined}
                       className={classnames({
-                        'dropdown-item-highlighted': highlightedIndex === index,
+                        'dropdown-item-highlighted':
+                          highlightedIndex === displayIndex,
                       })}
                     >
                       {item.label}
@@ -246,49 +300,24 @@ function OLAutocompleteInternal({
                 </Fragment>
               )
             })}
-            {showCreateOption && (
-              <>
-                <li role="separator" className="dropdown-divider" />
-                <li
-                  {...getItemProps({
-                    item: {
-                      type: 'create',
-                      inputValue: internalInputValue,
-                    },
-                    index: displayItems.length - 1,
-                  })}
-                >
-                  <DropdownItem
-                    as="span"
-                    role={undefined}
-                    className={classnames({
-                      'dropdown-item-highlighted':
-                        highlightedIndex === displayItems.length - 1,
-                    })}
-                  >
-                    <span className="text-muted">{createOptionPrefix} </span>
-                    <strong>'{internalInputValue}'</strong>
-                  </DropdownItem>
-                </li>
-              </>
+            {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 (
+                <Fragment key={`create-${item.inputValue}-${index}`}>
+                  {renderCreateOption(index)}
+                </Fragment>
+              )
+            }
 
             return (
               <li
-                key={
-                  item.type === 'create'
-                    ? `create-${item.inputValue}-${index}`
-                    : `${item.value}${index}`
-                }
-                {...getItemProps({
-                  item,
-                  index,
-                })}
+                key={`${item.value}${index}`}
+                {...getItemProps({ item, index })}
               >
                 <DropdownItem
                   as="span"
@@ -297,14 +326,7 @@ function OLAutocompleteInternal({
                     'dropdown-item-highlighted': highlightedIndex === index,
                   })}
                 >
-                  {isCreateOption ? (
-                    <>
-                      <span className="text-muted">{createOptionPrefix} </span>
-                      <strong>'{displayValue}'</strong>
-                    </>
-                  ) : (
-                    displayValue
-                  )}
+                  {item.label}
                 </DropdownItem>
               </li>
             )
@@ -318,13 +340,13 @@ function OLAutocompleteInternal({
     <div className={classnames('dropdown', 'd-block', 'ol-autocomplete')}>
       {expandUp ? (
         <>
-          {getResultsList()}
-          {getSearchBar()}
+          {renderResultsList()}
+          {renderSearchBar()}
         </>
       ) : (
         <>
-          {getSearchBar()}
-          {getResultsList()}
+          {renderSearchBar()}
+          {renderResultsList()}
         </>
       )}
     </div>