From 2df05505dbce4ab43335b3fc7a9f80014d6f29cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Thu, 30 Jan 2025 11:44:15 +0100 Subject: [PATCH] MultiCombobox: Add `Clear all` button (#99668) --- .../Combobox/MultiCombobox.test.tsx | 26 ++++++++++++++ .../src/components/Combobox/MultiCombobox.tsx | 36 +++++++++++++++++-- .../Combobox/getMultiComboboxStyles.ts | 5 +-- public/locales/en-US/grafana.json | 3 ++ public/locales/pseudo-LOCALE/grafana.json | 3 ++ 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 8ed509b26db..fa1274b5666 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -128,6 +128,32 @@ describe('MultiCombobox', () => { expect(await screen.findByText('d')).toBeInTheDocument(); }); + it('should remove value when clicking on the close icon of the pill', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + const onChange = jest.fn(); + render(); + const fistPillRemoveButton = await screen.findByRole('button', { name: 'Remove A' }); + await user.click(fistPillRemoveButton); + expect(onChange).toHaveBeenCalledWith(options.filter((o) => o.value !== 'a')); + }); + + it('should remove all selected items when clicking on clear all button', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + const onChange = jest.fn(); + render(); + const clearAllButton = await screen.findByTitle('Clear all'); + await user.click(clearAllButton); + expect(onChange).toHaveBeenCalledWith([]); + }); + describe('all option', () => { it('should render all option', async () => { const options = [ diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 1d5ab5bd6d8..a2d5063b096 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -6,6 +6,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useStyles2 } from '../../themes'; import { t } from '../../utils/i18n'; import { Checkbox } from '../Forms/Checkbox'; +import { Icon } from '../Icon/Icon'; import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Portal } from '../Portal/Portal'; @@ -36,7 +37,8 @@ interface MultiComboboxBaseProps extends Omit = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth } = props; + const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth, isClearable } = + props; const styles = useStyles2(getComboboxStyles); const [inputValue, setInputValue] = useState(''); @@ -80,7 +82,7 @@ export const MultiCombobox = (props: MultiComboboxPro [selectedItems] ); - const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem } = + const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem, reset } = useMultipleSelection({ selectedItems, // initally selected items, onStateChange: ({ type, selectedItems: newSelectedItems }) => { @@ -91,6 +93,7 @@ export const MultiCombobox = (props: MultiComboboxPro case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: case useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem: case useMultipleSelection.stateChangeTypes.FunctionSetSelectedItems: + case useMultipleSelection.stateChangeTypes.FunctionReset: // Unclear why newSelectedItems would be undefined, but this seems logical onChange(newSelectedItems ?? []); break; @@ -220,7 +223,16 @@ export const MultiCombobox = (props: MultiComboboxPro }); const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(options, isOpen); - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled, width, minWidth, maxWidth); + const multiStyles = useStyles2( + getMultiComboboxStyles, + isOpen, + invalid, + disabled, + width, + minWidth, + maxWidth, + isClearable + ); const virtualizerOptions = { count: options.length, @@ -284,6 +296,24 @@ export const MultiCombobox = (props: MultiComboboxPro />
+ {isClearable && selectedItems.length > 0 && ( + { + e.stopPropagation(); + reset(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + reset(); + } + }} + /> + )}
diff --git a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts index 06bd2dd2a22..3b330240995 100644 --- a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts @@ -12,7 +12,8 @@ export const getMultiComboboxStyles = ( disabled?: boolean, width?: number | 'auto', minWidth?: number, - maxWidth?: number + maxWidth?: number, + isClearable?: boolean ) => { const inputStyles = getInputStyles({ theme, invalid }); const focusStyles = getFocusStyles(theme); @@ -35,7 +36,7 @@ export const getMultiComboboxStyles = ( width: '100%', gap: theme.spacing(0.5), padding: theme.spacing(0.5), - paddingRight: 28, // Account for suffix + paddingRight: isClearable ? theme.spacing(5) : 28, // Account for suffix '&:focus-within': { ...focusStyles, }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 3b6111cb266..6f9cda10abe 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2072,6 +2072,9 @@ "all": { "title": "All", "title-filtered": "All (filtered)" + }, + "clear": { + "title": "Clear all" } }, "nav": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f04a543f927..6b3f2d081a7 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2072,6 +2072,9 @@ "all": { "title": "Åľľ", "title-filtered": "Åľľ (ƒįľŧęřęđ)" + }, + "clear": { + "title": "Cľęäř äľľ" } }, "nav": {