Skip to content

Commit

Permalink
feat: ComboBoxNormalized - windowed data component (#2072)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bmingles authored Jun 19, 2024
1 parent e6b55cf commit a30341a
Show file tree
Hide file tree
Showing 20 changed files with 287 additions and 161 deletions.
28 changes: 27 additions & 1 deletion packages/code-studio/src/styleguide/Pickers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PickerNormalized,
Checkbox,
ComboBox,
ComboBoxNormalized,
} from '@deephaven/components';
import { vsPerson } from '@deephaven/icons';
import { Icon } from '@adobe/react-spectrum';
Expand Down Expand Up @@ -61,10 +62,24 @@ function PersonIcon(): JSX.Element {
}

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

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

const [filteredItems, setFilteredItems] = useState(itemsWithIcons);

const onSearch = useCallback(
(searchText: string) =>
setFilteredItems(
searchText === ''
? itemsWithIcons
: itemsWithIcons.filter(
({ item }) => item?.textValue?.includes(searchText)
)
),
[]
);

const getInitialScrollPosition = useCallback(
async () =>
getPositionOfSelectedItem({
Expand Down Expand Up @@ -163,6 +178,17 @@ export function Pickers(): JSX.Element {
showItemIcons={showIcons}
onChange={onChange}
/>
<ComboBoxNormalized
label="ComboBox (Controlled)"
getInitialScrollPosition={getInitialScrollPosition}
normalizedItems={filteredItems}
selectedKey={selectedKey}
showItemIcons={showIcons}
onChange={onChange}
validationState={selectedKey == null ? 'invalid' : 'valid'}
errorMessage="Please select an item."
onInputChange={onSearch}
/>
</Flex>
</Flex>
</SampleSection>
Expand Down
5 changes: 2 additions & 3 deletions packages/components/src/SearchableCombobox.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Key, useCallback } from 'react';
import { ComboBox, Item, SpectrumComboBoxProps } from '@adobe/react-spectrum';
import type { FocusableRef } from '@react-types/shared';
import type { ReactSpectrumComponent } from '@deephaven/react-hooks';
import type { DOMRefValue, FocusableRef } from '@react-types/shared';
import TextWithTooltip from './TextWithTooltip';

export interface SearchableComboboxProps<TItem, TKey extends Key>
Expand All @@ -12,7 +11,7 @@ export interface SearchableComboboxProps<TItem, TKey extends Key>
> {
getItemDisplayText: (item: TItem | null | undefined) => string | null;
getKey: (item: TItem | null | undefined) => TKey | null;
scrollRef: React.RefObject<ReactSpectrumComponent<HTMLElement>>;
scrollRef: React.RefObject<DOMRefValue>;
onSelectionChange: (key: TKey | null) => void;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/spectrum/comboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ export function ComboBox({
const {
defaultSelectedKey,
disabledKeys,
ref,
selectedKey,
scrollRef,
...comboBoxProps
} = usePickerProps(props);

return (
<SpectrumComboBox
// eslint-disable-next-line react/jsx-props-no-spreading
{...comboBoxProps}
ref={scrollRef as FocusableRef<HTMLElement>}
UNSAFE_className={cl('dh-combobox', UNSAFE_className)}
ref={ref as FocusableRef<HTMLElement>}
// Type assertions are necessary here since Spectrum types don't account
// for number and boolean key values even though they are valid runtime
// values.
Expand Down
39 changes: 39 additions & 0 deletions packages/components/src/spectrum/comboBox/ComboBoxNormalized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ComboBox as SpectrumComboBox } from '@adobe/react-spectrum';
import { FocusableRef } from '@react-types/shared';
import cl from 'classnames';
import { PickerNormalizedPropsT, usePickerNormalizedProps } from '../picker';
import { ComboBoxProps } from './ComboBox';

export type ComboBoxNormalizedProps = PickerNormalizedPropsT<ComboBoxProps>;

/**
* ComboBox that takes an array of `NormalizedItem` or `NormalizedSection` items
* as children and uses a render item function to render the items. `NormalizedItem`
* and `NormalizedSection` datums always provide a `key` property but have an
* optional `item` property that can be lazy loaded. This is necessary to support
* windowed data since we need a representative key for every item in the
* collection.
*/
export function ComboBoxNormalized({
UNSAFE_className,
...props
}: ComboBoxNormalizedProps): JSX.Element {
const { forceRerenderKey, ref, ...pickerProps } =
usePickerNormalizedProps<ComboBoxNormalizedProps>(props);

return (
<SpectrumComboBox
// eslint-disable-next-line react/jsx-props-no-spreading
{...pickerProps}
key={forceRerenderKey}
ref={ref as FocusableRef<HTMLElement>}
UNSAFE_className={cl(
'dh-combobox',
'dh-combobox-normalized',
UNSAFE_className
)}
/>
);
}

export default ComboBoxNormalized;
1 change: 1 addition & 0 deletions packages/components/src/spectrum/comboBox/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './ComboBox';
export * from './ComboBoxNormalized';
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function ListViewWrapper<T>(
// of the parent container and only render the ListView when it has a non-zero
// height. See https://github.com/adobe/react-spectrum/issues/6213
const { ref: contentRectRef, contentRect } = useContentRect(
extractSpectrumHTMLElement
extractSpectrumHTMLElement<HTMLDivElement>
);

return (
Expand Down
11 changes: 2 additions & 9 deletions packages/components/src/spectrum/picker/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Picker as SpectrumPicker,
SpectrumPickerProps,
} from '@adobe/react-spectrum';
import type { DOMRef } from '@react-types/shared';
import cl from 'classnames';
import type { NormalizedItem } from '../utils';
import type { PickerProps } from './PickerProps';
Expand All @@ -19,19 +18,13 @@ export function Picker({
UNSAFE_className,
...props
}: PickerProps): JSX.Element {
const {
defaultSelectedKey,
disabledKeys,
selectedKey,
scrollRef,
...pickerProps
} = usePickerProps(props);
const { defaultSelectedKey, disabledKeys, selectedKey, ...pickerProps } =
usePickerProps<PickerProps, HTMLDivElement>(props);

return (
<SpectrumPicker
// eslint-disable-next-line react/jsx-props-no-spreading
{...pickerProps}
ref={scrollRef as DOMRef<HTMLDivElement>}
UNSAFE_className={cl('dh-picker', UNSAFE_className)}
// Type assertions are necessary here since Spectrum types don't account
// for number and boolean key values even though they are valid runtime
Expand Down
98 changes: 7 additions & 91 deletions packages/components/src/spectrum/picker/PickerNormalized.tsx
Original file line number Diff line number Diff line change
@@ -1,118 +1,34 @@
import { useMemo } from 'react';
import { Picker as SpectrumPicker } from '@adobe/react-spectrum';
import type { DOMRef } from '@react-types/shared';
import cl from 'classnames';
import { EMPTY_FUNCTION } from '@deephaven/utils';
import { Section } from '../shared';
import type { PickerNormalizedProps } from './PickerProps';

import {
getItemKey,
isNormalizedSection,
normalizeTooltipOptions,
useRenderNormalizedItem,
useStringifiedSelection,
} from '../utils';
import usePickerScrollOnOpen from './usePickerScrollOnOpen';
import usePickerNormalizedProps from './usePickerNormalizedProps';

/**
* Picker that takes an array of `NormalizedItem` or `NormalizedSection` items
* as children and uses a render item function to render the items. This is
* necessary to support windowed data.
*/
export function PickerNormalized({
normalizedItems,
tooltip = true,
selectedKey,
defaultSelectedKey,
disabledKeys,
showItemIcons,
UNSAFE_className,
getInitialScrollPosition,
onChange,
onOpenChange,
onScroll = EMPTY_FUNCTION,
onSelectionChange,
...props
}: PickerNormalizedProps): JSX.Element {
const tooltipOptions = useMemo(
() => normalizeTooltipOptions(tooltip),
[tooltip]
);

const renderNormalizedItem = useRenderNormalizedItem({
itemIconSlot: 'icon',
// Descriptions introduce variable item heights which throws off calculation
// of initial scroll position and setting viewport on windowed data. For now
// not going to implement description support in Picker.
// https://github.com/deephaven/web-client-ui/issues/1958
showItemDescriptions: false,
showItemIcons,
tooltipOptions,
});

// Spectrum doesn't re-render if only the `renderNormalizedItems` function
// changes, so we create a key from its dependencies that can be used to force
// re-render.
const forceRerenderKey = `${showItemIcons}-${tooltipOptions?.placement}`;

const { ref: scrollRef, onOpenChange: onOpenChangeInternal } =
usePickerScrollOnOpen({
getInitialScrollPosition,
onScroll,
onOpenChange,
});

// Spectrum Picker treats keys as strings if the `key` prop is explicitly
// set on `Item` elements. Since we do this in `renderItem`, we need to
// map original key types to and from strings so that selection works.
const {
selectedStringKey,
defaultSelectedStringKey,
disabledStringKeys,
onStringSelectionChange,
} = useStringifiedSelection({
normalizedItems,
selectedKey,
defaultSelectedKey,
disabledKeys,
onChange: onChange ?? onSelectionChange,
});
const { forceRerenderKey, ...pickerProps } = usePickerNormalizedProps<
PickerNormalizedProps,
HTMLDivElement
>(props);

return (
<SpectrumPicker
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
{...pickerProps}
key={forceRerenderKey}
ref={scrollRef as DOMRef<HTMLDivElement>}
UNSAFE_className={cl(
'dh-picker',
'dh-picker-normalized',
UNSAFE_className
)}
items={normalizedItems}
selectedKey={selectedStringKey}
defaultSelectedKey={defaultSelectedStringKey}
disabledKeys={disabledStringKeys}
onSelectionChange={onStringSelectionChange}
onOpenChange={onOpenChangeInternal}
>
{itemOrSection => {
if (isNormalizedSection(itemOrSection)) {
return (
<Section
key={getItemKey(itemOrSection)}
title={itemOrSection.item?.title}
items={itemOrSection.item?.items}
>
{renderNormalizedItem}
</Section>
);
}

return renderNormalizedItem(itemOrSection);
}}
</SpectrumPicker>
/>
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/components/src/spectrum/picker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './Picker';
export * from './PickerNormalized';
export * from './PickerProps';
export * from './usePickerItemScale';
export * from './usePickerNormalizedProps';
export * from './usePickerProps';
export * from './usePickerScrollOnOpen';
Loading

0 comments on commit a30341a

Please sign in to comment.