/* Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React, { useState, ReactNode, ChangeEvent, KeyboardEvent, useRef, ReactElement } from "react"; import classNames from "classnames"; import Autocompleter from "../../autocomplete/AutocompleteProvider"; import { Key } from "../../Keyboard"; import { ICompletion } from "../../autocomplete/Autocompleter"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; import { Icon as PillRemoveIcon } from "../../../res/img/icon-pill-remove.svg"; import { Icon as SearchIcon } from "../../../res/img/element-icons/roomlist/search.svg"; import useFocus from "../../hooks/useFocus"; interface AutocompleteInputProps { provider: Autocompleter; placeholder: string; selection: ICompletion[]; onSelectionChange: (selection: ICompletion[]) => void; maxSuggestions?: number; renderSuggestion?: (s: ICompletion) => ReactElement; renderSelection?: (m: ICompletion) => ReactElement; additionalFilter?: (suggestion: ICompletion) => boolean; } export const AutocompleteInput: React.FC = ({ provider, renderSuggestion, renderSelection, maxSuggestions = 5, placeholder, onSelectionChange, selection, additionalFilter, }) => { const [query, setQuery] = useState(""); const [suggestions, setSuggestions] = useState([]); const [isFocused, onFocusChangeHandlerFunctions] = useFocus(); const editorContainerRef = useRef(null); const editorRef = useRef(null); const focusEditor = (): void => { editorRef?.current?.focus(); }; const onQueryChange = async (e: ChangeEvent): Promise => { const value = e.target.value.trim(); setQuery(value); let matches = await provider.getCompletions( query, { start: query.length, end: query.length }, true, maxSuggestions, ); if (additionalFilter) { matches = matches.filter(additionalFilter); } setSuggestions(matches); }; const onClickInputArea = (): void => { focusEditor(); }; const onKeyDown = (e: KeyboardEvent): void => { const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey; // when the field is empty and the user hits backspace remove the right-most target if (!query && selection.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) { removeSelection(selection[selection.length - 1]); } }; const toggleSelection = (completion: ICompletion): void => { const newSelection = [...selection]; const index = selection.findIndex((selection) => selection.completionId === completion.completionId); if (index >= 0) { newSelection.splice(index, 1); } else { newSelection.push(completion); } onSelectionChange(newSelection); focusEditor(); setQuery(""); setSuggestions([]); }; const removeSelection = (completion: ICompletion): void => { const newSelection = [...selection]; const index = selection.findIndex((selection) => selection.completionId === completion.completionId); if (index >= 0) { newSelection.splice(index, 1); onSelectionChange(newSelection); } }; const hasPlaceholder = (): boolean => selection.length === 0 && query.length === 0; return (
0, })} onClick={onClickInputArea} data-testid="autocomplete-editor" > {selection.map((item) => ( ))}
{isFocused && suggestions.length ? (
{suggestions.map((item) => ( ))}
) : null}
); }; type SelectionItemProps = { item: ICompletion; onClick: (completion: ICompletion) => void; render?: (completion: ICompletion) => ReactElement; }; const SelectionItem: React.FC = ({ item, onClick, render }) => { const withContainer = (children: ReactNode): ReactElement => ( {children} onClick(item)} data-testid={`autocomplete-selection-remove-button-${item.completionId}`} > ); if (render) { return withContainer(render(item)); } return withContainer({item.completion}); }; type SuggestionItemProps = { item: ICompletion; selection: ICompletion[]; onClick: (completion: ICompletion) => void; render?: (completion: ICompletion) => ReactElement; }; const SuggestionItem: React.FC = ({ item, selection, onClick, render }) => { const isSelected = selection.some((selection) => selection.completionId === item.completionId); const classes = classNames({ "mx_AutocompleteInput_suggestion": true, "mx_AutocompleteInput_suggestion--selected": isSelected, }); const withContainer = (children: ReactNode): ReactElement => (
{ event.preventDefault(); onClick(item); }} data-testid={`autocomplete-suggestion-item-${item.completionId}`} > {children}
); if (render) { return withContainer(render(item)); } return withContainer( <> {item.completion} {item.completionId} , ); };