Skip to content

Commit a30341a

Browse files
authored
feat: ComboBoxNormalized - windowed data component (#2072)
* Created `ComboBoxNormalized` component. This component handles normalized item and section data which is needed to support windowed data. (there will be 1 more PR providing the ComboBox component in the jsapi-components package that will handle the table support. Similar to Picker) * Styleguide example showing controlled data + validation for no selection * Split out `usePickerNormalizedProps` hook from `PickerNormalized` so that the logic could be reused in `ComboBoxNormalized` * Cleaned up some generics for utils using Spectrum `DomRef` resolves #2071
1 parent e6b55cf commit a30341a

20 files changed

Lines changed: 287 additions & 161 deletions

packages/code-studio/src/styleguide/Pickers.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
PickerNormalized,
1010
Checkbox,
1111
ComboBox,
12+
ComboBoxNormalized,
1213
} from '@deephaven/components';
1314
import { vsPerson } from '@deephaven/icons';
1415
import { Icon } from '@adobe/react-spectrum';
@@ -61,10 +62,24 @@ function PersonIcon(): JSX.Element {
6162
}
6263

6364
export function Pickers(): JSX.Element {
64-
const [selectedKey, setSelectedKey] = useState<ItemKey | null>(null);
65+
const [selectedKey, setSelectedKey] = useState<ItemKey | null>(200);
6566

6667
const [showIcons, setShowIcons] = useState(true);
6768

69+
const [filteredItems, setFilteredItems] = useState(itemsWithIcons);
70+
71+
const onSearch = useCallback(
72+
(searchText: string) =>
73+
setFilteredItems(
74+
searchText === ''
75+
? itemsWithIcons
76+
: itemsWithIcons.filter(
77+
({ item }) => item?.textValue?.includes(searchText)
78+
)
79+
),
80+
[]
81+
);
82+
6883
const getInitialScrollPosition = useCallback(
6984
async () =>
7085
getPositionOfSelectedItem({
@@ -163,6 +178,17 @@ export function Pickers(): JSX.Element {
163178
showItemIcons={showIcons}
164179
onChange={onChange}
165180
/>
181+
<ComboBoxNormalized
182+
label="ComboBox (Controlled)"
183+
getInitialScrollPosition={getInitialScrollPosition}
184+
normalizedItems={filteredItems}
185+
selectedKey={selectedKey}
186+
showItemIcons={showIcons}
187+
onChange={onChange}
188+
validationState={selectedKey == null ? 'invalid' : 'valid'}
189+
errorMessage="Please select an item."
190+
onInputChange={onSearch}
191+
/>
166192
</Flex>
167193
</Flex>
168194
</SampleSection>

packages/components/src/SearchableCombobox.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/* eslint-disable react/jsx-props-no-spreading */
22
import { Key, useCallback } from 'react';
33
import { ComboBox, Item, SpectrumComboBoxProps } from '@adobe/react-spectrum';
4-
import type { FocusableRef } from '@react-types/shared';
5-
import type { ReactSpectrumComponent } from '@deephaven/react-hooks';
4+
import type { DOMRefValue, FocusableRef } from '@react-types/shared';
65
import TextWithTooltip from './TextWithTooltip';
76

87
export interface SearchableComboboxProps<TItem, TKey extends Key>
@@ -12,7 +11,7 @@ export interface SearchableComboboxProps<TItem, TKey extends Key>
1211
> {
1312
getItemDisplayText: (item: TItem | null | undefined) => string | null;
1413
getKey: (item: TItem | null | undefined) => TKey | null;
15-
scrollRef: React.RefObject<ReactSpectrumComponent<HTMLElement>>;
14+
scrollRef: React.RefObject<DOMRefValue>;
1615
onSelectionChange: (key: TKey | null) => void;
1716
}
1817

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ export function ComboBox({
1616
const {
1717
defaultSelectedKey,
1818
disabledKeys,
19+
ref,
1920
selectedKey,
20-
scrollRef,
2121
...comboBoxProps
2222
} = usePickerProps(props);
2323

2424
return (
2525
<SpectrumComboBox
2626
// eslint-disable-next-line react/jsx-props-no-spreading
2727
{...comboBoxProps}
28-
ref={scrollRef as FocusableRef<HTMLElement>}
2928
UNSAFE_className={cl('dh-combobox', UNSAFE_className)}
29+
ref={ref as FocusableRef<HTMLElement>}
3030
// Type assertions are necessary here since Spectrum types don't account
3131
// for number and boolean key values even though they are valid runtime
3232
// values.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ComboBox as SpectrumComboBox } from '@adobe/react-spectrum';
2+
import { FocusableRef } from '@react-types/shared';
3+
import cl from 'classnames';
4+
import { PickerNormalizedPropsT, usePickerNormalizedProps } from '../picker';
5+
import { ComboBoxProps } from './ComboBox';
6+
7+
export type ComboBoxNormalizedProps = PickerNormalizedPropsT<ComboBoxProps>;
8+
9+
/**
10+
* ComboBox that takes an array of `NormalizedItem` or `NormalizedSection` items
11+
* as children and uses a render item function to render the items. `NormalizedItem`
12+
* and `NormalizedSection` datums always provide a `key` property but have an
13+
* optional `item` property that can be lazy loaded. This is necessary to support
14+
* windowed data since we need a representative key for every item in the
15+
* collection.
16+
*/
17+
export function ComboBoxNormalized({
18+
UNSAFE_className,
19+
...props
20+
}: ComboBoxNormalizedProps): JSX.Element {
21+
const { forceRerenderKey, ref, ...pickerProps } =
22+
usePickerNormalizedProps<ComboBoxNormalizedProps>(props);
23+
24+
return (
25+
<SpectrumComboBox
26+
// eslint-disable-next-line react/jsx-props-no-spreading
27+
{...pickerProps}
28+
key={forceRerenderKey}
29+
ref={ref as FocusableRef<HTMLElement>}
30+
UNSAFE_className={cl(
31+
'dh-combobox',
32+
'dh-combobox-normalized',
33+
UNSAFE_className
34+
)}
35+
/>
36+
);
37+
}
38+
39+
export default ComboBoxNormalized;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './ComboBox';
2+
export * from './ComboBoxNormalized';

packages/components/src/spectrum/listView/ListViewWrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function ListViewWrapper<T>(
4646
// of the parent container and only render the ListView when it has a non-zero
4747
// height. See https://github.com/adobe/react-spectrum/issues/6213
4848
const { ref: contentRectRef, contentRect } = useContentRect(
49-
extractSpectrumHTMLElement
49+
extractSpectrumHTMLElement<HTMLDivElement>
5050
);
5151

5252
return (

packages/components/src/spectrum/picker/Picker.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
Picker as SpectrumPicker,
33
SpectrumPickerProps,
44
} from '@adobe/react-spectrum';
5-
import type { DOMRef } from '@react-types/shared';
65
import cl from 'classnames';
76
import type { NormalizedItem } from '../utils';
87
import type { PickerProps } from './PickerProps';
@@ -19,19 +18,13 @@ export function Picker({
1918
UNSAFE_className,
2019
...props
2120
}: PickerProps): JSX.Element {
22-
const {
23-
defaultSelectedKey,
24-
disabledKeys,
25-
selectedKey,
26-
scrollRef,
27-
...pickerProps
28-
} = usePickerProps(props);
21+
const { defaultSelectedKey, disabledKeys, selectedKey, ...pickerProps } =
22+
usePickerProps<PickerProps, HTMLDivElement>(props);
2923

3024
return (
3125
<SpectrumPicker
3226
// eslint-disable-next-line react/jsx-props-no-spreading
3327
{...pickerProps}
34-
ref={scrollRef as DOMRef<HTMLDivElement>}
3528
UNSAFE_className={cl('dh-picker', UNSAFE_className)}
3629
// Type assertions are necessary here since Spectrum types don't account
3730
// for number and boolean key values even though they are valid runtime

packages/components/src/spectrum/picker/PickerNormalized.tsx

Lines changed: 7 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,34 @@
1-
import { useMemo } from 'react';
21
import { Picker as SpectrumPicker } from '@adobe/react-spectrum';
3-
import type { DOMRef } from '@react-types/shared';
42
import cl from 'classnames';
5-
import { EMPTY_FUNCTION } from '@deephaven/utils';
6-
import { Section } from '../shared';
73
import type { PickerNormalizedProps } from './PickerProps';
84

9-
import {
10-
getItemKey,
11-
isNormalizedSection,
12-
normalizeTooltipOptions,
13-
useRenderNormalizedItem,
14-
useStringifiedSelection,
15-
} from '../utils';
16-
import usePickerScrollOnOpen from './usePickerScrollOnOpen';
5+
import usePickerNormalizedProps from './usePickerNormalizedProps';
176

187
/**
198
* Picker that takes an array of `NormalizedItem` or `NormalizedSection` items
209
* as children and uses a render item function to render the items. This is
2110
* necessary to support windowed data.
2211
*/
2312
export function PickerNormalized({
24-
normalizedItems,
25-
tooltip = true,
26-
selectedKey,
27-
defaultSelectedKey,
28-
disabledKeys,
29-
showItemIcons,
3013
UNSAFE_className,
31-
getInitialScrollPosition,
32-
onChange,
33-
onOpenChange,
34-
onScroll = EMPTY_FUNCTION,
35-
onSelectionChange,
3614
...props
3715
}: PickerNormalizedProps): JSX.Element {
38-
const tooltipOptions = useMemo(
39-
() => normalizeTooltipOptions(tooltip),
40-
[tooltip]
41-
);
42-
43-
const renderNormalizedItem = useRenderNormalizedItem({
44-
itemIconSlot: 'icon',
45-
// Descriptions introduce variable item heights which throws off calculation
46-
// of initial scroll position and setting viewport on windowed data. For now
47-
// not going to implement description support in Picker.
48-
// https://github.com/deephaven/web-client-ui/issues/1958
49-
showItemDescriptions: false,
50-
showItemIcons,
51-
tooltipOptions,
52-
});
53-
54-
// Spectrum doesn't re-render if only the `renderNormalizedItems` function
55-
// changes, so we create a key from its dependencies that can be used to force
56-
// re-render.
57-
const forceRerenderKey = `${showItemIcons}-${tooltipOptions?.placement}`;
58-
59-
const { ref: scrollRef, onOpenChange: onOpenChangeInternal } =
60-
usePickerScrollOnOpen({
61-
getInitialScrollPosition,
62-
onScroll,
63-
onOpenChange,
64-
});
65-
66-
// Spectrum Picker treats keys as strings if the `key` prop is explicitly
67-
// set on `Item` elements. Since we do this in `renderItem`, we need to
68-
// map original key types to and from strings so that selection works.
69-
const {
70-
selectedStringKey,
71-
defaultSelectedStringKey,
72-
disabledStringKeys,
73-
onStringSelectionChange,
74-
} = useStringifiedSelection({
75-
normalizedItems,
76-
selectedKey,
77-
defaultSelectedKey,
78-
disabledKeys,
79-
onChange: onChange ?? onSelectionChange,
80-
});
16+
const { forceRerenderKey, ...pickerProps } = usePickerNormalizedProps<
17+
PickerNormalizedProps,
18+
HTMLDivElement
19+
>(props);
8120

8221
return (
8322
<SpectrumPicker
8423
// eslint-disable-next-line react/jsx-props-no-spreading
85-
{...props}
24+
{...pickerProps}
8625
key={forceRerenderKey}
87-
ref={scrollRef as DOMRef<HTMLDivElement>}
8826
UNSAFE_className={cl(
8927
'dh-picker',
9028
'dh-picker-normalized',
9129
UNSAFE_className
9230
)}
93-
items={normalizedItems}
94-
selectedKey={selectedStringKey}
95-
defaultSelectedKey={defaultSelectedStringKey}
96-
disabledKeys={disabledStringKeys}
97-
onSelectionChange={onStringSelectionChange}
98-
onOpenChange={onOpenChangeInternal}
99-
>
100-
{itemOrSection => {
101-
if (isNormalizedSection(itemOrSection)) {
102-
return (
103-
<Section
104-
key={getItemKey(itemOrSection)}
105-
title={itemOrSection.item?.title}
106-
items={itemOrSection.item?.items}
107-
>
108-
{renderNormalizedItem}
109-
</Section>
110-
);
111-
}
112-
113-
return renderNormalizedItem(itemOrSection);
114-
}}
115-
</SpectrumPicker>
31+
/>
11632
);
11733
}
11834

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export * from './Picker';
22
export * from './PickerNormalized';
33
export * from './PickerProps';
44
export * from './usePickerItemScale';
5+
export * from './usePickerNormalizedProps';
56
export * from './usePickerProps';
67
export * from './usePickerScrollOnOpen';

0 commit comments

Comments
 (0)