Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ComboBoxNormalized - windowed data component #2072

Merged
merged 8 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading