From 7cfeff00d16211368d9f29afd313063bd5b47268 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 11:19:17 +0200 Subject: [PATCH 01/14] chore: fix styling separator line margin between tags --- .../themesource/datawidgets/web/_datagrid-dropdown-filter.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..b42beddf67 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; @@ -262,6 +261,7 @@ $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; } From dbb3ad59a93188d68656b1752652e20d8fd6eb21 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 11:24:56 +0200 Subject: [PATCH 02/14] chore: remove unused separator, it is done via css --- .../src/controls/base/ClearButton.tsx | 26 ++++++++++++------- .../src/controls/combobox/Combobox.tsx | 1 - .../src/controls/select/Select.tsx | 1 - 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx b/packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx index 6dba754f23..2a70606529 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx @@ -1,4 +1,4 @@ -import { createElement, Fragment, ReactElement } from "react"; +import { createElement, ReactElement, useCallback } from "react"; import { Cross } from "../picker-primitives"; type ClearButtonClassNamesProps = { @@ -10,18 +10,26 @@ type ClearButtonClassNamesProps = { type ClearButtonProps = { cls: ClearButtonClassNamesProps; onClick?: () => void; - showSeparator?: boolean; visible: boolean; }; export function ClearButton(props: ClearButtonProps): ReactElement | null { - const { cls, onClick, showSeparator, visible } = props; + const { cls, onClick, visible } = props; + + const onClickHandler = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (onClick) { + onClick(); + } + }, + [onClick] + ); + return visible ? ( - - - {showSeparator &&
} - + ) : null; } diff --git a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx b/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx index 6abe9f341e..176f3680b9 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx @@ -55,7 +55,6 @@ export const Combobox = observer(function Combobox(props: ComboboxProps) { props.onClear(); inputRef.current?.focus(); }} - showSeparator={false} visible={!props.empty} /> diff --git a/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx b/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx index 4d0770839c..c273f9af9a 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx @@ -52,14 +52,7 @@ export const Select = observer(function Select(props: SelectProps): React.ReactE > {props.value}
- { - props.onClear(); - toggleRef.current?.focus(); - }} - visible={showClear} - /> +
diff --git a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx index c13aecf3be..bdaaf779f8 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx @@ -103,14 +103,7 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea
)} - { - props.onClear(); - inputRef.current?.focus(); - }} - visible={!props.empty} - /> + From 9886f3d538b1526a470c162e13a1aa89938b448d Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 12:16:04 +0200 Subject: [PATCH 05/14] fix: add focus highlight for clear button --- .../datawidgets/web/_datagrid-dropdown-filter.scss | 5 +++++ 1 file changed, 5 insertions(+) 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 b42beddf67..8c5114b35f 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 @@ -149,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 { From b32c9ffb3e22833743a5404553a9aa4d1d77f849 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 13:09:27 +0200 Subject: [PATCH 06/14] chore: restructure select html layout, don't use button, move toggle button up the html tree - makes it easier to open the popup, previously it was not opening if clicked inside, but next to the border. - fixes issue with nested button tags. --- .../src/controls/base/OptionsWrapper.tsx | 9 +++++++- .../src/controls/select/Select.tsx | 22 ++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx b/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx index f0d8782c39..1fbf0994a5 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx @@ -39,7 +39,14 @@ export const OptionsWrapper = forwardRef((props: OptionsWrapperProps, ref: RefOb data-highlighted={highlightedIndex === index || undefined} key={item.value || index} className={cls.menuItem} - {...getItemProps({ item, index, "aria-selected": item.selected })} + {...getItemProps({ + item, + index, + "aria-selected": item.selected, + onClick: e => { + e.stopPropagation(); + } + })} > {showCheckboxes && ( diff --git a/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx b/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx index c273f9af9a..2df0d24218 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx @@ -1,7 +1,7 @@ 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"; @@ -18,7 +18,7 @@ interface SelectProps { style?: React.CSSProperties; useSelectProps: () => UseSelectProps; onClear: () => void; - onFocus?: React.FocusEventHandler; + onFocus?: React.FocusEventHandler; onMenuScroll?: React.UIEventHandler; } @@ -26,7 +26,6 @@ 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() ); @@ -37,25 +36,22 @@ export const Select = observer(function Select(props: SelectProps): React.ReactE return (
- +
Date: Fri, 27 Jun 2025 15:20:09 +0200 Subject: [PATCH 07/14] chore: improve selected tag visibility --- .../themesource/datawidgets/web/_datagrid-dropdown-filter.scss | 3 +++ 1 file changed, 3 insertions(+) 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 8c5114b35f..bbb25ff02c 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 @@ -270,6 +270,9 @@ $root: ".widget-dropdown-filter"; &:focus-visible { outline: var(--brand-primary, #264ae5) auto 1px; } + &:focus { + background-color: var(--color-primary-light, $color-primary-light); + } } #{$root}-input { From 747fb05868d84cee3e38d4cacc0a9f56257ae577 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 15:31:06 +0200 Subject: [PATCH 08/14] chore: do not close popup if already open and input is clicked --- .../picker/mixins/TagPickerControllerMixin.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts index 42ed406a37..8bf59339e7 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts @@ -142,6 +142,15 @@ export function TagPickerControllerMixin(Base: TBa highlightedIndex: state.highlightedIndex, inputValue: state.inputValue }; + case useCombobox.stateChangeTypes.InputClick: + if (state.isOpen) { + return { + ...changes, + isOpen: true, + highlightedIndex: state.highlightedIndex + }; + } + return changes; default: return { ...changes, From 862fa366411437292cc3d6142d495aa1a66d9424 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 16:33:23 +0200 Subject: [PATCH 09/14] chore: activate widget on clicking on the whole widget area --- .../src/controls/tag-picker/TagPicker.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx index bdaaf779f8..7c9d1f1785 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx @@ -31,12 +31,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 +62,16 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea data-expanded={isOpen} data-empty={props.empty ? true : undefined} style={props.style} + onClick={event => { + if (!isOpen && (event.target === event.currentTarget || event.target === inputContainerRef.current)) { + openMenu(); + } + }} > Current filter values: -
{ - if (event.currentTarget === event.target) { - inputRef.current?.focus(); - } - }} - > +
{selectedStyle === "boxes" && props.selected.map((item, index) => (
From d49507235f3851bf38c078c71158431405f3a76d Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 27 Jun 2025 16:45:22 +0200 Subject: [PATCH 10/14] chore: focus on tags when left arrow of backspace is clicked with cursor position 0 --- .../src/controls/tag-picker/TagPicker.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx index 7c9d1f1785..ad47d75e94 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx @@ -63,8 +63,12 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea data-empty={props.empty ? true : undefined} style={props.style} onClick={event => { - if (!isOpen && (event.target === event.currentTarget || event.target === inputContainerRef.current)) { - openMenu(); + if (event.target === event.currentTarget || event.target === inputContainerRef.current) { + if (!isOpen) { + openMenu(); + } else { + inputContainerRef.current?.querySelector("input")?.focus(); + } } }} > @@ -98,7 +102,7 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea onBlur: props.onBlur, onFocus: props.onFocus, placeholder: props.empty ? props.inputPlaceholder : undefined, - ...getDropdownProps({ preventKeyAction: isOpen }), + ...getDropdownProps(), "aria-describedby": props.empty ? undefined : `${helperText1} ${inputContainerId}` })} /> From c1d6c143dc4f8b875f4426dc7c50b63f23b6b74d Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Mon, 30 Jun 2025 11:40:08 +0200 Subject: [PATCH 11/14] chore: shrink input when there is no focus, prevents empty line when not focused --- .../datawidgets/web/_datagrid-dropdown-filter.scss | 8 ++++++++ 1 file changed, 8 insertions(+) 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 bbb25ff02c..23ff78272d 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 @@ -281,6 +281,14 @@ $root: ".widget-dropdown-filter"; width: initial; } + &:not(:focus-within) { + #{$root}-input { + opacity: 0; + flex-shrink: 1; + min-width: 1px; + } + } + #{$root}-clear { border-color: transparent; } From ad58bbc79bcc935ff4d247284168b9910b624c37 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Mon, 30 Jun 2025 16:15:05 +0200 Subject: [PATCH 12/14] feat: rework captions - introduce new options to configure placeholder text and empty caption - show placeholder when widget is not open and empty - split emptySelectionCaption and emptyOptionCaption --- .../datawidgets/web/_datagrid-dropdown-filter.scss | 2 +- .../src/DatagridDropdownFilter.editorConfig.ts | 4 +++- .../src/DatagridDropdownFilter.editorPreview.tsx | 6 +----- .../src/DatagridDropdownFilter.tsx | 4 +++- .../src/DatagridDropdownFilter.xml | 12 +++++++++++- .../src/components/RefFilterContainer.tsx | 6 +++++- .../src/components/StaticFilterContainer.tsx | 6 +++++- .../typings/DatagridDropdownFilterProps.d.ts | 4 ++++ .../src/controllers/picker/PickerBaseController.ts | 3 ++- .../src/controllers/picker/RefBaseController.ts | 5 +++-- .../src/controllers/picker/RefComboboxController.ts | 3 ++- .../src/controllers/picker/RefSelectController.ts | 4 ++-- .../src/controllers/picker/RefTagPickerController.ts | 3 ++- .../src/controllers/picker/StaticBaseController.ts | 5 +++-- .../controllers/picker/StaticComboboxController.ts | 1 + .../src/controllers/picker/StaticSelectController.ts | 4 ++-- .../controllers/picker/StaticTagPickerController.ts | 3 ++- .../picker/mixins/ComboboxControllerMixin.ts | 1 + .../picker/mixins/SelectControllerMixin.ts | 6 +++--- .../picker/mixins/TagPickerControllerMixin.ts | 1 + .../src/controls/base/OptionsWrapper.tsx | 2 +- .../src/controls/combobox/Combobox.tsx | 3 ++- .../src/controls/tag-picker/TagPicker.tsx | 3 ++- 23 files changed, 62 insertions(+), 29 deletions(-) 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 23ff78272d..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 @@ -281,7 +281,7 @@ $root: ".widget-dropdown-filter"; width: initial; } - &:not(:focus-within) { + &:not(:focus-within):not([data-empty]) { #{$root}-input { opacity: 0; flex-shrink: 1; 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 cd2dbe6373..5194e4211a 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts @@ -20,6 +20,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) { @@ -54,7 +56,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 630a2a9ce4..9fede9d5c2 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx @@ -26,11 +26,7 @@ const noop = (): void => {}; function getPreviewValue(props: DatagridDropdownFilterPreviewProps): string { let value = props.defaultValue; - if (!props.filterable) { - value ||= props.emptyOptionCaption || "Select"; - } else { - value ||= "Search"; - } + value ||= props.emptySelectionCaption || (props.filterable ? "Search" : "Select"); return value; } diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx index 5435f96422..fb7ff098a6 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx @@ -16,7 +16,9 @@ function Container(props: DatagridDropdownFilterContainerProps & Select_FilterAP parentChannelName: props.parentChannelName, name: props.name, multiselect: props.multiSelect, - emptyCaption: props.emptyOptionCaption?.value, + emptySelectionCaption: props.emptySelectionCaption?.value ?? "", + emptyOptionCaption: props.emptyOptionCaption?.value ?? "", + placeholder: props.filterInputPlaceholderCaption?.value ?? "", defaultValue: props.defaultValue?.value, filterable: props.filterable, selectionMethod: props.selectionMethod, diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml index 5e32a4a329..b28a99f14d 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml @@ -83,13 +83,23 @@ - + Input caption Assistive technology will read this upon reaching the input element. + + + Empty selection caption + This text is shown if no options are selected. For example 'No options are selected' or 'Select color'. + + + Filter input placeholder + This text is shown as placeholder for filterable filters. For example 'type to search'. + + diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx index 5b332e833e..50dd78fec4 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx @@ -18,7 +18,9 @@ export interface RefFilterContainerProps { ariaLabel?: string; className?: string; defaultValue?: string; - emptyCaption?: string; + emptyOptionCaption: string; + emptySelectionCaption: string; + placeholder: string; filterStore: RefFilterStore; multiselect: boolean; name: string; @@ -80,6 +82,7 @@ const ComboboxWidget = observer(function ComboboxWidget(props: RefFilterContaine ; onChange?: ActionValue; ariaLabel?: DynamicValue; + emptySelectionCaption?: DynamicValue; + filterInputPlaceholderCaption?: DynamicValue; } export interface DatagridDropdownFilterPreviewProps { @@ -62,4 +64,6 @@ export interface DatagridDropdownFilterPreviewProps { valueAttribute: string; onChange: {} | null; ariaLabel: string; + emptySelectionCaption: string; + filterInputPlaceholderCaption: string; } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts index a762277879..d88aaffd5f 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts @@ -58,5 +58,6 @@ export interface PickerBaseControllerProps { multiselect: boolean; onChange?: ActionValue; valueAttribute?: EditableValue; - emptyCaption?: string; + emptyOptionCaption?: string; + emptySelectionCaption?: string; } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts index c355a2e530..43a282cd76 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts @@ -35,6 +35,7 @@ export interface RefBaseControllerProps { multiselect: boolean; onChange?: ActionValue; valueAttribute?: EditableValue; - emptyCaption?: string; - placeholder?: string; + emptyOptionCaption: string; + emptySelectionCaption: string; + placeholder: string; } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts index d211132988..bbc714374b 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts @@ -4,7 +4,8 @@ import { RefBaseController, RefBaseControllerProps } from "./RefBaseController"; export class RefComboboxController extends ComboboxControllerMixin(RefBaseController) { constructor(props: RefBaseControllerProps) { super({ ...props, multiselect: false }); - this.inputPlaceholder = props.placeholder ?? "Search"; + this.inputPlaceholder = props.placeholder; + this.emptyCaption = props.emptySelectionCaption; } handleFocus = (event: React.FocusEvent): void => { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts index 6d5c46280c..d79b1844a0 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts @@ -4,8 +4,8 @@ import { SelectControllerMixin } from "./mixins/SelectControllerMixin"; export class RefSelectController extends SelectControllerMixin(RefBaseController) { constructor(props: RefBaseControllerProps) { super(props); - this.emptyOption.caption = props.emptyCaption || "None"; - this.placeholder = props.placeholder || "Search"; + this.emptyOption.caption = props.emptyOptionCaption; + this.emptyCaption = props.emptySelectionCaption; } handleFocus = (): void => { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts index 0595766f8d..43ba9178a7 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts @@ -15,7 +15,8 @@ export class RefTagPickerController extends TagPickerControllerMixin(RefBaseCont constructor(props: Props) { super(props); - this.inputPlaceholder = props.placeholder ?? "Search"; + this.inputPlaceholder = props.placeholder; + this.emptyCaption = props.emptySelectionCaption; this.filterSelectedOptions = props.selectionMethod === "rowClick"; this.selectedStyle = props.selectedItemsStyle; this.selectionMethod = this.selectedStyle === "boxes" ? props.selectionMethod : "checkbox"; diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts index b9506dcd99..62d75fa38a 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts @@ -55,8 +55,9 @@ export interface StaticBaseControllerProps { multiselect: boolean; onChange?: ActionValue; valueAttribute?: EditableValue; - emptyCaption?: string; - placeholder?: string; + emptyOptionCaption: string; + emptySelectionCaption: string; + placeholder: string; } export interface CustomOption { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts index c6410a631c..a22e8ccf1b 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts @@ -5,5 +5,6 @@ export class StaticComboboxController extends ComboboxControllerMixin(StaticBase constructor(props: StaticBaseControllerProps) { super({ ...props, multiselect: false }); this.inputPlaceholder = props.placeholder ?? "Search"; + this.emptyCaption = props.emptySelectionCaption; } } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts index 8174fbb975..5159b517c6 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts @@ -4,7 +4,7 @@ import { SelectControllerMixin } from "./mixins/SelectControllerMixin"; export class StaticSelectController extends SelectControllerMixin(StaticBaseController) { constructor(props: StaticBaseControllerProps) { super(props); - this.emptyOption.caption = props.emptyCaption || "None"; - this.placeholder = props.emptyCaption || "Select"; + this.emptyOption.caption = props.emptyOptionCaption; + this.emptyCaption = props.emptySelectionCaption; } } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts index 11bc90fd30..9ca14499a7 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts @@ -15,7 +15,8 @@ export class StaticTagPickerController extends TagPickerControllerMixin(StaticBa constructor(props: Props) { super(props); - this.inputPlaceholder = props.placeholder ?? "Search"; + this.inputPlaceholder = props.placeholder; + this.emptyCaption = props.emptySelectionCaption; this.filterSelectedOptions = props.selectionMethod === "rowClick"; this.selectedStyle = props.selectedItemsStyle; this.selectionMethod = this.selectedStyle === "boxes" ? props.selectionMethod : "checkbox"; diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts index 0364d9331f..cf24e575e0 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts @@ -26,6 +26,7 @@ export function ComboboxControllerMixin(Base: TBas touched = false; inputValue = ""; inputPlaceholder = ""; + emptyCaption = ""; constructor(...args: any[]) { super(...args); diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts index 07da0ed116..d2fc89e029 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts @@ -22,11 +22,11 @@ const none = "[[__none__]]" as const; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function SelectControllerMixin(Base: TBase) { return class SelectControllerMixin extends Base { - placeholder = "Select"; + emptyCaption = ""; readonly emptyOption = { value: none, - caption: "None", + caption: "", selected: false }; @@ -55,7 +55,7 @@ export function SelectControllerMixin(Base: TBase) const selected = this.filterStore.selectedOptions; if (selected.length < 1) { - return this.placeholder; + return this.emptyCaption; } return selected.map(option => option.caption).join(", "); diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts index 8bf59339e7..5a2079b33c 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts @@ -27,6 +27,7 @@ export function TagPickerControllerMixin(Base: TBa touched = false; inputValue = ""; inputPlaceholder = ""; + emptyCaption = ""; filterSelectedOptions = false; constructor(...args: any[]) { diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx b/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx index 1fbf0994a5..00962c98eb 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx @@ -61,7 +61,7 @@ export const OptionsWrapper = forwardRef((props: OptionsWrapperProps, ref: RefOb /> )} - {item.caption} + {item.caption || "\u00A0"} ))} diff --git a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx b/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx index d13f4a8951..b094302af6 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx @@ -11,6 +11,7 @@ import { Arrow, classes } from "../picker-primitives"; interface ComboboxProps { options: OptionWithState[]; inputPlaceholder: string; + emptyCaption: string; empty: boolean; className?: string; style?: React.CSSProperties; @@ -46,7 +47,7 @@ export const Combobox = observer(function Combobox(props: ComboboxProps) { ref: inputRef, onBlur: props.onBlur, onFocus: props.onFocus, - placeholder: props.inputPlaceholder + placeholder: props.empty ? (isOpen ? props.inputPlaceholder : props.emptyCaption) : undefined })} /> diff --git a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx index ad47d75e94..0288a7c68b 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx @@ -13,6 +13,7 @@ interface TagPickerProps { options: OptionWithState[]; empty: boolean; inputPlaceholder: string; + emptyCaption: string; showCheckboxes: boolean; selectedStyle?: "boxes" | "text"; ariaLabel?: string; @@ -101,7 +102,7 @@ export const TagPicker = observer(function TagPicker(props: TagPickerProps): Rea "aria-label": inputLabel, onBlur: props.onBlur, onFocus: props.onFocus, - placeholder: props.empty ? props.inputPlaceholder : undefined, + placeholder: props.empty ? (isOpen ? props.inputPlaceholder : props.emptyCaption) : undefined, ...getDropdownProps(), "aria-describedby": props.empty ? undefined : `${helperText1} ${inputContainerId}` })} From f2341aecf229d89c61631101d0a7680541eeed36 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Mon, 30 Jun 2025 17:16:22 +0200 Subject: [PATCH 13/14] fix: don't close popup on click --- .../picker/mixins/SelectControllerMixin.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts index d2fc89e029..fe2ab40320 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts @@ -87,6 +87,18 @@ export function SelectControllerMixin(Base: TBase) if (this.multiselect) { props.stateReducer = (state, { changes, type }) => { switch (type) { + case useSelect.stateChangeTypes.ToggleButtonClick: + if (state.isOpen) { + return { + ...changes, + isOpen: true, + highlightedIndex: state.highlightedIndex + }; + } + + return { + ...changes + }; case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter: case useSelect.stateChangeTypes.ItemClick: return { From 7d290091fd3b6441a21fcd4020e9299840fba347 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 3 Jul 2025 09:42:42 +0200 Subject: [PATCH 14/14] fix: unit tests --- .../components/__tests__/DataGridDropdownFilter.spec.tsx | 2 ++ .../src/controls/combobox/Combobox.tsx | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx index 2306dd4a38..ac36e7f529 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx @@ -22,6 +22,8 @@ const commonProps = { groupKey: "dropdown-filter", filterable: false, clearable: true, + emptyOptionCaption: dynamicValue("None"), + emptySelectOptionCaption: dynamicValue("Select"), selectionMethod: "checkbox" as const, selectedItemsStyle: "text" as const }; diff --git a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx b/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx index b094302af6..cc3c92c3d2 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx @@ -26,9 +26,8 @@ const cls = classes(); export const Combobox = observer(function Combobox(props: ComboboxProps) { const inputRef = useRef(null); - const { isOpen, highlightedIndex, getInputProps, getToggleButtonProps, getMenuProps, getItemProps } = useCombobox( - props.useComboboxProps() - ); + const { isOpen, highlightedIndex, getInputProps, getToggleButtonProps, getMenuProps, getItemProps, getLabelProps } = + useCombobox(props.useComboboxProps()); const { refs, floatingStyles } = useFloatingMenu(isOpen); @@ -40,10 +39,12 @@ export const Combobox = observer(function Combobox(props: ComboboxProps) { data-empty={props.empty ? true : undefined} style={props.style} > +
+ {props.emptyCaption} +