Skip to content

Commit c08bb7b

Browse files
authored
fix: ComboBox show all items on open (#2328)
DH-18088 & DH-18087: ComboBox now clears search filter when opening the ComboBox unless it is triggered by user input. ### Testing - Typing in a combobox should open it filtered by the search text - Clicking the dropdown should open the CombBox unfiltered - Typing while open should filter results > Note that there seems to be a Spectrum bug that can cause some weird scrolling behavior when typing in an opened ComboBox adobe/react-spectrum#7573 #### menu_trigger="focus" - Clicking search input should open it unfiltered Here's a script with a few different configurations ```python import deephaven.ui as ui from deephaven import time_table import datetime # Ticking table with initial row count of 200 that adds a row every second initial_row_count = 200 _table = time_table( "PT1S", start_time=datetime.datetime.now() - datetime.timedelta(seconds=initial_row_count), ).update( [ "Int=new Integer(i)", "Text=new String(`Display `+i)", ] ) item_list = [ui.item(f"Display {i}") for i in range(1, 201)] # Basic ComboBox @ui.component def ui_combo_box_basic(): value, set_value = ui.use_state("Display 91") return ui.combo_box( item_list, label=f"Basic ({value})", selected_key=value, on_change=set_value, width="size-3000" ) # Uncontrolled ComboBox (Table source) @ui.component def ui_combo_box_uncontrolled(table): value, set_value = ui.use_state("") combo1 = ui.combo_box( ui.item_table_source(table, key_column="Text", label_column="Text"), default_selected_key="Display 92", label=f"Uncontrolled Table Source ({value or 'None'})", on_change=set_value, width="size-3000" ) return combo1 # Controlled ComboBox (Table source) @ui.component def ui_combo_box_controlled(table, menu_trigger): value, set_value = ui.use_state("Display 93") combo1 = ui.combo_box( ui.item_table_source(table, key_column="Text", label_column="Text"), selected_key=value, label=f"Controlled Table Source ({value}) {menu_trigger or ''}", menu_trigger=menu_trigger, on_change=set_value, width="size-3000" ) btn = ui.button("Set Value", on_press=lambda: set_value("Display 104")) return combo1, btn # Controlled input ComboBox (Table source) @ui.component def ui_combo_box_input_controlled(table, menu_trigger): input_value, set_input_value = ui.use_state("Display 94") value, set_value = ui.use_state("Display 94") combo1 = ui.combo_box( ui.item_table_source(table, key_column="Text", label_column="Text"), input_value=input_value, on_input_change=set_input_value, default_selected_key=value, label=f"Controlled Input Table Source ({value}) {menu_trigger or ''}", menu_trigger=menu_trigger, on_change=set_value, width="size-3000" ) btn = ui.button("Set Input", on_press=lambda: set_input_value("Display 104")) return combo1, btn # Layout @ui.component def ui_layout(): return ( ui_combo_box_basic(), ui_combo_box_uncontrolled(_table), ui_combo_box_controlled(_table, None), ui_combo_box_controlled(_table, "focus"), ui_combo_box_input_controlled(_table, None), ) tests = ui_layout() ```
1 parent 3608b15 commit c08bb7b

6 files changed

Lines changed: 60 additions & 12 deletions

File tree

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@react-spectrum/theme-default": "^3.5.1",
3636
"@react-spectrum/toast": "^3.0.0-beta.16",
3737
"@react-spectrum/utils": "^3.11.5",
38+
"@react-types/combobox": "3.13.1",
3839
"@react-types/radio": "^3.8.1",
3940
"@react-types/shared": "^3.22.1",
4041
"@react-types/textfield": "^3.9.1",

packages/components/src/spectrum/comboBox/ComboBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { NormalizedItem } from '../utils';
1010
import { type PickerPropsT, usePickerProps } from '../picker';
1111

1212
export type ComboBoxProps = PickerPropsT<SpectrumComboBoxProps<NormalizedItem>>;
13-
13+
export { type MenuTriggerAction } from '@react-types/combobox';
1414
export { SpectrumComboBox };
1515

1616
export const ComboBox = React.forwardRef(function ComboBox(

packages/components/src/spectrum/picker/usePickerScrollOnOpen.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ describe('usePickerScrollOnOpen', () => {
4949
expect(result.current.ref).toBe(mockUsePopoverOnScrollRefResult.ref);
5050
});
5151

52-
it.each([true, false])(
53-
'should return a callback that calls popoverOnOpenChange and onOpenChange: %s',
54-
isOpen => {
52+
it.each([
53+
[true, undefined],
54+
[false, undefined],
55+
[true, 'input'],
56+
[false, 'input'],
57+
] as const)(
58+
'should return a callback that calls popoverOnOpenChange and onOpenChange: %s, %s',
59+
(isOpen, menuTrigger) => {
5560
const { result } = renderHook(() =>
5661
usePickerScrollOnOpen({
5762
getInitialScrollPosition,
@@ -60,12 +65,12 @@ describe('usePickerScrollOnOpen', () => {
6065
})
6166
);
6267

63-
result.current.onOpenChange(isOpen);
68+
result.current.onOpenChange(isOpen, menuTrigger);
6469

6570
expect(mockUsePopoverOnScrollRefResult.onOpenChange).toHaveBeenCalledWith(
6671
isOpen
6772
);
68-
expect(onOpenChange).toHaveBeenCalledWith(isOpen);
73+
expect(onOpenChange).toHaveBeenCalledWith(isOpen, menuTrigger);
6974
}
7075
);
7176
});

packages/components/src/spectrum/picker/usePickerScrollOnOpen.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import {
44
findSpectrumPickerScrollArea,
55
usePopoverOnScrollRef,
66
} from '@deephaven/react-hooks';
7+
import type { MenuTriggerAction } from '../comboBox';
78

89
export interface UsePickerScrollOnOpenOptions {
910
getInitialScrollPosition?: () => Promise<number | null | undefined>;
1011
onScroll: (event: Event) => void;
11-
onOpenChange?: (isOpen: boolean) => void;
12+
onOpenChange?: (isOpen: boolean, menuTrigger?: MenuTriggerAction) => void;
1213
}
1314

1415
export interface UsePickerScrollOnOpenResult<THtml extends HTMLElement> {
1516
ref: DOMRef<THtml>;
16-
onOpenChange: (isOpen: boolean) => void;
17+
onOpenChange: (isOpen: boolean, menuTrigger?: MenuTriggerAction) => void;
1718
}
1819

1920
/**
@@ -37,11 +38,11 @@ export function usePickerScrollOnOpen<THtml extends HTMLElement = HTMLElement>({
3738
);
3839

3940
const onOpenChangeInternal = useCallback(
40-
(isOpen: boolean): void => {
41+
(isOpen: boolean, menuTrigger?: MenuTriggerAction): void => {
4142
// Attach scroll event handling
4243
popoverOnOpenChange(isOpen);
4344

44-
onOpenChange?.(isOpen);
45+
onOpenChange?.(isOpen, menuTrigger);
4546
},
4647
[onOpenChange, popoverOnOpenChange]
4748
);

packages/jsapi-components/src/spectrum/ComboBox.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {
22
ComboBoxNormalized,
3+
type MenuTriggerAction,
34
type NormalizedItem,
45
type SpectrumComboBoxProps,
56
} from '@deephaven/components';
6-
import { useCallback } from 'react';
7+
import { useCallback, useRef } from 'react';
78
import { type PickerWithTableProps } from './PickerProps';
89
import { usePickerProps } from './utils';
910

@@ -18,19 +19,57 @@ export function ComboBox(props: ComboBoxProps): JSX.Element {
1819
...pickerProps
1920
} = usePickerProps<ComboBoxProps>(props);
2021

22+
const isOpenRef = useRef(false);
23+
const inputValueRef = useRef('');
24+
2125
const onInputChange = useCallback(
2226
(value: string) => {
2327
onInputChangeInternal?.(value);
24-
onSearchTextChange(value);
28+
29+
// Only apply search text if ComboBox is open.
30+
if (isOpenRef.current) {
31+
onSearchTextChange(value);
32+
}
33+
// When the ComboBox is closed, `onInputChange` may have been called as a
34+
// result of user search input, ComboBox selection, or by selected key
35+
// prop changes. We can't determine the source here, so we clear the search
36+
// text and store the search value so that the list is unfiltered the next
37+
// time the ComboBox is opened. We also store the search value so we can
38+
// re-apply it in `onOpenChange` if the ComboBox is opened by user search
39+
// input.
40+
else {
41+
onSearchTextChange('');
42+
inputValueRef.current = value;
43+
}
2544
},
2645
[onInputChangeInternal, onSearchTextChange]
2746
);
2847

48+
const onOpenChange = useCallback(
49+
(isOpen: boolean, menuTrigger?: MenuTriggerAction) => {
50+
pickerProps.onOpenChange?.(isOpen);
51+
52+
// Reset the search text when the ComboBox is closed.
53+
if (!isOpen) {
54+
onSearchTextChange('');
55+
}
56+
// Restore search text when ComboBox has been opened by user input.
57+
else if (menuTrigger === 'input') {
58+
onSearchTextChange(inputValueRef.current);
59+
}
60+
61+
// Store the open state so that `onInputChange` has access to it.
62+
isOpenRef.current = isOpen;
63+
},
64+
[onSearchTextChange, pickerProps]
65+
);
66+
2967
return (
3068
<ComboBoxNormalized
3169
// eslint-disable-next-line react/jsx-props-no-spreading
3270
{...pickerProps}
3371
onInputChange={onInputChange}
72+
onOpenChange={onOpenChange}
3473
/>
3574
);
3675
}

0 commit comments

Comments
 (0)