Skip to content

[WC-3026] Combobox: Enable onFilterInputChange action with ActionVariables #1782

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/combobox-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/combobox-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/combobox-web/src/Combobox.xml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@
<caption>On leave action</caption>
<description />
</property>

<property key="onChangeFilterInputEvent" type="action" required="false">
<caption>On change filter input</caption>
<description />
<actionVariables>
<actionVariable key="filterInput" caption="Filter Input" type="String" />
</actionVariables>
</property>
</propertyGroup>
<propertyGroup caption="Accessibility">
<propertyGroup caption="Accessibility">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export function MultiSelection({
a11yConfig,
menuFooterContent,
ariaRequired,
...options
labelId,
inputId,
ariaLabel,
readOnlyStyle,
noOptionsText
}: SelectionBaseProps<MultiSelector>): ReactElement {
const {
isOpen,
Expand All @@ -33,11 +37,11 @@ export function MultiSelection({
items,
setSelectedItems,
toggleSelectedItem
} = useDownshiftMultiSelectProps(selector, options, a11yConfig.a11yStatusMessage);
} = useDownshiftMultiSelectProps(selector, { inputId, labelId }, a11yConfig.a11yStatusMessage);
const inputRef = useRef<HTMLInputElement>(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(
Expand Down Expand Up @@ -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(
Expand All @@ -91,7 +95,7 @@ export function MultiSelection({
<ComboboxWrapper
isOpen={isOpen}
readOnly={selector.readOnly}
readOnlyStyle={options.readOnlyStyle}
readOnlyStyle={readOnlyStyle}
getToggleButtonProps={getToggleButtonProps}
validation={selector.validation}
isLoading={lazyLoading && selector.options.isLoading}
Expand Down Expand Up @@ -168,7 +172,7 @@ export function MultiSelection({
<SelectAllButton
disabled={items.length === 0}
value={isOptionsSelected}
id={`${options.inputId}-select-all-button`}
id={`${inputId}-select-all-button`}
ariaLabel={a11yConfig.ariaLabels.selectAll}
onChange={() => {
if (isOptionsSelected === "all") {
Expand All @@ -181,15 +185,15 @@ export function MultiSelection({
) : undefined
}
menuFooterContent={menuFooterContent}
inputId={options.inputId}
inputId={inputId}
selector={selector}
isOpen={isOpen}
highlightedIndex={highlightedIndex}
selectableItems={items}
getItemProps={getItemProps}
getMenuProps={getMenuProps}
selectedItems={selectedItems}
noOptionsText={options.noOptionsText}
noOptionsText={noOptionsText}
onOptionClick={() => {
inputRef.current?.focus();
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function SingleSelection({
reset,
isOpen,
highlightedIndex
} = useDownshiftSingleSelectProps(selector, options, a11yConfig.a11yStatusMessage);
} = useDownshiftSingleSelectProps(selector, { ...options }, a11yConfig.a11yStatusMessage);
const inputRef = useRef<HTMLInputElement>(null);
const lazyLoading = selector.lazyLoading ?? false;
const { onScroll } = useLazyLoading({
Expand Down
2 changes: 2 additions & 0 deletions packages/pluggableWidgets/combobox-web/src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@

onEnterEvent?: () => void;
onLeaveEvent?: () => void;
onFilterInputChange?: (filterValue: string) => void;
}

export interface SingleSelector extends SelectorBase<"single", string> {}

Check warning on line 85 in packages/pluggableWidgets/combobox-web/src/helpers/types.ts

View workflow job for this annotation

GitHub Actions / Run code quality check

An interface declaring no members is equivalent to its supertype

Check warning on line 85 in packages/pluggableWidgets/combobox-web/src/helpers/types.ts

View workflow job for this annotation

GitHub Actions / Run code quality check

An interface declaring no members is equivalent to its supertype
export interface MultiSelector extends SelectorBase<"multi", string[]> {
selectedItemsStyle: SelectedItemsStyleEnum;
selectionMethod: SelectionMethodEnum;
Expand All @@ -101,6 +102,7 @@
tabIndex: number;
ariaRequired: DynamicValue<boolean>;
ariaLabel?: string;
onFilterInputChange?: (filterValue: string) => void;
a11yConfig: {
ariaLabels: {
clearSelection: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,18 @@ export function useDownshiftSingleSelectProps(
onSelectedItemChange({ selectedItem }: UseComboboxStateChange<string>) {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
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";

export function useGetSelector(props: ComboboxContainerProps): Selector {
const selectorRef = useRef<Selector | undefined>(undefined);
const [, setInput] = useState({});
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
const lastExecutedValueRef = useRef<string>("");

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -89,6 +89,7 @@ export interface ComboboxContainerProps {
onChangeEvent?: ActionValue;
onEnterEvent?: ActionValue;
onLeaveEvent?: ActionValue;
onChangeFilterInputEvent?: ActionValue<{ filterInput: Option<string> }>;
ariaRequired: DynamicValue<boolean>;
ariaLabel?: DynamicValue<string>;
clearButtonAriaLabel?: DynamicValue<string>;
Expand Down Expand Up @@ -145,6 +146,7 @@ export interface ComboboxPreviewProps {
onChangeDatabaseEvent: {} | null;
onEnterEvent: {} | null;
onLeaveEvent: {} | null;
onChangeFilterInputEvent: {} | null;
ariaRequired: string;
ariaLabel: string;
clearButtonAriaLabel: string;
Expand Down
Loading