diff --git a/ui/src/dropdown.test.tsx b/ui/src/dropdown.test.tsx index 833f33ad2f..8d53958b9a 100644 --- a/ui/src/dropdown.test.tsx +++ b/ui/src/dropdown.test.tsx @@ -880,4 +880,88 @@ describe('Dropdown.tsx', () => { }) }) }) -}) \ No newline at end of file + + describe('Dialog dropdown exact', () => { + let dialogProps: Dropdown + + const createChoices = (size: number) => Array.from(Array(size).keys()).map(key => ({ name: String(key), label: `Choice ${key}` })) + const overOneHundredChoices = createChoices(101) + const choices = createChoices(10) + + beforeEach(() => { + dialogProps = { + ...defaultProps, + popup: 'always', + exactSearch: true, + choices + }; + }); + it('Sets correct args after exact filter', () => { + const { getByText, getByTestId, getAllByRole } = render() + + fireEvent.click(getByTestId(name)) + fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } }) + fireEvent.click(getAllByRole('checkbox')[0]) + fireEvent.click(getByText('Select')) + + expect(wave.args[name]).toMatchObject(['1', '9']) + }); + it('Filters exact correctly', () => { + const { getByTestId, getAllByRole } = render() + + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } }) + expect(getAllByRole('listitem')).toHaveLength(1) + }); + + it('Filters correctly - reset filter', () => { + const { getByTestId, getAllByRole } = render() + + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } }) + expect(getAllByRole('listitem')).toHaveLength(1) + + fireEvent.change(getByTestId(`${name}-search`), { target: { value: '' } }) + expect(getAllByRole('listitem')).toHaveLength(10) + }); + + it('Resets filtered items on cancel', () => { + const { getByTestId, getAllByRole, getByText } = render() + + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } }) + expect(getAllByRole('listitem')).toHaveLength(1) + fireEvent.click(getByText('Cancel')) + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + }); + + it('Resets filtered items on submit', () => { + const { getByTestId, getAllByRole, getByText } = render() + + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } }) + expect(getAllByRole('listitem')).toHaveLength(1) + fireEvent.click(getByText('Select')) + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + }); + + it('Resets filtered items on single valued submit', () => { + const { getByTestId, getAllByRole } = render() + + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } }) + expect(getAllByRole('listitem')).toHaveLength(1) + fireEvent.click(getAllByRole('checkbox')[0]) + fireEvent.click(getByTestId(name)) + expect(getAllByRole('listitem')).toHaveLength(10) + }); + + }) +}) diff --git a/ui/src/dropdown.tsx b/ui/src/dropdown.tsx index b013578093..b0146f8c1c 100644 --- a/ui/src/dropdown.tsx +++ b/ui/src/dropdown.tsx @@ -17,7 +17,7 @@ import { B, Id, S, U } from './core' import React from 'react' import { stylesheet } from 'typestyle' import { Choice } from './choice_group' -import { fuzzysearch } from './parts/utils' +import { fuzzysearch, exactsearch } from './parts/utils' import { clas, cssVar, pc, px } from './theme' import { wave } from './ui' @@ -61,6 +61,8 @@ export interface Dropdown { tooltip?: S /** Whether to present the choices using a pop-up dialog. By default pops up a dialog only for more than 100 choices. Defaults to 'auto'. */ popup?: 'auto' | 'always' | 'never' + /**Whether the search will be exact or fuzzy */ + exactSearch?: B } type DropdownItem = { @@ -170,10 +172,19 @@ const getPageSpecification = () => ({ itemCount: PAGE_SIZE, height: ROW_HEIGHT * PAGE_SIZE } as Fluent.IPageSpecification), choicesToItems = (choices: Choice[] = [], v?: S | S[]) => choices.map(({ name, label, disabled = false }, idx) => ({ name, text: label || name, idx, checked: Array.isArray(v) ? v.includes(name) : v === name, show: true, disabled })), - useItems = (choices?: Choice[], v?: S | S[]) => { + useItems = (choices?: Choice[], v?: S | S[], exactSearch?: boolean) => { const [items, setItems] = React.useState(choicesToItems(choices, v)) - const onSearchChange = (_e?: React.ChangeEvent, newVal = '') => setItems(items => items.map(i => ({ ...i, show: fuzzysearch(i.text, newVal) }))) + const onSearchChange = (_e?: React.ChangeEvent, newVal = '') => { + setItems((items) => + items.map((i) => ({ + ...i, + show: exactSearch + ? exactsearch(i.text, newVal) // Assuming exactsearch is a function for exact matching + : fuzzysearch(i.text, newVal), // Assuming fuzzysearch is a function for fuzzy matching + })) + ); + }; return [items, setItems, onSearchChange] as const }, onRenderCell = (onChecked: any) => (item?: DropdownItem) => item @@ -323,4 +334,4 @@ export const XDropdown = ({ model: m }: { model: Dropdown }) => ? : (m.choices?.length || 0) > 100 ? - : \ No newline at end of file + : diff --git a/ui/src/parts/utils.ts b/ui/src/parts/utils.ts index b4b2ef7b96..84e3db06b9 100644 --- a/ui/src/parts/utils.ts +++ b/ui/src/parts/utils.ts @@ -24,6 +24,14 @@ export function fuzzysearch(haystack: S, needle: S) { } return true } +// parts / utils.ts +export const exactsearch = (searchTerm: string, itemText: string): boolean => { + + // Convert both strings to lowercase for case-insensitive comparison + return itemText.toLowerCase() === searchTerm.toLowerCase(); // Exact match +}; + + // https://github.com/h2oai/wave/issues/1395. export const fixMenuOverflowStyles: Partial = { @@ -32,4 +40,4 @@ export const fixMenuOverflowStyles: Partial = { '.ms-ContextualMenu-link': { lineHeight: 'unset' }, '.ms-ContextualMenu-submenuIcon': { lineHeight: 'unset', display: 'flex', alignItems: 'center' }, } -} \ No newline at end of file +}