import React, { InputHTMLAttributes, ChangeEvent, FocusEvent, KeyboardEvent, Ref, useRef, useContext, useState, useEffect, ReactNode, useId, useMemo, useCallback, FC, } from 'react'; import classnames from 'classnames'; import { AutoAlign, AutoAlignInjectedProps } from './AutoAlign'; import { Button } from './Button'; import { FormElement, FormElementProps } from './FormElement'; import { Icon, IconCategory } from './Icon'; import { Spinner } from './Spinner'; import { isElInChildren, registerStyle } from './util'; import { ComponentSettingsContext } from './ComponentSettings'; import { useControlledValue, useEventCallback, useMergeRefs } from './hooks'; import { createFC } from './common'; import { Bivariant } from './typeUtils'; /** * */ export type LookupEntry = { label: string; value: string; icon?: string; scope?: string; category?: IconCategory; meta?: string; }; /** * */ export type LookupScope = { label: string; value: string; icon: string; category?: IconCategory; }; /** * Key handler configuration for keyboard navigation */ type KeyHandlerConfig = { type: 'search' | 'scope'; opened: boolean; onOpen: () => void; onClose: () => void; onNavigateDown: () => void; onNavigateUp: () => void; onSelect: () => void; isTabNavigationIgnored: (direction: 'forward' | 'backward') => boolean; onTabNavigation: (direction: 'forward' | 'backward') => void; }; /** * Custom hook for keyboard event handling in lookup components */ const useKeyHandler = (config: KeyHandlerConfig) => { return useEventCallback((e: KeyboardEvent) => { const { opened, onOpen, onClose, onNavigateDown, onNavigateUp, onSelect, isTabNavigationIgnored, onTabNavigation, } = config; switch (e.keyCode) { case 40: // ArrowDown e.preventDefault(); e.stopPropagation(); if (!opened) { onOpen(); } else { onNavigateDown(); } break; case 38: // ArrowUp e.preventDefault(); e.stopPropagation(); if (opened) { onNavigateUp(); } break; case 13: // Enter e.preventDefault(); e.stopPropagation(); onSelect(); break; case 27: // Escape e.preventDefault(); e.stopPropagation(); if (opened) { onClose(); } break; case 9: // Tab if (!isTabNavigationIgnored(e.shiftKey ? 'backward' : 'forward')) { e.preventDefault(); e.stopPropagation(); onTabNavigation(e.shiftKey ? 'backward' : 'forward'); } else { onClose(); } break; } }); }; /** * Props for LookupSelectedState component */ type LookupSelectedStateProps = { selected: LookupEntry; disabled?: boolean; listboxId: string; onRemoveSelection: () => void; }; /** * Component for displaying selected state */ const LookupSelectedState: FC = ({ selected, disabled, listboxId, onRemoveSelection, }) => { const formElementClassnames = classnames( 'slds-combobox__form-element', 'slds-input-has-icon', `slds-input-has-icon_${selected.icon ? 'left-' : ''}right` ); return ( {selected.icon && ( )} {selected.label} Remove selected option ); }; /** * Props for LookupScopeSelectorContainer component */ type LookupScopeSelectorContainerProps = { scopeListboxId: string; dropdownRef: React.MutableRefObject; children: React.ReactNode; } & AutoAlignInjectedProps; /** * Container component for scope selector dropdown with AutoAlign */ const LookupScopeSelectorContainer: FC = ({ scopeListboxId, dropdownRef, children, alignment, autoAlignContentRef, }) => { const [vertAlign, align] = alignment; const dropdownClassNames = classnames( 'slds-dropdown', vertAlign ? `slds-dropdown_${vertAlign}` : undefined, align ? `slds-dropdown_${align}` : undefined, 'slds-dropdown_length-with-icon-7', 'slds-dropdown_fluid' ); return ( {children} ); }; /** * Props for LookupScopeSelector component */ type LookupScopeSelectorProps = { portalClassName: string; scopes: LookupScope[]; targetScope?: string; disabled?: boolean; scopeListboxId: string; getScopeOptionId: (index: number) => string; onScopeMenuClick?: () => void; onScopeSelect?: (scope: string) => void; }; /** * Component for scope selector in multi-entity lookup */ const LookupScopeSelector: FC = ({ portalClassName, scopes, targetScope, disabled, scopeListboxId, getScopeOptionId, onScopeMenuClick: onScopeMenuClick_, onScopeSelect: onScopeSelect_, }) => { const dropdownRef = useRef(null); const [scopeOpened, setScopeOpened] = useState(false); const [scopeFocusedIndex, setScopeFocusedIndex] = useState(-1); const currentScope = scopes.find((scope) => scope.value === targetScope) ?? scopes[0]; // Scroll focused scope element into view const scrollFocusedScopeIntoView = useEventCallback( (nextFocusedIndex: number) => { if (nextFocusedIndex < 0 || !scopes) { return; } const scopeDropdown = document.getElementById(scopeListboxId); if (!scopeDropdown) { return; } const targetElement = document.getElementById( getScopeOptionId(nextFocusedIndex) ); if ( !(targetElement instanceof HTMLElement) || !scopeDropdown.contains(targetElement) ) { return; } targetElement.focus(); } ); const onScopeSelect = useEventCallback((scope: string) => { setScopeOpened(false); onScopeSelect_?.(scope); }); const onScopeMenuClick = useEventCallback(() => { setScopeOpened(!scopeOpened); onScopeMenuClick_?.(); }); const onScopeKeyDown = useKeyHandler({ type: 'scope', opened: scopeOpened, onOpen: () => { if (!scopes) return; setScopeOpened(true); setScopeFocusedIndex(0); }, onClose: () => { setScopeOpened(false); setScopeFocusedIndex(-1); }, onNavigateDown: () => { if (!scopes) return; const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1); setScopeFocusedIndex(nextIndex); scrollFocusedScopeIntoView(nextIndex); }, onNavigateUp: () => { if (!scopes) return; const prevIndex = Math.max(scopeFocusedIndex - 1, 0); setScopeFocusedIndex(prevIndex); scrollFocusedScopeIntoView(prevIndex); }, onSelect: () => { if (!scopes) return; if (scopeOpened && scopeFocusedIndex >= 0) { const selectedScope = scopes[scopeFocusedIndex]; if (selectedScope) { onScopeSelect(selectedScope.value); setScopeOpened(false); setScopeFocusedIndex(-1); } } else { setScopeOpened(!scopeOpened); } }, isTabNavigationIgnored: (direction) => { if (!scopes) { return false; } return ( scopeFocusedIndex === -1 || (direction === 'backward' && scopeFocusedIndex <= 0) || (direction === 'forward' && scopeFocusedIndex >= scopes.length - 1) ); }, onTabNavigation: (direction) => { if (!scopes) return; if (direction === 'backward') { if (scopeFocusedIndex <= 0) { setScopeOpened(false); setScopeFocusedIndex(-1); } else { const prevIndex = Math.max(scopeFocusedIndex - 1, 0); setScopeFocusedIndex(prevIndex); scrollFocusedScopeIntoView(prevIndex); } } else { if (scopeFocusedIndex >= scopes.length - 1) { setScopeOpened(false); setScopeFocusedIndex(-1); } else { const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1); setScopeFocusedIndex(nextIndex); scrollFocusedScopeIntoView(nextIndex); } } }, }); const onScopeBlur = useEventCallback((e: FocusEvent) => { if (e.relatedTarget !== null) { if (!scopes) { return; } const prevIndex = Math.max(scopeFocusedIndex - 1, 0); const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1); if ( e.relatedTarget.id === getScopeOptionId(prevIndex) || e.relatedTarget.id === getScopeOptionId(nextIndex) ) { // catch keyborad event return; } } setTimeout(() => { if (!isFocusedInComponent()) { setScopeOpened(false); } }, 10); }); const { getActiveElement } = useContext(ComponentSettingsContext); const isFocusedInComponent = useEventCallback(() => { const targetEl = getActiveElement(); return isElInChildren(dropdownRef.current, targetEl); }); return ( Filter Search by: {scopeOpened && ( {(injectedProps) => ( {scopes.map((scope, index) => ( onScopeSelect(scope.value)} > {scope.icon && ( )} {scope.label} ))} )} )} ); }; const SCOPE_INPUT_ZINDEX = 1; /** * Props for LookupSearchInput component */ type LookupSearchInputProps = { searchText: string; disabled?: boolean; opened: boolean; focusedValue?: string; iconAlign: 'left' | 'right'; comboboxId?: string; listboxId: string; optionIdPrefix: string; inputRef: Ref; onInputClick: () => void; onInputChange: (e: ChangeEvent) => void; onInputFocus: () => void; onInputBlur: (e: FocusEvent) => void; onInputKeyDown: (e: KeyboardEvent) => void; onSearchIconClick: () => void; } & Omit< InputHTMLAttributes, 'onChange' | 'onBlur' | 'onFocus' | 'onKeyDown' | 'value' >; /** * Component for search input */ const LookupSearchInput: FC = ({ searchText, disabled, opened, focusedValue, iconAlign, comboboxId, listboxId, optionIdPrefix, inputRef, onInputClick, onInputChange, onInputFocus, onInputBlur, onInputKeyDown, onSearchIconClick, ...rprops }) => { const hasValue = searchText.length > 0; const inputClassNames = classnames('slds-input', 'slds-combobox__input', { 'slds-has-focus': opened, 'slds-combobox__input-value': hasValue, }); const inputIconClasses = iconAlign === 'left' ? 'slds-combobox__form-element slds-input-has-icon slds-input-has-icon_left' : 'slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right'; return ( {iconAlign === 'left' && ( )} {iconAlign === 'right' && ( )} ); }; /** * Props for LookupOption component */ type LookupOptionProps = { entry: LookupEntry; isFocused: boolean; getOptionId: (value: string) => string; onOptionClick: (entry: LookupEntry) => void; onOptionFocus: (value: string) => void; }; /** * Component for individual lookup option */ const LookupOption: FC = ({ entry, isFocused, getOptionId, onOptionClick, onOptionFocus, }) => { const itemClassNames = classnames( 'slds-media', 'slds-media_center', 'slds-listbox__option', 'slds-listbox__option_entity', { 'slds-listbox__option_has-meta': entry.meta, 'slds-has-focus': isFocused, } ); return ( onOptionFocus(entry.value)} onClick={() => onOptionClick(entry)} > {entry.icon && ( )} {entry.label} {entry.meta && ( {entry.meta} )} ); }; /** * Props for LookupDropdownContainer component */ type LookupDropdownContainerProps = { listboxId: string; loading?: boolean; dropdownRef: Ref; children: React.ReactNode; } & AutoAlignInjectedProps; /** * Container component for dropdown with merged refs */ const LookupDropdownContainer: FC = ({ listboxId, loading, dropdownRef, children, alignment, autoAlignContentRef, }) => { const [vertAlign, align] = alignment; const dropdownClassNames = classnames( 'react-slds-lookup-dropdown-main', 'slds-dropdown', vertAlign ? `slds-dropdown_${vertAlign}` : undefined, align ? `slds-dropdown_${align}` : undefined, 'slds-dropdown_length-with-icon-7', 'slds-dropdown_fluid', 'slds-scrollable_none' ); return ( {children} ); }; /** * Props for LookupDropdown component */ type LookupDropdownProps = { portalClassName: string; opened: boolean; loading?: boolean; listboxId: string; dropdownRef: Ref; listHeaderRenderer?: ( rendererProps: React.HTMLAttributes ) => JSX.Element; listHeaderIdSeed: string; listFooterRenderer?: ( rendererProps: React.HTMLAttributes ) => JSX.Element; listFooterIdSeed: string; filteredData: LookupEntry[]; focusedValue?: string; getOptionId: (value: string) => string; onOptionClick: (entry: LookupEntry) => void; onOptionFocus: (value: string) => void; onBlur: (e: FocusEvent) => void; onKeyDown: (e: KeyboardEvent) => void; }; /** * Component for dropdown menu */ const LookupDropdown: FC = ({ portalClassName, opened, loading, listboxId, dropdownRef, listHeaderRenderer, listHeaderIdSeed, listFooterRenderer, listFooterIdSeed, filteredData, focusedValue, getOptionId, onOptionClick, onOptionFocus, onBlur, onKeyDown, }) => { if (!opened) return null; return ( {(injectedProps) => ( {listHeaderRenderer ? ( {listHeaderRenderer({ id: getOptionId(listHeaderIdSeed), className: 'slds-listbox__option', role: 'option', 'aria-selected': 'true', tabIndex: 0, onFocus: () => onOptionFocus(listHeaderIdSeed), })} ) : null} {filteredData.map((entry) => ( ))} {loading ? ( ) : null} {listFooterRenderer ? ( {listFooterRenderer({ id: getOptionId(listFooterIdSeed), className: 'slds-listbox__option', role: 'option', tabIndex: 0, onFocus: () => onOptionFocus(listFooterIdSeed), })} ) : null} )} ); }; // manually replaces where `max-height` is specified const LIST_PARENT_MAX_HEIGHT = 'unset'; const LIST_CONTENT_MAX_HEIGHT = 'calc((1.5rem + 1rem) * 7)'; // copied from `.slds-dropdown_length-with-icon-7` /** * */ function useInitComponentStyle() { useEffect(() => { registerStyle('lookup-search', [ [ '.react-slds-lookup-current-scope-icon-container', `{ top: 50%; transform: translateY(-50%); left: 14.2%; pointer-events: none; z-index: ${ SCOPE_INPUT_ZINDEX + 1 }; }`, ], ['.react-slds-lookup-scope-input:not(:disabled)', '{ cursor: pointer; }'], [ '.react-slds-lookup-scope-input.react-slds-lookup-scope-input.react-slds-lookup-scope-input', `{ padding-left: 1.5rem; z-index: ${SCOPE_INPUT_ZINDEX}; }`, ], [ '.react-slds-lookup-scope-down-icon-container', `{ line-height: 0; bottom: 17%; right: 15.7%; pointer-events: none; z-index: ${ SCOPE_INPUT_ZINDEX + 1 }; }`, ], [ '.react-slds-lookup-scope-down-icon-container .react-slds-lookup-scope-down-icon', '{ width: 0.8rem; height: 0.8rem; }', ], [ '.react-slds-lookup-dropdown-main', `{ max-height: ${LIST_PARENT_MAX_HEIGHT}; min-width: 15rem; }`, ], [ '.react-slds-lookup-list', `{ max-height: ${LIST_CONTENT_MAX_HEIGHT}; }`, ], ]); }, []); } /** * */ export type LookupProps = { label?: string; disabled?: boolean; required?: boolean; error?: FormElementProps['error']; iconAlign?: 'left' | 'right'; value?: string | null; defaultValue?: string | null; selected?: LookupEntry | null; defaultSelected?: LookupEntry | null; opened?: boolean; defaultOpened?: boolean; searchText?: string; defaultSearchText?: string; loading?: boolean; data?: LookupEntry[]; lookupFilter?: Bivariant< (entry: LookupEntry, searchText?: string, scope?: string) => boolean >; listHeaderRenderer?: ( rendererProps: React.HTMLAttributes ) => JSX.Element; listFooterRenderer?: ( rendererProps: React.HTMLAttributes ) => JSX.Element; tooltip?: ReactNode; tooltipIcon?: string; // Multi Entity Lookup (scope) props scopes?: LookupScope[]; targetScope?: string; defaultTargetScope?: string; cols?: number; elementRef?: Ref; inputRef?: Ref; dropdownRef?: Ref; onSearchTextChange?: (searchText: string) => void; onScopeMenuClick?: () => void; onScopeSelect?: (scope: string) => void; onLookupRequest?: (searchText: string) => void; onBlur?: () => void; onFocus?: () => void; onSelect?: Bivariant<(entry: LookupEntry | null) => void>; onValueChange?: (value: string | null, prevValue?: string | null) => void; onComplete?: (cancel?: boolean) => void; } & Omit< InputHTMLAttributes, 'onChange' | 'onBlur' | 'onFocus' | 'onSelect' | 'value' | 'defaultValue' >; /** * */ export const Lookup = createFC( (props) => { const { id: id_, value: value_, defaultValue, selected: selected_, defaultSelected, opened: opened_, defaultOpened, searchText: searchText_, defaultSearchText, cols, label, required, error, className, disabled, loading, lookupFilter, listHeaderRenderer, listFooterRenderer, data = [], tooltip, tooltipIcon, // Multi Entity Lookup props scopes, targetScope: targetScope_, defaultTargetScope, onScopeMenuClick: onScopeMenuClick_, onScopeSelect: onScopeSelect_, // Icon alignment iconAlign = 'right', onSelect: onSelect_, onSearchTextChange: onSearchTextChange_, onLookupRequest: onLookupRequest_, onBlur: onBlur_, onFocus: onFocus_, onValueChange, onComplete, elementRef: elementRef_, inputRef: inputRef_, dropdownRef: dropdownRef_, ...rprops } = props; useInitComponentStyle(); const fallbackId = useId(); const comboboxId = id_ ?? `${fallbackId}-combobox`; const listboxId = `${fallbackId}-listbox`; const optionIdPrefix = `${comboboxId}-option`; const getOptionId = (value: string) => `${optionIdPrefix}-${value}`; const scopeListboxId = `${comboboxId}-scope-listbox`; const getScopeOptionId = (index: number) => `${scopeListboxId}-option-${index}`; const [value, setValue] = useControlledValue( value_, defaultValue ?? null ); const [selected, setSelected] = useControlledValue( selected_, defaultSelected ?? data?.find((entry) => entry.value === value) ?? null ); const [opened, setOpened] = useControlledValue( opened_, defaultOpened ?? false ); const [searchText, setSearchText] = useControlledValue( searchText_, defaultSearchText ?? '' ); const [targetScope, setTargetScope] = useControlledValue( targetScope_, defaultTargetScope ?? (scopes && scopes.length > 0 ? scopes[0].value : undefined) ); const [focusedValue, setFocusedValue] = useState(); const listHeaderIdSeed = useMemo( () => [...data.map((entry) => entry.value), 'header'].join('-'), [data] ); const listFooterIdSeed = useMemo( () => [...data.map((entry) => entry.value), 'footer'].join('-'), [data] ); // Memoized option values - filtered data with optional header and footer const optionValues = useMemo(() => { const filteredData = lookupFilter ? data.filter((entry) => lookupFilter(entry, searchText, targetScope)) : data; return [ listHeaderRenderer ? listHeaderIdSeed : undefined, ...filteredData.map((entry) => entry.value), listFooterRenderer ? listFooterIdSeed : undefined, ].filter((value) => value !== undefined); }, [ data, lookupFilter, searchText, targetScope, listHeaderRenderer, listHeaderIdSeed, listFooterRenderer, listFooterIdSeed, ]); // Get next option value for keyboard navigation const getNextValue = useCallback( (currentValue?: string) => { if (optionValues.length === 0) return undefined; if (!currentValue) return optionValues[0]; const currentIndex = optionValues.indexOf(currentValue); return optionValues[ Math.min(currentIndex + 1, optionValues.length - 1) ]; // not wrap around }, [optionValues] ); // Get previous option value for keyboard navigation const getPrevValue = useCallback( (currentValue?: string) => { if (optionValues.length === 0) return undefined; if (!currentValue) return optionValues[optionValues.length - 1]; const currentIndex = optionValues.indexOf(currentValue); return optionValues[Math.max(currentIndex - 1, 0)]; // not wrap around }, [optionValues] ); // Scroll focused element into view const scrollFocusedElementIntoView = useEventCallback( (nextFocusedValue: string | undefined) => { if (!nextFocusedValue || !dropdownElRef.current) { return; } const targetElement = document.getElementById( getOptionId(nextFocusedValue) ); const dropdownContainer = dropdownElRef.current; if ( !(targetElement instanceof HTMLElement) || !dropdownContainer.contains(targetElement) ) { return; } targetElement.focus(); } ); // Set initial focus when dropdown opens useEffect(() => { if (!opened) { setFocusedValue(undefined); } }, [opened, optionValues, focusedValue, scrollFocusedElementIntoView]); const elRef = useRef(null); const elementRef = useMergeRefs([elRef, elementRef_]); const inputElRef = useRef(null); const inputRef = useMergeRefs([inputElRef, inputRef_]); const dropdownElRef = useRef(null); const dropdownRef = useMergeRefs([dropdownElRef, dropdownRef_]); const onScopeMenuClick = useEventCallback(() => { setOpened(false); onScopeMenuClick_?.(); }); const onSelect = useEventCallback((selectedEntry: LookupEntry | null) => { const currValue = selectedEntry?.value ?? null; setValue(currValue); setSelected(selectedEntry); onValueChange?.(currValue, value); onSelect_?.(selectedEntry); }); const onSearchTextChange = useEventCallback((newSearchText: string) => { setSearchText(newSearchText); onSearchTextChange_?.(newSearchText); if (newSearchText && !opened) { setOpened(true); onLookupRequest_?.(newSearchText); } }); const onInputClick = useEventCallback(() => { onFocus_?.(); }); const onInputChange = useEventCallback( (e: ChangeEvent) => { const newSearchText = e.target.value; onSearchTextChange(newSearchText); setOpened(true); onLookupRequest_?.(newSearchText); } ); const onInputFocus = useEventCallback(() => { onFocus_?.(); }); const onInputBlur = useEventCallback((e: FocusEvent) => { if (e.relatedTarget !== null) { const prevValue = getPrevValue(focusedValue); const nextValue = getNextValue(focusedValue); if ( (prevValue && e.relatedTarget.id === getOptionId(prevValue)) || (nextValue && e.relatedTarget.id === getOptionId(nextValue)) ) { // catch keyborad event return; } } setTimeout(() => { if (!isFocusedInComponent()) { setOpened(false); onBlur_?.(); onComplete?.(true); } }, 10); }); const { getActiveElement } = useContext(ComponentSettingsContext); const isFocusedInComponent = useEventCallback(() => { const targetEl = getActiveElement(); return ( isElInChildren(containerRef.current, targetEl) || isElInChildren(dropdownElRef.current, targetEl) ); }); const onInputKeyDown = useKeyHandler({ type: 'search', opened, onOpen: () => { setOpened(true); onLookupRequest_?.(searchText); }, onClose: () => { setOpened(false); onComplete?.(true); }, onNavigateDown: () => { const nextValue = getNextValue(focusedValue); setFocusedValue(nextValue); scrollFocusedElementIntoView(nextValue); }, onNavigateUp: () => { const prevValue = getPrevValue(focusedValue); setFocusedValue(prevValue); scrollFocusedElementIntoView(prevValue); }, onSelect: () => { if (opened && focusedValue) { const selectedEntry = data.find( (entry) => entry.value === focusedValue ); if (selectedEntry) { onSelect(selectedEntry); setOpened(false); onComplete?.(); } } else if (searchText) { setOpened(true); onLookupRequest_?.(searchText); } }, isTabNavigationIgnored: (direction) => { const currentIndex = focusedValue ? optionValues.indexOf(focusedValue) : -1; return ( currentIndex === -1 || (direction === 'backward' && currentIndex <= 0) || (direction === 'forward' && currentIndex >= optionValues.length - 1) ); }, onTabNavigation: (direction) => { const currentIndex = focusedValue ? optionValues.indexOf(focusedValue) : -1; if (direction === 'backward') { if (currentIndex <= 0) { setOpened(false); onComplete?.(); } else { const prevValue = getPrevValue(focusedValue); setFocusedValue(prevValue); scrollFocusedElementIntoView(prevValue); } } else { if (currentIndex >= optionValues.length - 1) { setOpened(false); onComplete?.(); } else { const nextValue = getNextValue(focusedValue); setFocusedValue(nextValue); scrollFocusedElementIntoView(nextValue); } } }, }); const onOptionClick = useEventCallback((entry: LookupEntry) => { onSelect(entry); setOpened(false); setTimeout(() => { inputElRef.current?.focus(); onComplete?.(); }, 10); }); const onOptionFocus = useEventCallback((value: string) => { setFocusedValue(value); }); const onSearchIconClick = useEventCallback(() => { inputElRef.current?.focus(); setOpened(true); onLookupRequest_?.(searchText); }); const onRemoveSelection = useEventCallback(() => { onSelect(null); setSearchText(''); setOpened(true); onLookupRequest_?.(''); setTimeout(() => { inputElRef.current?.focus(); }, 10); }); const hasSelection = selected != null; const containerRef = useRef(null); const containerClassNames = classnames( 'react-slds-lookup', `react-slds-lookup-scope-${scopes ? 'multi' : 'single'}`, 'slds-combobox_container', { 'slds-has-selection': hasSelection, }, className ); const comboboxClassNames = classnames( 'slds-combobox', 'slds-dropdown-trigger', 'slds-dropdown-trigger_click', { 'slds-is-open': opened, } ); const formElemProps = { controlId: comboboxId, label, required, error, cols, tooltip, tooltipIcon, elementRef, }; const filteredData = lookupFilter ? data.filter((entry) => lookupFilter(entry, searchText, targetScope)) : data; // Render selected state if (hasSelection && selected) { return ( ); } // Render search state with optional scope selector if (scopes && scopes.length > 0) { // Multi Entity Lookup with scope selector return ( { setTargetScope(scope); onScopeSelect_?.(scope); }} /> ); } // Render simple search state (no scopes) return ( ); }, { isFormElement: true } );