export function MultipleComboBox()

in apps/chat/src/components/Common/MultipleComboBox.tsx [81:367]


export function MultipleComboBox<T>({
  items,
  initialSelectedItems,
  placeholder,
  notFoundPlaceholder,
  itemRow,
  selectedItemRow,
  disabled,
  hasDeleteAll = false,
  itemHeightClassName,
  fontSize = 'text-sm',
  getItemLabel,
  getItemValue,
  onChangeSelectedItems,
  className,
  validationRegExp,
  handleError,
  handleClearError,
  hideSuggestions,
}: Props<T>) {
  const { t } = useTranslation(Translation.Common);
  const [inputValue, setInputValue] = useState<string | undefined>('');
  const [floatingWidth, setFloatingWidth] = useState(0);

  const inputRef = useRef<HTMLInputElement>(null);

  const { x, y, refs, strategy, update } = useFloating({
    placement: 'bottom-start',
    strategy: 'fixed',
    middleware: [
      size({
        apply({ rects }) {
          setFloatingWidth(rects.reference.width);
        },
      }),
      offset(4),
      flip(),
    ],
  });

  const {
    getSelectedItemProps,
    getDropdownProps,
    removeSelectedItem,
    selectedItems,
    addSelectedItem,
    setSelectedItems,
  } = useMultipleSelection({
    selectedItems: initialSelectedItems || [],
    onStateChange({ selectedItems: newSelectedItems, type }) {
      switch (type) {
        case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
        case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
        case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
        case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
          if (!newSelectedItems) {
            return;
          }
          onChangeSelectedItems(newSelectedItems);

          break;
        default:
          break;
      }
    },
  });

  const displayedItems = useMemo(
    () => getFilteredItems({ inputValue, getItemLabel, items, selectedItems }),
    [selectedItems, inputValue, items, getItemLabel],
  );

  const {
    isOpen,
    getMenuProps,
    getInputProps,
    highlightedIndex,
    getItemProps,
    selectedItem,
  } = useCombobox({
    items: displayedItems,
    itemToString: (item: T | null) => (item ? getItemLabel(item) : 'null item'),
    defaultHighlightedIndex: 0, // after selection, highlight the first item.
    selectedItem: null,
    inputValue,
    stateReducer(_, actionAndChanges) {
      const { changes, type } = actionAndChanges;

      switch (type) {
        case useCombobox.stateChangeTypes.InputKeyDownEnter:
        case useCombobox.stateChangeTypes.ItemClick:
          return {
            ...changes,
            isOpen: true, // keep the menu open after selection.
            highlightedIndex: 0, // with the first option highlighted.
          };
        default:
          return changes;
      }
    },
    onStateChange({
      inputValue: newInputValue,
      type,
      selectedItem: newSelectedItem = inputValue as T,
    }) {
      switch (type) {
        case useCombobox.stateChangeTypes.InputKeyDownEnter:
        case useCombobox.stateChangeTypes.ItemClick:
        case useCombobox.stateChangeTypes.InputBlur:
          if (!newSelectedItem) {
            return;
          }

          if (
            getItemLabel(newSelectedItem) &&
            !getItemLabel(newSelectedItem).trim()
          ) {
            return;
          }

          if (
            validationRegExp &&
            typeof newSelectedItem === 'string' &&
            !validationRegExp.test(newSelectedItem)
          ) {
            handleError?.();
            return;
          }

          addSelectedItem(newSelectedItem);
          onChangeSelectedItems([...(selectedItems ?? []), newSelectedItem]);
          setInputValue('');

          break;

        case useCombobox.stateChangeTypes.InputChange:
          handleClearError?.();
          setInputValue(newInputValue);
          break;
        default:
          break;
      }
    },
  });

  useLayoutEffect(() => {
    if (isOpen && refs.reference.current && refs.floating.current) {
      return autoUpdate(refs.reference.current, refs.floating.current, update);
    }
  }, [isOpen, update, refs.floating, refs.reference]);

  return (
    <div
      className={classNames(
        'relative w-full bg-transparent md:max-w-[205px]',
        className,
      )}
      data-qa="multiple-combobox"
    >
      <div className="flex w-full flex-col gap-1">
        <div
          ref={refs.reference as RefObject<HTMLDivElement>}
          onClick={() => {
            if (!inputRef.current) {
              return;
            }
            inputRef.current.focus();
          }}
          className="relative flex min-h-[31px] w-full flex-wrap gap-1 p-1"
        >
          {selectedItems &&
            selectedItems.map((selectedItemForRender, index) => {
              return (
                <Tooltip
                  key={`selected-item-${getItemLabel(
                    selectedItemForRender,
                  )}-${index}`}
                  tooltip={getItemLabel(selectedItemForRender).trim()}
                  contentClassName="text-xs"
                >
                  <span
                    className={classNames(
                      'flex items-center justify-between gap-2 rounded bg-accent-primary-alpha px-2 py-1.5',
                      itemHeightClassName ? itemHeightClassName : 'h-[23px]',
                    )}
                    {...getSelectedItemProps({
                      selectedItem: selectedItemForRender,
                      index,
                    })}
                  >
                    {selectedItemRow ? (
                      createElement(selectedItemRow, {
                        item: selectedItemForRender,
                      })
                    ) : (
                      <span className="max-w-[150px] truncate break-all text-xs">
                        {getItemLabel(selectedItemForRender)}
                      </span>
                    )}
                    <span
                      data-qa={`unselect-item-${getItemValue(
                        selectedItemForRender,
                      )}`}
                      className="cursor-pointer"
                      onClick={(e) => {
                        e.stopPropagation();
                        removeSelectedItem(selectedItemForRender);
                      }}
                    >
                      <IconX size={14} className="text-secondary" />
                    </span>
                  </span>
                </Tooltip>
              );
            })}
          <input
            name="option-input"
            disabled={disabled}
            placeholder={selectedItems.length ? '' : placeholder || ''}
            className={classNames(
              'w-full min-w-[10px] overflow-auto whitespace-break-spaces break-words bg-transparent outline-none placeholder:text-secondary',
              selectedItems.length ? 'pl-1' : 'pl-2',
              fontSize,
            )}
            {...getInputProps({
              ...getDropdownProps({
                preventKeyAction: isOpen,
                ref: inputRef,
              }),
            })}
          />
        </div>

        <ul
          className={classNames(
            'z-10 max-h-80 overflow-auto rounded bg-layer-3',
            (hideSuggestions || !isOpen) && 'hidden',
          )}
          {...getMenuProps(
            { ref: refs.floating as RefObject<HTMLUListElement> },
            { suppressRefError: true },
          )}
          style={{
            position: strategy,
            top: y ?? '',
            left: x ?? '',
            width: `${floatingWidth}px`,
          }}
        >
          {displayedItems?.length > 0
            ? displayedItems.map((item, index) => (
                <li
                  className={classNames(
                    'group flex min-h-[31px] w-full cursor-pointer flex-col justify-center whitespace-break-spaces break-words px-3 text-xs',
                    highlightedIndex === index && 'bg-accent-primary-alpha',
                    selectedItem === item && 'bg-accent-primary-alpha',
                  )}
                  key={`${getItemValue(item)}${index}`}
                  {...getItemProps({ item, index })}
                >
                  {itemRow
                    ? createElement(itemRow, { item })
                    : getItemLabel(item)}
                </li>
              ))
            : !!inputValue?.length && (
                <li className="px-3 py-2">
                  {notFoundPlaceholder || t('No available items')}
                </li>
              )}
        </ul>
      </div>
      {hasDeleteAll && selectedItems.length > 0 ? (
        <span
          className="cursor-pointer py-2"
          onClick={(e) => {
            e.stopPropagation();
            setSelectedItems([]);
            onChangeSelectedItems([]);
          }}
        >
          <IconX height={18} width={18} />
        </span>
      ) : null}
    </div>
  );
}