Skip to content

Commit 27de5c6

Browse files
committed
fix: prevent focus jump when selecting empty or not empty in filter controls
1 parent be24bd6 commit 27de5c6

8 files changed

Lines changed: 190 additions & 3 deletions

File tree

packages/pluggableWidgets/datagrid-number-filter-web/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Fixed
1010

1111
- We fixed an issue with filter selector dropdown not choosing the best placement on small viewports.
12+
- We fixed an issue where selecting Empty or Not empty could cause keyboard focus to jump away from the filter controls.
1213

1314
## [3.9.0] - 2026-03-23
1415

packages/pluggableWidgets/datagrid-text-filter-web/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Fixed
1010

1111
- We fixed an issue with filter selector dropdown not choosing the best placement on small viewports.
12+
- We fixed an issue where selecting Empty or Not empty could cause keyboard focus to jump away from the filter controls.
1213

1314
## [3.8.1] - 2026-02-19
1415

packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,19 @@ describe("Text Filter", () => {
237237
expect(attribute.setValue).toHaveBeenLastCalledWith(undefined);
238238
});
239239

240+
it("keeps focus in filter controls when empty operator is selected", async () => {
241+
render(<DatagridTextFilter {...commonProps} />);
242+
243+
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
244+
const triggerButton = screen.getByRole("combobox", { name: "Equal" });
245+
246+
await user.click(triggerButton);
247+
await user.click(screen.getByRole("option", { name: "Empty" }));
248+
249+
expect(screen.getByRole("textbox")).toBeDisabled();
250+
expect(document.body).not.toHaveFocus();
251+
});
252+
240253
afterAll(() => {
241254
(window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined;
242255
});

packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/__snapshots__/DatagridTextFilter.spec.tsx.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ exports[`Text Filter with single instance with multiple attributes renders corre
1414
>
1515
<button
1616
aria-activedescendant=""
17-
aria-controls="downshift-:r8:-menu"
17+
aria-controls="downshift-:r9:-menu"
1818
aria-expanded="false"
1919
aria-haspopup="listbox"
2020
aria-label="Equal"
2121
class="btn btn-default filter-selector-button button-icon equal"
22-
id="downshift-:r8:-toggle-button"
22+
id="downshift-:r9:-toggle-button"
2323
role="combobox"
2424
tabindex="0"
2525
>
@@ -28,7 +28,7 @@ exports[`Text Filter with single instance with multiple attributes renders corre
2828
<ul
2929
aria-label="Select filter type"
3030
class="filter-selectors hidden"
31-
id="downshift-:r8:-menu"
31+
id="downshift-:r9:-menu"
3232
role="listbox"
3333
style="position: fixed; left: 0px; top: 0px; transform: translate(0px, 0px);"
3434
/>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { describe, expect, it, jest } from "@jest/globals";
2+
import { NumberFilterController } from "../controllers/input/NumberInputController";
3+
import { Number_InputFilterInterface } from "../typings/InputFilterInterface";
4+
5+
describe("NumberFilterController", () => {
6+
function createFilter(): Number_InputFilterInterface {
7+
return {
8+
storeType: "input",
9+
filterFunction: "equal",
10+
defaultState: ["equal"],
11+
arg1: {
12+
type: "number",
13+
value: undefined,
14+
displayValue: "42",
15+
isValid: true
16+
},
17+
arg2: {
18+
type: "number",
19+
value: undefined,
20+
displayValue: "",
21+
isValid: true
22+
},
23+
reset: jest.fn(),
24+
clear: jest.fn(),
25+
UNSAFE_overwriteFilterFunction: jest.fn(),
26+
UNSAFE_setDefaults: jest.fn()
27+
};
28+
}
29+
30+
it("focuses input for value-based operators", () => {
31+
const controller = new NumberFilterController({
32+
filter: createFilter(),
33+
defaultFilter: "equal",
34+
adjustableFilterFunction: true
35+
});
36+
37+
const focus = jest.fn();
38+
Object.defineProperty(controller.inputRef, "current", {
39+
value: { focus },
40+
writable: true
41+
});
42+
43+
controller.handleFilterFnChange("greater");
44+
45+
expect(focus).toHaveBeenCalledTimes(1);
46+
});
47+
48+
it("does not focus input and clears value for empty", () => {
49+
const controller = new NumberFilterController({
50+
filter: createFilter(),
51+
defaultFilter: "equal",
52+
adjustableFilterFunction: true
53+
});
54+
55+
const focus = jest.fn();
56+
Object.defineProperty(controller.inputRef, "current", {
57+
value: { focus },
58+
writable: true
59+
});
60+
61+
controller.handleFilterFnChange("empty");
62+
63+
expect(controller.input1.value).toBe("");
64+
expect(focus).not.toHaveBeenCalled();
65+
});
66+
67+
it("does not focus input and clears value for notEmpty", () => {
68+
const controller = new NumberFilterController({
69+
filter: createFilter(),
70+
defaultFilter: "equal",
71+
adjustableFilterFunction: true
72+
});
73+
74+
const focus = jest.fn();
75+
Object.defineProperty(controller.inputRef, "current", {
76+
value: { focus },
77+
writable: true
78+
});
79+
80+
controller.handleFilterFnChange("notEmpty");
81+
82+
expect(controller.input1.value).toBe("");
83+
expect(focus).not.toHaveBeenCalled();
84+
});
85+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { describe, expect, it, jest } from "@jest/globals";
2+
import { StringFilterController } from "../controllers/input/StringInputController";
3+
import { String_InputFilterInterface } from "../typings/InputFilterInterface";
4+
5+
describe("StringFilterController", () => {
6+
function createFilter(): String_InputFilterInterface {
7+
return {
8+
storeType: "input",
9+
filterFunction: "equal",
10+
defaultState: ["equal"],
11+
arg1: {
12+
type: "string",
13+
value: undefined,
14+
displayValue: "initial",
15+
isValid: true
16+
},
17+
arg2: {
18+
type: "string",
19+
value: undefined,
20+
displayValue: "",
21+
isValid: true
22+
},
23+
reset: jest.fn(),
24+
clear: jest.fn(),
25+
UNSAFE_overwriteFilterFunction: jest.fn(),
26+
UNSAFE_setDefaults: jest.fn()
27+
};
28+
}
29+
30+
it("focuses input for value-based operators", () => {
31+
const controller = new StringFilterController({
32+
filter: createFilter(),
33+
defaultFilter: "equal",
34+
adjustableFilterFunction: true
35+
});
36+
37+
const focus = jest.fn();
38+
Object.defineProperty(controller.inputRef, "current", {
39+
value: { focus },
40+
writable: true
41+
});
42+
43+
controller.handleFilterFnChange("contains");
44+
45+
expect(focus).toHaveBeenCalledTimes(1);
46+
});
47+
48+
it("does not focus input and clears value for empty", () => {
49+
const controller = new StringFilterController({
50+
filter: createFilter(),
51+
defaultFilter: "equal",
52+
adjustableFilterFunction: true
53+
});
54+
55+
const focus = jest.fn();
56+
Object.defineProperty(controller.inputRef, "current", {
57+
value: { focus },
58+
writable: true
59+
});
60+
61+
controller.handleFilterFnChange("empty");
62+
63+
expect(controller.input1.value).toBe("");
64+
expect(focus).not.toHaveBeenCalled();
65+
});
66+
67+
it("does not focus input and clears value for notEmpty", () => {
68+
const controller = new StringFilterController({
69+
filter: createFilter(),
70+
defaultFilter: "equal",
71+
adjustableFilterFunction: true
72+
});
73+
74+
const focus = jest.fn();
75+
Object.defineProperty(controller.inputRef, "current", {
76+
value: { focus },
77+
writable: true
78+
});
79+
80+
controller.handleFilterFnChange("notEmpty");
81+
82+
expect(controller.input1.value).toBe("");
83+
expect(focus).not.toHaveBeenCalled();
84+
});
85+
});

packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export class NumberFilterController {
6363
this.filter.filterFunction = fn;
6464
if (fn === "empty" || fn === "notEmpty") {
6565
this.input1.setValue("");
66+
return;
6667
}
6768
this.inputRef.current?.focus();
6869
};

packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class StringFilterController {
6868
this.filter.filterFunction = fn;
6969
if (fn === "empty" || fn === "notEmpty") {
7070
this.input1.setValue("");
71+
return;
7172
}
7273
this.inputRef.current?.focus();
7374
};

0 commit comments

Comments
 (0)