import { EuiTitle, EuiInputPopover, EuiSelectable } from "@elastic/eui"; import React, { useEffect, useRef, useState } from "react"; import { SuggestionModes, TagSuggestionInstance, } from "../hooks/useSearchInputWithTags"; interface TagSearchInterface { currentTag: string; tagsString: string; setTagsString: (tagsString: string) => void; acceptSuggestion: (suggestion: TagSuggestionInstance) => void; tagSuggestions: TagSuggestionInstance[]; suggestionMode: SuggestionModes; setCursorPosition: (position: number | undefined) => void; } interface SelectableOption { label: string; checked?: "on" | "off" | undefined; suggestion: TagSuggestionInstance; } // Helper Functions const suggestionFormatter = (item: TagSuggestionInstance) => { return { label: item.suggestion, suggestion: item, showIcons: false, append: {item.description}, }; }; const getCursorPosition = ( inputNode: React.MutableRefObject ) => { return inputNode.current?.selectionStart || undefined; }; const computePlaceholderText = ( tagSuggestions: TagSuggestionInstance[] | undefined ) => { return !tagSuggestions ? "" : "e.g. " + tagSuggestions .slice(0, 2) .map((s) => `"${s.suggestion}"`) .join(" or "); }; const generateResultsCount = ( currentTag: string, suggestionMode: SuggestionModes, tagSuggestions: TagSuggestionInstance[] ) => { let resultsCount = undefined; const currentTagIsEmpty = currentTag.length <= 0; const currentTagHasNoValue = currentTag.split(":")[1] === ""; const operatingWord = currentTagIsEmpty || currentTagHasNoValue ? "possible" : "matching"; const counterWord = suggestionMode === "KEY" ? `key` : `value`; if (tagSuggestions.length > 0) { const isPlural = tagSuggestions.length > 1 ? "s" : ""; resultsCount = ( {`${tagSuggestions.length} ${operatingWord} ${counterWord}${isPlural}`} ); } return resultsCount; }; // Hooks const useInputHack = ( setTagsString: (s: string) => void, setCursorPosition: (n: number | undefined) => void ) => { // HACK --- route around the lack of onChange // See: https://github.com/elastic/eui/issues/5651 const inputNode = useRef(null); useEffect(() => { const cb = () => { const s: string = inputNode.current?.value || ""; setTagsString(s); setCursorPosition(getCursorPosition(inputNode)); }; const copiedNode = inputNode.current; if (copiedNode) { copiedNode.addEventListener("input", cb); } return () => { if (copiedNode) { copiedNode.removeEventListener("input", cb); } }; }, [inputNode, setTagsString, setCursorPosition]); return inputNode; }; const useSelectableOptions = ( tagSuggestions: TagSuggestionInstance[], acceptSuggestion: (suggestion: TagSuggestionInstance) => void ) => { const [options, setOptions] = useState( tagSuggestions ? tagSuggestions.map(suggestionFormatter) : [] ); const onSelectableChange = (options: SelectableOption[]) => { // Get the thing that just got "checked" const clickedItem = options.find((option) => option.checked === "on"); if (clickedItem) { acceptSuggestion(clickedItem.suggestion); } setOptions(options); }; useEffect(() => { // Update options when new set of suggestions are passed down setOptions(tagSuggestions.map(suggestionFormatter)); }, [tagSuggestions, setOptions]); return { options, onSelectableChange, }; }; const TagSearch = ({ currentTag, tagsString, setTagsString, acceptSuggestion, tagSuggestions, suggestionMode, setCursorPosition, }: TagSearchInterface) => { // HACK --- route around the lack of onChange const inputNode = useInputHack(setTagsString, setCursorPosition); // Handling Suggestion Options const { options, onSelectableChange } = useSelectableOptions( tagSuggestions, // Gets turned into options acceptSuggestion // Get triggered when an option is selected ); // Using EuiInputPopover: https://elastic.github.io/eui/#/layout/popover const [hasFocus, setHasFocus] = useState(false); // Props for EuiFieldSearch const searchProps = { value: tagsString, inputRef: (node: HTMLInputElement | null) => { // HTMLInputElement is hooked into useInputHack inputNode.current = node; }, onFocus: () => { setHasFocus(true); }, fullWidth: true, placeholder: computePlaceholderText(tagSuggestions), }; const resultsCount = generateResultsCount( currentTag, suggestionMode, tagSuggestions ); return ( <>

Filter by Tags

{ setHasFocus(true); }} onBlur={() => { setHasFocus(false); }} searchable={true} isPreFiltered={true} searchProps={searchProps} aria-label="Filter by " onChange={onSelectableChange} options={options} singleSelection={true} listProps={{ bordered: true }} > {(list, search) => { return ( {search}} isOpen={hasFocus} closePopover={() => { setHasFocus(false); }} > {resultsCount} {list} ); }} ); }; export default TagSearch;