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..4d19b55e67 100644 --- a/packages/pluggableWidgets/combobox-web/package.json +++ b/packages/pluggableWidgets/combobox-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/combobox-web", "widgetName": "Combobox", - "version": "2.4.3", + "version": "2.5.0", "description": "Configurable Combo box widget with suggestions and autocomplete.", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", @@ -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.editorConfig.ts b/packages/pluggableWidgets/combobox-web/src/Combobox.editorConfig.ts index d232b04d11..86108e9a14 100644 --- a/packages/pluggableWidgets/combobox-web/src/Combobox.editorConfig.ts +++ b/packages/pluggableWidgets/combobox-web/src/Combobox.editorConfig.ts @@ -178,6 +178,10 @@ export function getProperties( hidePropertiesIn(defaultProperties, values, ["loadingType"]); } + if (values.onChangeFilterInputEvent === null) { + hidePropertiesIn(defaultProperties, values, ["filterInputDebounceInterval"]); + } + return defaultProperties; } diff --git a/packages/pluggableWidgets/combobox-web/src/Combobox.xml b/packages/pluggableWidgets/combobox-web/src/Combobox.xml index 5f87880ab5..c1994358df 100644 --- a/packages/pluggableWidgets/combobox-web/src/Combobox.xml +++ b/packages/pluggableWidgets/combobox-web/src/Combobox.xml @@ -314,21 +314,33 @@ - On change action + On change - On change action + On change - On enter action + On enter - On leave action + On leave + + + On filter input change + + + + + + + Debounce interval + The debounce interval for each filter input change event triggered in milliseconds. + diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/MultiSelection.spec.tsx b/packages/pluggableWidgets/combobox-web/src/__tests__/MultiSelection.spec.tsx index be4ee06860..694265e13e 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/MultiSelection.spec.tsx +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/MultiSelection.spec.tsx @@ -76,7 +76,8 @@ describe("Combo box (Association)", () => { ], selectedItemsSorting: "none", customEditability: "default", - customEditabilityExpression: dynamic(false) + customEditabilityExpression: dynamic(false), + filterInputDebounceInterval: 200 }; if (defaultProps.optionsSourceAssociationCaptionType === "expression") { defaultProps.optionsSourceAssociationCaptionExpression!.get = i => dynamic(`${i.id}`); diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx b/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx index 9bc51c9330..a61740d899 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx @@ -79,7 +79,8 @@ describe("Combo box (Association)", () => { ], selectedItemsSorting: "none", customEditability: "default", - customEditabilityExpression: dynamic(false) + customEditabilityExpression: dynamic(false), + filterInputDebounceInterval: 200 }; if (defaultProps.optionsSourceAssociationCaptionType === "expression") { defaultProps.optionsSourceAssociationCaptionExpression!.get = i => dynamic(`${i.id}`); diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx b/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx index 8d71b176bb..698dcafe57 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx @@ -77,7 +77,8 @@ describe("Combo box (Static values)", () => { ], selectedItemsSorting: "none", customEditability: "default", - customEditabilityExpression: dynamic(false) + customEditabilityExpression: dynamic(false), + filterInputDebounceInterval: 200 }; if (defaultProps.optionsSourceAssociationCaptionType === "expression") { defaultProps.optionsSourceAssociationCaptionExpression!.get = i => dynamic(`${i.id}`); diff --git a/packages/pluggableWidgets/combobox-web/src/helpers/types.ts b/packages/pluggableWidgets/combobox-web/src/helpers/types.ts index edf5013642..1fb5795f7c 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..a1ec0b1870 100644 --- a/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts +++ b/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts @@ -2,12 +2,13 @@ import { UseComboboxProps, UseComboboxReturnValue, UseComboboxState, + UseComboboxStateChange, UseComboboxStateChangeOptions, UseMultipleSelectionReturnValue, useCombobox, useMultipleSelection } from "downshift"; -import { useMemo, useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { A11yStatusMessage, MultiSelector } from "../helpers/types"; export type UseDownshiftMultiSelectPropsReturnValue = UseMultipleSelectionReturnValue & @@ -148,8 +149,11 @@ function useComboboxProps( selectedItem: null, inputId: options?.inputId, labelId: options?.labelId, - onInputValueChange({ inputValue }) { + onInputValueChange({ inputValue, type }: UseComboboxStateChange) { selector.options.setSearchTerm(inputValue!); + if (selector.onFilterInputChange && type === useCombobox.stateChangeTypes.InputChange) { + 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..707e343d03 100644 --- a/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts +++ b/packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts @@ -2,12 +2,12 @@ import { UseComboboxProps, UseComboboxReturnValue, UseComboboxState, - UseComboboxStateChange, UseComboboxStateChangeOptions, + UseComboboxStateChange, useCombobox } from "downshift"; -import { useMemo, useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { A11yStatusMessage, SingleSelector } from "../helpers/types"; interface Options { @@ -29,8 +29,11 @@ export function useDownshiftSingleSelectProps( onSelectedItemChange({ selectedItem }: UseComboboxStateChange) { selector.setValue(selectedItem ?? null); }, - onInputValueChange({ inputValue }) { + onInputValueChange({ inputValue, type }: UseComboboxStateChange) { selector.options.setSearchTerm(inputValue!); + if (selector.onFilterInputChange && type === useCombobox.stateChangeTypes.InputChange) { + 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..95713bd07b 100644 --- a/packages/pluggableWidgets/combobox-web/src/hooks/useGetSelector.ts +++ b/packages/pluggableWidgets/combobox-web/src/hooks/useGetSelector.ts @@ -1,15 +1,43 @@ -import { useRef, useState } from "react"; +import { debounce } from "@mendix/widget-plugin-platform/utils/debounce"; +import { useMemo, useRef, useState } from "react"; import { ComboboxContainerProps } from "../../typings/ComboboxProps"; import { getSelector } from "../helpers/getSelector"; import { Selector } from "../helpers/types"; +function onInputValueChange( + onChangeFilterInputEvent: ComboboxContainerProps["onChangeFilterInputEvent"], + filterValue?: string +): void { + if (!onChangeFilterInputEvent) { + return; + } + if (onChangeFilterInputEvent.canExecute && !onChangeFilterInputEvent.isExecuting) { + onChangeFilterInputEvent.execute({ + filterInput: filterValue + }); + } +} + export function useGetSelector(props: ComboboxContainerProps): Selector { const selectorRef = useRef(undefined); const [, setInput] = useState({}); + const [onFilterChangeDebounce] = useMemo( + () => + debounce((filterValue?: string) => { + onInputValueChange(props.onChangeFilterInputEvent, filterValue); + }, props.filterInputDebounceInterval ?? 200), + [props.onChangeFilterInputEvent, props.filterInputDebounceInterval] + ); + if (!selectorRef.current) { selectorRef.current = getSelector(props); selectorRef.current.options.onAfterSearchTermChange(() => setInput({})); + } else { + if (!selectorRef.current.onFilterInputChange) { + selectorRef.current.onFilterInputChange = onFilterChangeDebounce; + } } selectorRef.current.updateProps(props); + return selectorRef.current; } diff --git a/packages/pluggableWidgets/combobox-web/src/package.xml b/packages/pluggableWidgets/combobox-web/src/package.xml index 5fcc7377bf..7607566d28 100644 --- a/packages/pluggableWidgets/combobox-web/src/package.xml +++ b/packages/pluggableWidgets/combobox-web/src/package.xml @@ -1,6 +1,6 @@ - + diff --git a/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts b/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts index d23d3a96ac..aec3e503fb 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,8 @@ export interface ComboboxContainerProps { onChangeEvent?: ActionValue; onEnterEvent?: ActionValue; onLeaveEvent?: ActionValue; + onChangeFilterInputEvent?: ActionValue<{ filterInput: Option }>; + filterInputDebounceInterval: number; ariaRequired: DynamicValue; ariaLabel?: DynamicValue; clearButtonAriaLabel?: DynamicValue; @@ -145,6 +147,8 @@ export interface ComboboxPreviewProps { onChangeDatabaseEvent: {} | null; onEnterEvent: {} | null; onLeaveEvent: {} | null; + onChangeFilterInputEvent: {} | null; + filterInputDebounceInterval: number | null; ariaRequired: string; ariaLabel: string; clearButtonAriaLabel: string;