Skip to content

Commit d639810

Browse files
committed
chore: backport dropdown improvements
1 parent 8073e8a commit d639810

30 files changed

+263
-121
lines changed

packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid-dropdown-filter.scss

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@mixin scroll-shadow {
22
background:
3-
/* Shadow Cover TOP */
3+
/* Shadow Cover TOP */
44
linear-gradient(white 30%, rgba(255, 255, 255, 0)) center top,
55
/* Shadow Cover BOTTOM */ linear-gradient(rgba(255, 255, 255, 0), white 70%) center bottom,
66
/* Shadow TOP */ linear-gradient(0deg, rgba(255, 255, 255, 0.6), rgba(197, 197, 197, 0.6)) center top,
@@ -138,7 +138,6 @@ $root: ".widget-dropdown-filter";
138138
&-clear {
139139
@include btn-with-cross;
140140
align-items: center;
141-
align-self: center;
142141
display: flex;
143142
flex-shrink: 0;
144143
justify-self: end;
@@ -150,6 +149,11 @@ $root: ".widget-dropdown-filter";
150149
&:has(+ #{$root}-toggle) {
151150
border-inline-end: 1px solid var(--gray, #787d87);
152151
}
152+
153+
&:focus-visible {
154+
outline-offset: -2px;
155+
outline: var(--brand-primary, $brand-primary) solid 1px;
156+
}
153157
}
154158

155159
&-state-icon {
@@ -262,9 +266,13 @@ $root: ".widget-dropdown-filter";
262266
justify-content: center;
263267
line-height: 1.334;
264268
padding: var(--wdf-tag-padding);
269+
margin: var(--spacing-smallest, 2px);
265270
&:focus-visible {
266271
outline: var(--brand-primary, #264ae5) auto 1px;
267272
}
273+
&:focus {
274+
background-color: var(--color-primary-light, $color-primary-light);
275+
}
268276
}
269277

270278
#{$root}-input {
@@ -273,6 +281,14 @@ $root: ".widget-dropdown-filter";
273281
width: initial;
274282
}
275283

284+
&:not(:focus-within):not([data-empty]) {
285+
#{$root}-input {
286+
opacity: 0;
287+
flex-shrink: 1;
288+
min-width: 1px;
289+
}
290+
}
291+
276292
#{$root}-clear {
277293
border-color: transparent;
278294
}

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

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

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- We enhanced dropdown widget customization by separating texts for empty selection, empty option caption, and input placeholder. This change allows for more granular control over the widget's text configurations.
12+
13+
- We improved dropdown widget usability with enhanced keyboard navigation, visual feedback, and interaction behavior.
14+
15+
### Breaking changes
16+
17+
- Text configurations for empty option, empty selection, and input placeholder need to be reviewed and reconfigured.
18+
919
## [2.10.1] - 2025-04-16
1020

1121
### Fixed

packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mendix/datagrid-dropdown-filter-web",
33
"widgetName": "DatagridDropdownFilter",
4-
"version": "2.10.1",
4+
"version": "2.32.0",
55
"description": "",
66
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
77
"license": "Apache-2.0",

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export function getProperties(values: DatagridDropdownFilterPreviewProps, defaul
2020
if (values.filterable) {
2121
hidePropertyIn(defaultProperties, values, "clearable");
2222
hidePropertyIn(defaultProperties, values, "emptyOptionCaption");
23+
} else {
24+
hidePropertyIn(defaultProperties, values, "filterInputPlaceholderCaption");
2325
}
2426

2527
if (!showSelectedItemsStyle) {
@@ -54,7 +56,7 @@ export const getPreview = (values: DatagridDropdownFilterPreviewProps, isDarkMod
5456
text({
5557
fontColor: palette.text.secondary,
5658
italic: true
57-
})(values.emptyOptionCaption || " ")
59+
})(values.emptySelectionCaption || " ")
5860
],
5961
grow: 1
6062
} as ContainerProps,

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ function Preview(props: DatagridDropdownFilterPreviewProps): ReactElement {
1010
return (
1111
<Select
1212
className={props.class}
13+
ariaLabel={props.ariaLabel}
1314
style={parseStyle(props.style)}
1415
options={[]}
1516
empty={!props.clearable}
1617
clearable={props.clearable}
18+
showCheckboxes={false}
1719
value={getPreviewValue(props)}
1820
onClear={noop}
1921
useSelectProps={() => ({ items: [] })}
@@ -25,11 +27,7 @@ const noop = (): void => {};
2527

2628
function getPreviewValue(props: DatagridDropdownFilterPreviewProps): string {
2729
let value = props.defaultValue;
28-
if (!props.filterable) {
29-
value ||= props.emptyOptionCaption || "Select";
30-
} else {
31-
value ||= "Search";
32-
}
30+
value ||= props.emptySelectionCaption || (props.filterable ? "Search" : "Select");
3331
return value;
3432
}
3533

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { RefFilterContainer } from "./components/RefFilterContainer";
77

88
function Container(props: DatagridDropdownFilterContainerProps & Select_FilterAPIv2): React.ReactElement {
99
const commonProps = {
10-
ariaLabel: props.ariaLabel?.value,
10+
ariaLabel: props.ariaLabel?.value ?? "",
1111
className: props.class,
1212
tabIndex: props.tabIndex,
1313
styles: props.style,
@@ -16,7 +16,9 @@ function Container(props: DatagridDropdownFilterContainerProps & Select_FilterAP
1616
parentChannelName: props.parentChannelName,
1717
name: props.name,
1818
multiselect: props.multiSelect,
19-
emptyCaption: props.emptyOptionCaption?.value,
19+
emptyOptionCaption: props.emptyOptionCaption?.value ?? "",
20+
emptySelectionCaption: props.emptySelectionCaption?.value ?? "",
21+
placeholder: props.filterInputPlaceholderCaption?.value ?? "",
2022
defaultValue: props.defaultValue?.value,
2123
filterable: props.filterable,
2224
selectionMethod: props.selectionMethod,

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545
<property key="emptyOptionCaption" type="textTemplate" required="false">
4646
<caption>Empty option caption</caption>
4747
<description />
48+
<translations>
49+
<translation lang="en_US">None</translation>
50+
<translation lang="nl_NL">Niets</translation>
51+
</translations>
4852
</property>
4953
<property key="clearable" type="boolean" defaultValue="true">
5054
<caption>Clearable</caption>
@@ -83,13 +87,32 @@
8387
</property>
8488
</propertyGroup>
8589
</propertyGroup>
86-
<propertyGroup caption="Accessibility">
90+
<propertyGroup caption="Advanced">
8791
<propertyGroup caption="Accessibility">
8892
<property key="ariaLabel" type="textTemplate" required="false">
8993
<caption>Input caption</caption>
9094
<description>Assistive technology will read this upon reaching the input element.</description>
9195
</property>
9296
</propertyGroup>
97+
<propertyGroup caption="Texts">
98+
<property key="emptySelectionCaption" type="textTemplate" required="false">
99+
<caption>Empty selection caption</caption>
100+
<description>This text is shown if no options are selected. For example 'Select color' or 'No options are selected'.</description>
101+
<translations>
102+
<translation lang="en_US">Select</translation>
103+
<translation lang="nl_NL">Selecteer</translation>
104+
</translations>
105+
</property>
106+
107+
<property key="filterInputPlaceholderCaption" type="textTemplate" required="false">
108+
<caption>Filter input placeholder</caption>
109+
<description>This text is shown as placeholder for filterable filters. For example 'Type to search'.</description>
110+
<translations>
111+
<translation lang="en_US">Search</translation>
112+
<translation lang="nl_NL">Zoeken</translation>
113+
</translations>
114+
</property>
115+
</propertyGroup>
93116
</propertyGroup>
94117
</properties>
95118
</widget>

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import { SelectedItemsStyleEnum, SelectionMethodEnum } from "../../typings/Datag
1515
import { useOnScrollBottom } from "@mendix/widget-plugin-hooks/useOnScrollBottom";
1616

1717
export interface RefFilterContainerProps {
18-
ariaLabel?: string;
18+
ariaLabel: string;
1919
className?: string;
2020
defaultValue?: string;
21-
emptyCaption?: string;
21+
emptyOptionCaption: string;
22+
emptySelectionCaption: string;
23+
placeholder: string;
2224
filterStore: RefFilterStore;
2325
multiselect: boolean;
2426
name: string;
@@ -66,6 +68,7 @@ const SelectWidget = observer(function SelectWidget(props: RefFilterContainerPro
6668
showCheckboxes={ctrl1.multiselect}
6769
className={props.className}
6870
style={props.styles}
71+
ariaLabel={props.ariaLabel}
6972
/>
7073
);
7174
});
@@ -80,6 +83,8 @@ const ComboboxWidget = observer(function ComboboxWidget(props: RefFilterContaine
8083
<Combobox
8184
options={ctrl2.options}
8285
inputPlaceholder={ctrl2.inputPlaceholder}
86+
emptyCaption={ctrl2.emptyCaption}
87+
ariaLabel={props.ariaLabel}
8388
useComboboxProps={ctrl2.useComboboxProps}
8489
onClear={ctrl2.handleClear}
8590
onFocus={ctrl2.handleFocus}
@@ -109,6 +114,8 @@ const TagPickerWidget = observer(function TagPickerWidget(props: RefFilterContai
109114
onFocus={ctrl3.handleFocus}
110115
onMenuScroll={handleMenuScroll}
111116
inputPlaceholder={ctrl3.inputPlaceholder}
117+
emptyCaption={ctrl3.emptyCaption}
118+
ariaLabel={props.ariaLabel}
112119
empty={ctrl3.isEmpty}
113120
showCheckboxes={props.selectionMethod === "checkbox"}
114121
selectedStyle={props.selectedItemsStyle}

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import { usePickerJSActions } from "@mendix/widget-plugin-filtering/helpers/useP
1919
import { useFrontendType } from "../hooks/useFrontendType";
2020

2121
export interface StaticFilterContainerProps {
22-
ariaLabel?: string;
22+
ariaLabel: string;
2323
className?: string;
2424
defaultValue?: string;
25-
emptyCaption?: string;
25+
emptyOptionCaption: string;
26+
emptySelectionCaption: string;
27+
placeholder: string;
2628
filterOptions: FilterOptionsType[];
2729
filterStore: StaticSelectFilterStore;
2830
multiselect: boolean;
@@ -68,6 +70,7 @@ const SelectWidget = observer(function SelectWidget(props: StaticFilterContainer
6870
showCheckboxes={ctrl1.multiselect}
6971
className={props.className}
7072
style={props.styles}
73+
ariaLabel={props.ariaLabel}
7174
/>
7275
);
7376
});
@@ -81,6 +84,8 @@ const ComboboxWidget = observer(function ComboboxWidget(props: StaticFilterConta
8184
<Combobox
8285
options={ctrl2.options}
8386
inputPlaceholder={ctrl2.inputPlaceholder}
87+
emptyCaption={ctrl2.emptyCaption}
88+
ariaLabel={props.ariaLabel}
8489
useComboboxProps={ctrl2.useComboboxProps}
8590
onClear={ctrl2.handleClear}
8691
onFocus={ctrl2.handleFocus}
@@ -106,6 +111,8 @@ const TagPickerWidget = observer(function TagPickerWidget(props: StaticFilterCon
106111
onClear={ctrl3.handleClear}
107112
onBlur={ctrl3.handleBlur}
108113
inputPlaceholder={ctrl3.inputPlaceholder}
114+
emptyCaption={ctrl3.emptyCaption}
115+
ariaLabel={props.ariaLabel}
109116
empty={ctrl3.isEmpty}
110117
showCheckboxes={props.selectionMethod === "checkbox"}
111118
selectedStyle={props.selectedItemsStyle}

packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ const commonProps = {
2222
groupKey: "dropdown-filter",
2323
filterable: false,
2424
clearable: true,
25+
emptySelectionCaption: dynamicValue("Select"),
26+
emptyOptionCaption: dynamicValue("None"),
27+
ariaLabel: dynamicValue("AriaLabel"),
2528
selectionMethod: "checkbox" as const,
2629
selectedItemsStyle: "text" as const
2730
};
@@ -117,7 +120,7 @@ describe("Dropdown Filter", () => {
117120
/>
118121
);
119122

120-
expect(screen.getByRole("combobox")).toHaveAccessibleName("enum_value_1");
123+
expect(document.querySelector(".widget-dropdown-filter-toggle")).toHaveTextContent("enum_value_1");
121124
});
122125

123126
it("don't sync defaultValue with state when defaultValue changes from undefined to string", async () => {
@@ -132,7 +135,7 @@ describe("Dropdown Filter", () => {
132135
);
133136

134137
await waitFor(() => {
135-
expect(screen.getByRole("combobox")).toHaveAccessibleName("Select");
138+
expect(document.querySelector(".widget-dropdown-filter-toggle")).toHaveTextContent("Select");
136139
});
137140

138141
// “Real” context causes widgets to re-renders multiple times, replicate this in mocked context.
@@ -156,7 +159,7 @@ describe("Dropdown Filter", () => {
156159
);
157160

158161
await waitFor(() => {
159-
expect(screen.getByRole("combobox")).toHaveAccessibleName("Select");
162+
expect(document.querySelector(".widget-dropdown-filter-toggle")).toHaveTextContent("Select");
160163
});
161164
});
162165

@@ -172,7 +175,7 @@ describe("Dropdown Filter", () => {
172175
/>
173176
);
174177

175-
expect(screen.getByRole("combobox")).toHaveAccessibleName("xyz");
178+
expect(screen.getByText("xyz", { selector: ".widget-dropdown-filter-toggle" })).toBeInTheDocument();
176179

177180
// “Real” context causes widgets to re-renders multiple times, replicate this in mocked context.
178181
rerender(
@@ -195,7 +198,9 @@ describe("Dropdown Filter", () => {
195198
);
196199

197200
await waitFor(() => {
198-
expect(screen.getByRole("combobox")).toHaveAccessibleName("xyz");
201+
expect(
202+
screen.getByText("xyz", { selector: ".widget-dropdown-filter-toggle" })
203+
).toBeInTheDocument();
199204
});
200205
});
201206
});
@@ -471,8 +476,8 @@ describe("Dropdown Filter", () => {
471476
<DatagridDropdownFilter {...commonProps} auto multiSelect={false} filterOptions={[]} />
472477
);
473478

474-
expect(fragment1().querySelector("button")?.getAttribute("aria-controls")).not.toBe(
475-
fragment2().querySelector("button")?.getAttribute("aria-controls")
479+
expect(fragment1().querySelector("div[role='combobox']")?.getAttribute("aria-controls")).not.toBe(
480+
fragment2().querySelector("div[role='combobox']")?.getAttribute("aria-controls")
476481
);
477482
});
478483

0 commit comments

Comments
 (0)