diff --git a/packages/pluggableWidgets/combobox-web/CHANGELOG.md b/packages/pluggableWidgets/combobox-web/CHANGELOG.md index 2d993ae99d..487537f839 100644 --- a/packages/pluggableWidgets/combobox-web/CHANGELOG.md +++ b/packages/pluggableWidgets/combobox-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added a new "On change filter input" event that triggers when users type in the combobox filter field, passing the current filter text as an action variable to enable custom nanoflows/microflows for dynamic filtering scenarios. + ## [2.4.3] - 2025-07-22 ### Fixed diff --git a/packages/pluggableWidgets/combobox-web/package.json b/packages/pluggableWidgets/combobox-web/package.json index f0e229cd9d..557ad8927e 100644 --- a/packages/pluggableWidgets/combobox-web/package.json +++ b/packages/pluggableWidgets/combobox-web/package.json @@ -20,7 +20,7 @@ }, "packagePath": "com.mendix.widget.web", "marketplace": { - "minimumMXVersion": "10.7.0", + "minimumMXVersion": "10.22.0", "appNumber": 219304, "appName": "Combo box", "reactReady": true diff --git a/packages/pluggableWidgets/combobox-web/src/Combobox.xml b/packages/pluggableWidgets/combobox-web/src/Combobox.xml index 5f87880ab5..a67eda1303 100644 --- a/packages/pluggableWidgets/combobox-web/src/Combobox.xml +++ b/packages/pluggableWidgets/combobox-web/src/Combobox.xml @@ -329,6 +329,14 @@ On leave action + + + On change filter input + + + + + diff --git a/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx b/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx index 7e586ba40d..a818484653 100644 --- a/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx +++ b/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx @@ -16,7 +16,11 @@ export function MultiSelection({ a11yConfig, menuFooterContent, ariaRequired, - ...options + labelId, + inputId, + ariaLabel, + readOnlyStyle, + noOptionsText }: SelectionBaseProps): ReactElement { const { isOpen, @@ -33,11 +37,11 @@ export function MultiSelection({ items, setSelectedItems, toggleSelectedItem - } = useDownshiftMultiSelectProps(selector, options, a11yConfig.a11yStatusMessage); + } = useDownshiftMultiSelectProps(selector, { inputId, labelId }, a11yConfig.a11yStatusMessage); const inputRef = useRef(null); const isSelectedItemsBoxStyle = selector.selectedItemsStyle === "boxes"; const isOptionsSelected = selector.isOptionsSelected(); - const inputLabel = getInputLabel(options.inputId); + const inputLabel = getInputLabel(inputId); const hasLabel = useMemo(() => Boolean(inputLabel), [inputLabel]); const inputProps = getInputProps({ ...getDropdownProps( @@ -65,7 +69,7 @@ export function MultiSelection({ disabled: selector.readOnly, readOnly: selector.options.filterType === "none", "aria-required": ariaRequired.value, - "aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined + "aria-label": !hasLabel && ariaLabel ? ariaLabel : undefined }); const memoizedselectedCaptions = useMemo( @@ -91,7 +95,7 @@ export function MultiSelection({ { if (isOptionsSelected === "all") { @@ -181,7 +185,7 @@ export function MultiSelection({ ) : undefined } menuFooterContent={menuFooterContent} - inputId={options.inputId} + inputId={inputId} selector={selector} isOpen={isOpen} highlightedIndex={highlightedIndex} @@ -189,7 +193,7 @@ export function MultiSelection({ getItemProps={getItemProps} getMenuProps={getMenuProps} selectedItems={selectedItems} - noOptionsText={options.noOptionsText} + noOptionsText={noOptionsText} onOptionClick={() => { inputRef.current?.focus(); }} diff --git a/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx b/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx index eccabce5b2..a8435a7af6 100644 --- a/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx +++ b/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx @@ -27,7 +27,7 @@ export function SingleSelection({ reset, isOpen, highlightedIndex - } = useDownshiftSingleSelectProps(selector, options, a11yConfig.a11yStatusMessage); + } = useDownshiftSingleSelectProps(selector, { ...options }, a11yConfig.a11yStatusMessage); const inputRef = useRef(null); const lazyLoading = selector.lazyLoading ?? false; const { onScroll } = useLazyLoading({ diff --git a/packages/pluggableWidgets/combobox-web/src/helpers/types.ts b/packages/pluggableWidgets/combobox-web/src/helpers/types.ts index edf5013642..a0cadbce34 100644 --- a/packages/pluggableWidgets/combobox-web/src/helpers/types.ts +++ b/packages/pluggableWidgets/combobox-web/src/helpers/types.ts @@ -79,6 +79,7 @@ interface SelectorBase { onEnterEvent?: () => void; onLeaveEvent?: () => void; + onFilterInputChange?: (filterValue: string) => void; } export interface SingleSelector extends SelectorBase<"single", string> {} @@ -101,6 +102,7 @@ export interface SelectionBaseProps { tabIndex: number; ariaRequired: DynamicValue; ariaLabel?: string; + onFilterInputChange?: (filterValue: string) => void; a11yConfig: { ariaLabels: { clearSelection: string; diff --git a/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts b/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts index fbef975c0c..a7078240cd 100644 --- a/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts +++ b/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts @@ -148,8 +148,17 @@ function useComboboxProps( selectedItem: null, inputId: options?.inputId, labelId: options?.labelId, - onInputValueChange({ inputValue }) { + onInputValueChange({ inputValue, type }) { selector.options.setSearchTerm(inputValue!); + + if ( + selector.onFilterInputChange && + type?.includes("input_change") && + inputValue && + inputValue.trim()?.length > 0 + ) { + selector.onFilterInputChange(inputValue); + } }, getA11yStatusMessage(options) { let message = diff --git a/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts b/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts index f65a2d9cbc..543d365061 100644 --- a/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts +++ b/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts @@ -29,8 +29,18 @@ export function useDownshiftSingleSelectProps( onSelectedItemChange({ selectedItem }: UseComboboxStateChange) { selector.setValue(selectedItem ?? null); }, - onInputValueChange({ inputValue }) { + onInputValueChange({ inputValue, type }) { selector.options.setSearchTerm(inputValue!); + + if ( + selector.onFilterInputChange && + type && + (type === "__input_change__" || type?.includes("input_change")) && + inputValue && + inputValue.trim().length > 0 + ) { + selector.onFilterInputChange(inputValue!); + } }, getA11yStatusMessage(options) { const selectedItem = selector.caption.get(selector.currentId); diff --git a/packages/pluggableWidgets/combobox-web/src/hooks/useGetSelector.ts b/packages/pluggableWidgets/combobox-web/src/hooks/useGetSelector.ts index a0ac85d1a9..1118ced41b 100644 --- a/packages/pluggableWidgets/combobox-web/src/hooks/useGetSelector.ts +++ b/packages/pluggableWidgets/combobox-web/src/hooks/useGetSelector.ts @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useRef, useState, useCallback } from "react"; import { ComboboxContainerProps } from "../../typings/ComboboxProps"; import { getSelector } from "../helpers/getSelector"; import { Selector } from "../helpers/types"; @@ -6,10 +6,45 @@ import { Selector } from "../helpers/types"; export function useGetSelector(props: ComboboxContainerProps): Selector { const selectorRef = useRef(undefined); const [, setInput] = useState({}); + const debounceTimeoutRef = useRef(); + const lastExecutedValueRef = useRef(""); + + const onFilterInputChange = useCallback( + (filterValue: string) => { + if (!props.onChangeFilterInputEvent) { + return; + } + + if (lastExecutedValueRef.current === filterValue) { + return; + } + + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + debounceTimeoutRef.current = setTimeout(() => { + lastExecutedValueRef.current = filterValue; + try { + if (props.onChangeFilterInputEvent?.canExecute && !props.onChangeFilterInputEvent?.isExecuting) { + props.onChangeFilterInputEvent.execute({ + filterInput: filterValue + }); + } + } catch (error) { + console.error("Error executing onChangeFilterInputEvent:", error); + } + }, 300); + }, + [props.onChangeFilterInputEvent] + ); + if (!selectorRef.current) { selectorRef.current = getSelector(props); selectorRef.current.options.onAfterSearchTermChange(() => setInput({})); } selectorRef.current.updateProps(props); + selectorRef.current.onFilterInputChange = onFilterInputChange; + return selectorRef.current; } diff --git a/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts b/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts index d23d3a96ac..80ba167b99 100644 --- a/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts +++ b/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts @@ -4,7 +4,7 @@ * @author Mendix Widgets Framework Team */ import { ComponentType, ReactNode } from "react"; -import { ActionValue, DynamicValue, EditableValue, ListValue, ListAttributeValue, ListExpressionValue, ListWidgetValue, ReferenceValue, ReferenceSetValue, SelectionSingleValue, SelectionMultiValue } from "mendix"; +import { ActionValue, DynamicValue, EditableValue, ListValue, Option, ListAttributeValue, ListExpressionValue, ListWidgetValue, ReferenceValue, ReferenceSetValue, SelectionSingleValue, SelectionMultiValue } from "mendix"; import { Big } from "big.js"; export type SourceEnum = "context" | "database" | "static"; @@ -89,6 +89,7 @@ export interface ComboboxContainerProps { onChangeEvent?: ActionValue; onEnterEvent?: ActionValue; onLeaveEvent?: ActionValue; + onChangeFilterInputEvent?: ActionValue<{ filterInput: Option }>; ariaRequired: DynamicValue; ariaLabel?: DynamicValue; clearButtonAriaLabel?: DynamicValue; @@ -145,6 +146,7 @@ export interface ComboboxPreviewProps { onChangeDatabaseEvent: {} | null; onEnterEvent: {} | null; onLeaveEvent: {} | null; + onChangeFilterInputEvent: {} | null; ariaRequired: string; ariaLabel: string; clearButtonAriaLabel: string;