diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid-dropdown-filter.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid-dropdown-filter.scss index b77663d6cc..6be2a5dda0 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid-dropdown-filter.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid-dropdown-filter.scss @@ -138,7 +138,6 @@ $root: ".widget-dropdown-filter"; &-clear { @include btn-with-cross; align-items: center; - align-self: center; display: flex; flex-shrink: 0; justify-self: end; @@ -150,6 +149,11 @@ $root: ".widget-dropdown-filter"; &:has(+ #{$root}-toggle) { border-inline-end: 1px solid var(--gray, #787d87); } + + &:focus { + border-radius: 2px; + outline: 2px solid var(--brand-primary, $brand-primary); + } } &-state-icon { @@ -262,9 +266,13 @@ $root: ".widget-dropdown-filter"; justify-content: center; line-height: 1.334; padding: var(--wdf-tag-padding); + margin: var(--spacing-smallest, 2px); &:focus-visible { outline: var(--brand-primary, #264ae5) auto 1px; } + &:focus { + background-color: var(--color-primary-light, $color-primary-light); + } } #{$root}-input { @@ -273,6 +281,14 @@ $root: ".widget-dropdown-filter"; width: initial; } + &:not(:focus-within):not([data-empty]) { + #{$root}-input { + opacity: 0; + flex-shrink: 1; + min-width: 1px; + } + } + #{$root}-clear { border-color: transparent; } diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js-snapshots/dataGridDateFilter-chromium-linux.png b/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js-snapshots/dataGridDateFilter-chromium-linux.png index d8058f7b58..ce8aaa4df1 100644 Binary files a/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js-snapshots/dataGridDateFilter-chromium-linux.png and b/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js-snapshots/dataGridDateFilter-chromium-linux.png differ diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-dropdown-filter-web/CHANGELOG.md index f3a4b1743f..fe25aabb82 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- 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. +- We improved dropdown widget usability with enhanced keyboard navigation, visual feedback, and interaction behavior. + +### Breaking changes + +- Text configurations for empty option, empty selection, and input placeholder need to be reviewed and reconfigured. + ## [2.10.1] - 2025-04-16 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js b/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js index f0dd977a21..ebaab052ee 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js @@ -26,11 +26,11 @@ test.describe("datagrid-dropdown-filter-web", () => { test.describe("using enumeration as attribute", () => { test("shows the expected result", async ({ page }) => { - await page.click(".mx-name-datagrid1 .mx-name-dataGridDrop_downFilter1"); - await page.waitForSelector(".widget-dropdown-filter-menu-slot > ul > li:nth-child(2)"); - await page.click(".widget-dropdown-filter-menu-slot > ul > li:nth-child(2)"); + await page.locator(".mx-name-datagrid1 .mx-name-dataGridDrop_downFilter1").click({ delay: 1 }); + await page.waitForSelector(".widget-dropdown-filter-menu-slot > ul > li:nth-child(1)"); + await page.locator(".widget-dropdown-filter-menu-slot > ul > li:nth-child(1)").click({ delay: 1 }); await page.waitForTimeout(300); // wait for filter to apply - await page.click("#DataGrid4-column0"); + await page.locator("#DataGrid4-column0").click({ delay: 1 }); const cells = await page.$$eval(".mx-name-datagrid1 .td", elements => elements.map(element => element.textContent) ); @@ -38,13 +38,13 @@ test.describe("datagrid-dropdown-filter-web", () => { }); test("shows the expected result with multiple selected items", async ({ page }) => { - await page.click(".mx-name-datagrid1 .mx-name-dataGridDrop_downFilter1"); + await page.locator(".mx-name-datagrid1 .mx-name-dataGridDrop_downFilter1").click({ delay: 1 }); + await page.waitForSelector(".widget-dropdown-filter-menu-slot > ul > li:nth-child(1)"); + await page.locator(".widget-dropdown-filter-menu-slot > ul > li:nth-child(1)").click({ delay: 1 }); await page.waitForSelector(".widget-dropdown-filter-menu-slot > ul > li:nth-child(2)"); - await page.click(".widget-dropdown-filter-menu-slot > ul > li:nth-child(2)"); - await page.waitForSelector(".widget-dropdown-filter-menu-slot > ul > li:nth-child(3)"); - await page.click(".widget-dropdown-filter-menu-slot > ul > li:nth-child(3)"); + await page.locator(".widget-dropdown-filter-menu-slot > ul > li:nth-child(2)").click({ delay: 1 }); await page.waitForTimeout(300); // wait for filter to apply - await page.click("#DataGrid4-column0"); + await page.locator("#DataGrid4-column0").click({ delay: 1 }); const cells = await page.$$eval(".mx-name-datagrid1 .td", elements => elements.map(element => element.textContent) ); @@ -54,20 +54,20 @@ test.describe("datagrid-dropdown-filter-web", () => { test.describe("using boolean as attribute", () => { test("shows the expected result", async ({ page }) => { - await page.getByRole("combobox", { name: "Empty" }).click(); + await page.getByRole("combobox", { name: "Select me" }).click({ delay: 1 }); const dropdownItem = await page.locator(".widget-dropdown-filter-menu-slot > ul > li:nth-child(3)"); await expect(dropdownItem).toHaveText("No"); - await dropdownItem.click(); - await page.locator("#DataGrid4-column1").click(); + await dropdownItem.click({ delay: 1 }); + await page.locator("#DataGrid4-column1").click({ delay: 1 }); const cells = await page.locator(".mx-name-datagrid1 .tr"); expect(cells).toHaveCount(1); }); test("shows no results when no items selected", async ({ page }) => { - await page.getByRole("combobox", { name: "Empty" }).click(); + await page.getByRole("combobox", { name: "Select me" }).click({ delay: 1 }); const dropdownItem = await page.locator(".widget-dropdown-filter-menu-slot > ul > li:nth-child(1)"); //the first item means none selected - await dropdownItem.click(); - await page.locator("#DataGrid4-column1").click(); + await dropdownItem.click({ delay: 1 }); + await page.locator("#DataGrid4-column1").click({ delay: 1 }); const cells = await page.locator(".mx-name-datagrid1 .tr"); expect(cells).toHaveCount(4); }); diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js-snapshots/dataGridDropDownFilter-undefined-chromium-linux.png b/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js-snapshots/dataGridDropDownFilter-undefined-chromium-linux.png index 2cff2f3e54..c2313fac74 100644 Binary files a/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js-snapshots/dataGridDropDownFilter-undefined-chromium-linux.png and b/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilter.spec.js-snapshots/dataGridDropDownFilter-undefined-chromium-linux.png differ diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilterAssociation.spec.js b/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilterAssociation.spec.js index 7e9425f7a6..bf0b069744 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilterAssociation.spec.js +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/e2e/DataGridDropDownFilterAssociation.spec.js @@ -15,25 +15,25 @@ test.describe("datagrid-dropdown-filter-web", () => { test("show list of Companies with empty option on top of the list", async ({ page }) => { const menu = () => page.locator("text=FMC Corp"); - await page.locator(".mx-name-drop_downFilter2").click(); + await page.locator(".mx-name-drop_downFilter2").click({ delay: 1 }); await expect(menu()).toBeVisible(); const list = page.locator(".widget-dropdown-filter-menu-slot > ul > li"); await expect(list).toHaveCount(21); - await expect(list.nth(0)).toHaveText("None"); + await expect(list.nth(0)).toHaveText("Nada"); await expect(list.nth(2)).toHaveText("FMC Corp"); await expect(list.nth(20)).toHaveText("PETsMART Inc"); }); test("set value after option is clicked", async ({ page }) => { - const select = () => page.getByRole("columnheader", { name: "sort Company" }).getByLabel("Search"); + const select = () => page.getByRole("combobox", { name: "Select company" }); const toggle = page.locator(".widget-dropdown-filter-toggle"); const menu = () => page.locator("text=FMC Corp"); const option1 = () => page.getByRole("option", { name: "Brown-Forman Corporation" }); - const clickOutside = async () => page.locator("body").click(); + const clickOutside = async () => page.locator("body").click({ delay: 1 }); - await select().click(); + await select().click({ delay: 1 }); await expect(menu()).toBeVisible(); - await option1().click(); + await option1().click({ delay: 1 }); await expect(toggle.nth(3)).toHaveText("Brown-Forman Corporation"); await clickOutside(); await expect(menu()).not.toBeVisible(); @@ -44,14 +44,18 @@ test.describe("datagrid-dropdown-filter-web", () => { }); test.describe("multiselect", () => { + let select; + let menu; + test.beforeEach(async ({ page }) => { + select = () => page.getByRole("combobox", { name: "Select role" }); + menu = () => select().getByRole("listbox"); + }); test("shows list of Roles", async ({ page }) => { - const select = () => page.getByRole("columnheader", { name: "Roles" }).getByLabel("Search"); - const menu = () => page.locator("text=Economist"); - const option1 = () => page.getByRole("option", { name: "Economist" }); - const option2 = () => page.getByRole("option", { name: "Public librarian" }); - const option3 = () => page.getByRole("option", { name: "Prison officer" }); + const option1 = () => menu().getByRole("option", { name: "Economist" }); + const option2 = () => menu().getByRole("option", { name: "Public librarian" }); + const option3 = () => menu().getByRole("option", { name: "Prison officer" }); - await select().click(); + await select().click({ delay: 1 }); await expect(menu().first()).toBeVisible(); await expect(option1()).toBeVisible(); await expect(option2()).toBeVisible(); @@ -59,11 +63,10 @@ test.describe("datagrid-dropdown-filter-web", () => { }); test("does filtering when option is checked", async ({ page }) => { - const select = () => page.getByRole("columnheader", { name: "Roles" }).getByLabel("Search"); - const option2 = () => page.getByRole("option", { name: "Public librarian" }); + const option2 = () => menu().getByRole("option", { name: "Public librarian" }); - await select().click(); - await option2().click(); + await select().click({ delay: 1 }); + await option2().click({ delay: 1 }); const rows = page.locator(".mx-name-dataGrid21 .tr"); await expect(rows).toHaveCount(5); // 4 rows + 1 header row }); @@ -75,7 +78,7 @@ test.describe("datagrid-dropdown-filter-web", () => { const menu = () => page.getByRole("option", { name: "Environmental scientist" }); const clickOutside = async () => (await page.locator("body")).click(); - await select().click(); + await select().click({ delay: 1 }); const checkedOptions = await menu().locator("input:checked"); await expect(checkedOptions).toHaveCount(0); await clickOutside(); @@ -88,8 +91,8 @@ test.describe("datagrid-dropdown-filter-web", () => { const menu = () => page.getByRole("option", { name: "Environmental scientist" }); const option1 = () => page.getByRole("option", { name: "Environmental scientist" }); - await select().click(); - await option1().click(); + await select().click({ delay: 1 }); + await option1().click({ delay: 1 }); const checkedOptions = await menu().locator("input:checked"); await expect(checkedOptions).toHaveCount(1); await expect(checkedOptions.first()).toBeChecked(); @@ -106,9 +109,9 @@ test.describe("datagrid-dropdown-filter-web", () => { const option1 = () => page.getByRole("option", { name: "Environmental scientist" }); const option2 = () => page.getByRole("option", { name: "Trader" }); - await select().click(); - await option1().click(); - await option2().click(); + await select().click({ delay: 1 }); + await option1().click({ delay: 1 }); + await option2().click({ delay: 1 }); const checkedOptions = await menu().locator("input:checked"); await expect(checkedOptions).toHaveCount(2); await expect(checkedOptions.first()).toBeChecked(); diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json b/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json index 0e6f46aeee..de81e79b9b 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/datagrid-dropdown-filter-web", "widgetName": "DatagridDropdownFilter", - "version": "3.0.0", + "version": "3.0.1", "description": "", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", @@ -23,7 +23,7 @@ }, "testProject": { "githubUrl": "https://github.com/mendix/testProjects", - "branchName": "datagrid-dropdown-filter-web/data-widgets-3.0" + "branchName": "datagrid-dropdown-filter-web/dw3.0-dropdown-rework" }, "scripts": { "build": "pluggable-widgets-tools build:ts", diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts index fd528df8a8..feca3faba7 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts @@ -22,6 +22,8 @@ export function getProperties(values: DatagridDropdownFilterPreviewProps, defaul if (values.filterable) { hidePropertyIn(defaultProperties, values, "clearable"); hidePropertyIn(defaultProperties, values, "emptyOptionCaption"); + } else { + hidePropertyIn(defaultProperties, values, "filterInputPlaceholderCaption"); } if (!showSelectedItemsStyle) { @@ -71,7 +73,7 @@ export const getPreview = (values: DatagridDropdownFilterPreviewProps, isDarkMod text({ fontColor: palette.text.secondary, italic: true - })(values.emptyOptionCaption || " ") + })(values.emptySelectionCaption || " ") ], grow: 1 } as ContainerProps, diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx index 59596a465a..942160104a 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx @@ -10,10 +10,12 @@ function Preview(props: DatagridDropdownFilterPreviewProps): ReactElement { return ( - )} + )} - {item.caption} + {item.caption || "\u00A0"} ))} diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx index 6abe9f341e..3eabd1adbf 100644 --- a/packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx @@ -11,6 +11,8 @@ import { Arrow, classes } from "../picker-primitives"; interface ComboboxProps { options: OptionWithState[]; inputPlaceholder: string; + emptyCaption: string; + ariaLabel: string; empty: boolean; className?: string; style?: React.CSSProperties; @@ -42,11 +44,11 @@ export const Combobox = observer(function Combobox(props: ComboboxProps) { diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx index a6aa518c6c..1e7a45a4ed 100644 --- a/packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx @@ -1,7 +1,9 @@ import { useMemo } from "react"; -import { autoUpdate, size, useFloating } from "@floating-ui/react-dom"; +import { autoUpdate, size, useFloating, ReferenceType } from "@floating-ui/react-dom"; -export function useFloatingMenu(open: boolean): ReturnType { +export function useFloatingMenu( + open: boolean +): ReturnType> { const middleware = useMemo( () => [ size({ @@ -15,7 +17,7 @@ export function useFloatingMenu(open: boolean): ReturnType { [] ); - return useFloating({ + return useFloating({ open, placement: "bottom-start", strategy: "fixed", diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx index 03ec32bd94..874024f8ce 100644 --- a/packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx @@ -1,11 +1,12 @@ import cn from "classnames"; import { useSelect, UseSelectProps } from "downshift"; import { observer } from "mobx-react-lite"; -import React, { createElement, useRef } from "react"; +import React, { createElement } from "react"; import { OptionWithState } from "../../typings/OptionWithState"; +import { ClearButton } from "../base/ClearButton"; import { OptionsWrapper } from "../base/OptionsWrapper"; import { useFloatingMenu } from "../hooks/useFloatingMenu"; -import { Arrow, classes, Cross } from "../picker-primitives"; +import { Arrow, classes } from "../picker-primitives"; interface SelectProps { value: string; @@ -13,19 +14,19 @@ interface SelectProps { clearable: boolean; empty: boolean; className?: string; - showCheckboxes?: boolean; + showCheckboxes: boolean; style?: React.CSSProperties; useSelectProps: () => UseSelectProps; onClear: () => void; - onFocus?: React.FocusEventHandler; + onFocus?: React.FocusEventHandler; onMenuScroll?: React.UIEventHandler; + ariaLabel: string; } const cls = classes(); export const Select = observer(function Select(props: SelectProps): React.ReactElement { const { empty: isEmpty, showCheckboxes, clearable } = props; - const toggleRef = useRef(null); const { getToggleButtonProps, getMenuProps, getItemProps, isOpen, highlightedIndex } = useSelect( props.useSelectProps() ); @@ -36,40 +37,32 @@ export const Select = observer(function Select(props: SelectProps): React.ReactE return (
- +
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx index 23f9dc04c5..39050d79d7 100644 --- a/packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx @@ -13,9 +13,10 @@ interface TagPickerProps { options: OptionWithState[]; empty: boolean; inputPlaceholder: string; + emptyCaption: string; showCheckboxes: boolean; selectedStyle?: "boxes" | "text"; - ariaLabel?: string; + ariaLabel: string; className?: string; style?: React.CSSProperties; useMultipleSelectionProps: () => UseMultipleSelectionProps; @@ -31,12 +32,20 @@ const cls = classes(); export const TagPicker = observer(function TagPicker(props: TagPickerProps): React.ReactElement { const [inputContainerId, helperText1] = [useId(), useId()]; const { showCheckboxes, selectedStyle = "boxes", ariaLabel: inputLabel = "Search" } = props; - const inputRef = useRef(null); + const inputContainerRef = useRef(null); const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection( props.useMultipleSelectionProps() ); - const { inputValue, isOpen, highlightedIndex, getInputProps, getToggleButtonProps, getMenuProps, getItemProps } = - useCombobox(props.useComboboxProps()); + const { + inputValue, + isOpen, + highlightedIndex, + getInputProps, + getToggleButtonProps, + getMenuProps, + getItemProps, + openMenu + } = useCombobox(props.useComboboxProps()); const { refs, floatingStyles } = useFloatingMenu(isOpen); return ( @@ -54,19 +63,20 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea data-expanded={isOpen} data-empty={props.empty ? true : undefined} style={props.style} + onClick={event => { + if (event.target === event.currentTarget || event.target === inputContainerRef.current) { + if (!isOpen) { + openMenu(); + } else { + inputContainerRef.current?.querySelector("input")?.focus(); + } + } + }} > Current filter values: -
{ - if (event.currentTarget === event.target) { - inputRef.current?.focus(); - } - }} - > +
{selectedStyle === "boxes" && props.selected.map((item, index) => (
@@ -107,7 +117,7 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea cls={cls} onClick={() => { props.onClear(); - inputRef.current?.focus(); + inputContainerRef.current?.querySelector("input")?.focus(); }} visible={!props.empty} /> @@ -116,6 +126,7 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea