Skip to content

Commit 40d725e

Browse files
authored
[WC-3255] Fix filter in single selected combobox (#2036)
2 parents 00208d2 + 50cb279 commit 40d725e

File tree

8 files changed

+115
-22
lines changed

8 files changed

+115
-22
lines changed

packages/pluggableWidgets/combobox-web/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- We fixed an issue with filter input in single selection mode being disabled in some cases.
12+
13+
### Changed
14+
15+
- We made it possibly to clear the selection in single selection mode with Backspace key.
16+
- We improved keyboard navigation for multi selection with
17+
918
## [2.7.0] - 2026-01-14
1019

1120
### Changed

packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect } from "@playwright/test";
1+
import { expect, test } from "@playwright/test";
22

33
test.afterEach("Cleanup session", async ({ page }) => {
44
// Because the test isolation that will open a new session for every test executed, and that exceeds Mendix's license limit of 5 sessions, so we need to force logout after each test.
@@ -10,14 +10,15 @@ test.describe("combobox-web", () => {
1010
await page.goto("/p/combobox");
1111
await page.waitForLoadState("networkidle");
1212
await page.click(".mx-name-actionButton1");
13+
await page.waitForLoadState("networkidle");
1314
});
1415

1516
test.describe("data source types", () => {
1617
test("renders combobox using association", async ({ page }) => {
1718
const comboBox = page.locator(".mx-name-comboBox1");
1819
await expect(comboBox).toBeVisible({ timeout: 10000 });
1920
await expect(comboBox).toHaveScreenshot(`comboBoxAssociation.png`);
20-
await page.click(".mx-name-comboBox1");
21+
await comboBox.click();
2122
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
2223
`comboBoxAssociationOpen.png`
2324
);
@@ -28,7 +29,7 @@ test.describe("combobox-web", () => {
2829
const comboBox = page.locator(".mx-name-comboBox4");
2930
await expect(comboBox).toBeVisible({ timeout: 10000 });
3031
await expect(comboBox).toHaveScreenshot(`comboBoxAssociationRowClick.png`);
31-
await page.click(".mx-name-comboBox4");
32+
await comboBox.click();
3233
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
3334
`comboBoxAssociationRowClickOpen.png`
3435
);
@@ -38,7 +39,7 @@ test.describe("combobox-web", () => {
3839
const comboBox = page.locator(".mx-name-comboBox2");
3940
await expect(comboBox).toBeVisible({ timeout: 10000 });
4041
await expect(comboBox).toHaveScreenshot(`comboBoxEnum.png`);
41-
await page.click(".mx-name-comboBox2");
42+
await comboBox.click();
4243
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
4344
`comboBoxEnumOpen.png`
4445
);
@@ -48,7 +49,7 @@ test.describe("combobox-web", () => {
4849
await page.click(".mx-name-tabPage2");
4950
const comboBox = page.locator(".mx-name-comboBox5");
5051
await expect(comboBox).toBeVisible({ timeout: 10000 });
51-
await page.click(".mx-name-comboBox5");
52+
await comboBox.click();
5253
await expect(page.locator(".mx-name-comboBox5 .widget-combobox-menu").first()).toHaveScreenshot(
5354
`comboBoxEnumFooter.png`
5455
);
@@ -58,15 +59,15 @@ test.describe("combobox-web", () => {
5859
await page.click(".mx-name-tabPage2");
5960
const comboBox = page.locator(".mx-name-comboBox6");
6061
await expect(comboBox).toBeVisible({ timeout: 10000 });
61-
await page.click(".mx-name-comboBox6");
62+
await comboBox.click();
6263
await expect(comboBox).toHaveScreenshot(`comboBoxReadOnly.png`);
6364
});
6465

6566
test("renders combobox using boolean", async ({ page }) => {
6667
const comboBox = page.locator(".mx-name-comboBox3");
6768
await expect(comboBox).toBeVisible({ timeout: 10000 });
6869
await expect(comboBox).toHaveScreenshot(`comboBoxBoolean.png`);
69-
await page.click(".mx-name-comboBox3");
70+
await comboBox.click();
7071
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
7172
`comboBoxBooleanOpen.png`
7273
);
@@ -77,7 +78,7 @@ test.describe("combobox-web", () => {
7778
const comboBox = page.locator(".mx-name-comboBox7");
7879
await expect(comboBox).toBeVisible({ timeout: 10000 });
7980
await expect(comboBox).toHaveScreenshot(`comboBoxStatic.png`);
80-
await page.click(".mx-name-comboBox7");
81+
await comboBox.click();
8182
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
8283
`comboBoxStaticOpen.png`
8384
);
@@ -88,7 +89,7 @@ test.describe("combobox-web", () => {
8889
const comboBox = page.locator(".mx-name-comboBox8");
8990
await expect(comboBox).toBeVisible({ timeout: 10000 });
9091
await expect(comboBox).toHaveScreenshot(`comboBoxDatabase.png`);
91-
await page.click(".mx-name-comboBox8");
92+
await comboBox.click();
9293
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
9394
`comboBoxDatabaseOpen.png`
9495
);
@@ -97,8 +98,8 @@ test.describe("combobox-web", () => {
9798
test("renders a filter result", async ({ page }) => {
9899
const comboBox = page.locator(".mx-name-comboBox2");
99100
await expect(comboBox).toBeVisible({ timeout: 10000 });
100-
await page.click(".mx-name-comboBox2");
101-
await page.locator(".mx-name-comboBox2 .widget-combobox-input").fill("A");
101+
await comboBox.click();
102+
await getFilterInput(comboBox).fill("A");
102103
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
103104
`comboBoxFiltering.png`
104105
);
@@ -108,7 +109,7 @@ test.describe("combobox-web", () => {
108109
await page.click(".mx-name-tabPage2");
109110
const comboBox = page.locator(".mx-name-comboBox4");
110111
await expect(comboBox).toBeVisible({ timeout: 10000 });
111-
await page.locator(".mx-name-comboBox4 .widget-combobox-icon-container").first().click();
112+
await comboBox.locator(".widget-combobox-icon-container").first().click();
112113
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
113114
`comboBoxRemoveSelection.png`
114115
);
@@ -118,11 +119,77 @@ test.describe("combobox-web", () => {
118119
await page.click(".mx-name-tabPage2");
119120
const comboBox = page.locator(".mx-name-comboBox4");
120121
await expect(comboBox).toBeVisible({ timeout: 10000 });
121-
await page.locator(".mx-name-comboBox4 .widget-combobox-clear-button").nth(3).click();
122+
await comboBox.locator(".widget-combobox-clear-button").nth(3).click();
122123
await expect(page.locator(".modal-body .mx-name-layoutGrid1").first()).toHaveScreenshot(
123124
`comboBoxRemoveAllSelection.png`
124125
);
125126
});
126127
});
127128
});
129+
130+
test.describe("searching and selecting", () => {
131+
test("clears with backspace", async ({ page }) => {
132+
const comboBox = page.locator(".mx-name-comboBox2");
133+
await expect(comboBox).toBeVisible({ timeout: 10000 });
134+
135+
// check nothing is selected
136+
await expect(getSelectedText(comboBox)).toContainClass("widget-combobox-placeholder-empty");
137+
138+
// open the dropdown
139+
await page.click(".mx-name-comboBox2");
140+
141+
// select europe
142+
await getOptionItem(comboBox, "Europe").click({ delay: 10 });
143+
await expect(getSelectedText(comboBox)).toContainText("Europe");
144+
145+
// check input stays focused
146+
await expect(getFilterInput(comboBox)).toBeFocused();
147+
148+
// press Backspace to clear
149+
await page.keyboard.press("Backspace");
150+
151+
// check if cleared
152+
await expect(getSelectedText(comboBox)).toContainClass("widget-combobox-placeholder-empty");
153+
});
154+
155+
test("types filter when selected", async ({ page }) => {
156+
const comboBox = page.locator(".mx-name-comboBox2");
157+
await expect(comboBox).toBeVisible({ timeout: 10000 });
158+
159+
// check nothing is selected
160+
await expect(getSelectedText(comboBox)).toContainClass("widget-combobox-placeholder-empty");
161+
162+
// open the dropdown
163+
await page.click(".mx-name-comboBox2");
164+
165+
// select europe
166+
await getOptionItem(comboBox, "Europe").click({ delay: 10 });
167+
await expect(getSelectedText(comboBox)).toContainText("Europe");
168+
169+
// check input stays focused
170+
await expect(getFilterInput(comboBox)).toBeFocused();
171+
172+
// type filter text
173+
await page.keyboard.type("aaa");
174+
175+
// check if filtered
176+
await expect(getOptions(comboBox)).toHaveText(["Antartica", "Australia"]);
177+
});
178+
});
128179
});
180+
181+
function getOptions(combobox) {
182+
return combobox.locator(`[role=listbox] [role=option]`);
183+
}
184+
185+
function getOptionItem(combobox, text) {
186+
return combobox.locator(`[role=listbox] [role=option]:has-text("${text}")`);
187+
}
188+
189+
function getSelectedText(combobox) {
190+
return combobox.locator(".widget-combobox-placeholder-text");
191+
}
192+
193+
function getFilterInput(combobox) {
194+
return combobox.locator("input");
195+
}

packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ exports[`Combo box (Association) renders combobox widget 1`] = `
2323
class="widget-combobox-input"
2424
id="comboBox1"
2525
placeholder=" "
26-
readonly=""
2726
role="combobox"
2827
tabindex="0"
2928
value=""

packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/StaticSelection.spec.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ exports[`Combo box (Static values) renders combobox widget 1`] = `
2323
class="widget-combobox-input"
2424
id="comboBox1"
2525
placeholder=" "
26-
readonly=""
2726
role="combobox"
2827
tabindex="0"
2928
value=""

packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function MultiSelection({
1818
ariaRequired,
1919
...options
2020
}: SelectionBaseProps<MultiSelector>): ReactElement {
21+
const inputRef = useRef<HTMLInputElement>(null);
2122
const {
2223
isOpen,
2324
getToggleButtonProps,
@@ -33,8 +34,7 @@ export function MultiSelection({
3334
items,
3435
setSelectedItems,
3536
toggleSelectedItem
36-
} = useDownshiftMultiSelectProps(selector, options, a11yConfig.a11yStatusMessage);
37-
const inputRef = useRef<HTMLInputElement>(null);
37+
} = useDownshiftMultiSelectProps(selector, options, inputRef, a11yConfig.a11yStatusMessage);
3838
const isSelectedItemsBoxStyle = selector.selectedItemsStyle === "boxes";
3939
const isOptionsSelected = selector.isOptionsSelected();
4040
const inputLabel = getInputLabel(options.inputId);

packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export function SingleSelection({
2626
getMenuProps,
2727
reset,
2828
isOpen,
29-
highlightedIndex
29+
highlightedIndex,
30+
inputValue,
31+
selectItem
3032
} = useDownshiftSingleSelectProps(selector, options, a11yConfig.a11yStatusMessage);
3133
const inputRef = useRef<HTMLInputElement>(null);
3234
const lazyLoading = selector.lazyLoading ?? false;
@@ -63,10 +65,15 @@ export function SingleSelection({
6365
const inputProps = getInputProps(
6466
{
6567
disabled: selector.readOnly,
66-
readOnly: selector.options.filterType === "none" || !!selector.currentId,
68+
readOnly: selector.options.filterType === "none",
6769
ref: inputRef,
6870
"aria-required": ariaRequired.value,
69-
"aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined
71+
"aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined,
72+
onKeyDown: e => {
73+
if (e.key === "Backspace" && inputValue === "") {
74+
selectItem(null);
75+
}
76+
}
7077
},
7178
{ suppressRefError: true }
7279
);

packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftMultiSelectProps.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
useMultipleSelection,
99
UseMultipleSelectionReturnValue
1010
} from "downshift";
11-
import { useCallback, useMemo } from "react";
11+
import { RefObject, useCallback, useMemo } from "react";
1212
import { A11yStatusMessage, MultiSelector } from "../helpers/types";
1313

1414
export type UseDownshiftMultiSelectPropsReturnValue = UseMultipleSelectionReturnValue<string> &
@@ -36,6 +36,7 @@ interface Options {
3636
export function useDownshiftMultiSelectProps(
3737
selector: MultiSelector,
3838
options: Options,
39+
inputRef: RefObject<HTMLInputElement | null>,
3940
a11yStatusMessage: A11yStatusMessage
4041
): UseDownshiftMultiSelectPropsReturnValue {
4142
const {
@@ -57,14 +58,24 @@ export function useDownshiftMultiSelectProps(
5758
selector.setValue(selectedItems ?? []);
5859
},
5960

60-
onStateChange({ selectedItems: newSelectedItems, type }) {
61+
onStateChange({ selectedItems: newSelectedItems, type, activeIndex: newActiveIndex }) {
6162
switch (type) {
6263
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
6364
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
65+
if (newActiveIndex === -1) {
66+
inputRef.current?.focus();
67+
}
68+
setSelectedItems(newSelectedItems!);
69+
break;
6470
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
6571
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
6672
setSelectedItems(newSelectedItems!);
6773
break;
74+
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownNavigationNext:
75+
if (newActiveIndex === -1) {
76+
inputRef.current?.focus();
77+
}
78+
break;
6879
default:
6980
break;
7081
}

packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export function useDownshiftSingleSelectProps(
9494
case useCombobox.stateChangeTypes.FunctionCloseMenu:
9595
return {
9696
...changes,
97+
selectedItem: state.selectedItem,
9798
isOpen: false,
9899
inputValue: ""
99100
};

0 commit comments

Comments
 (0)