Skip to content

Commit bfbf7b1

Browse files
authored
feat: Picker - Table support for key + label columns (#1876)
Initial table support for Picker - key and value column support - Scroll to selected item when popover opens - Viewport data now supports re-use of existing item objects via `reuseItemsOnTableResize` flag ## Testing ### Scroll to item fix for non-table data (this should work without any changes to plugins repo) ```python import deephaven.ui as ui from deephaven.ui import use_state items = list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") @ui.component def picker(): value, set_value = use_state("M") # Picker for selecting values pick = ui.picker( label="Text", on_selection_change=set_value, selected_key=value, children=items ) # Show current selection in a ui.text component text = ui.text("Selection: " + value) # Display picker and output in a flex column return ui.flex( pick, text, direction="column", margin=10, gap=10, ) p = picker() ``` ### Table support There is a draft PR in the plugins repo that enables the table support on the plugins side of things (note that it currently will have type errors but should run) deephaven/deephaven-plugins#382 Here's an example that demonstrates a large number of items that also tick ```python import deephaven.ui as ui from deephaven.ui import use_state from deephaven import time_table import datetime mylist = ["mars", "pluto", "saturn"] simple_ticking = time_table("PT2S", start_time=datetime.datetime.now() - datetime.timedelta(seconds=2000)).update([ 'Id=(new Integer(i * 100))', "Planet=mylist[ (int) (3 * Math.random()) % 3 ]", ]) @ui.component def picker(): value, set_value = use_state(13800) print("Test", value) # Picker for selecting values pick = ui.picker( simple_ticking, key_column="Id", label_column="Planet", label="Text", on_selection_change=set_value, selected_key=value, ) # Show current selection in a ui.text component text = ui.text("Selection: " + str(value)) # Display picker and output in a flex column return ui.flex( pick, text, direction="column", margin=10, gap=10, ) picker_table = picker() ``` resolves #1858
1 parent 96298f6 commit bfbf7b1

34 files changed

Lines changed: 1506 additions & 333 deletions

__mocks__/dh-core.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,15 @@ class SourceType {
19891989
static Z = 'Z';
19901990
}
19911991

1992+
class ValueType {
1993+
static BOOLEAN = 'BOOLEAN';
1994+
static DATETIME = 'DATETIME';
1995+
static DOUBLE = 'DOUBLE';
1996+
static LONG = 'LONG';
1997+
static NUMBER = 'NUMBER';
1998+
static STRING = 'STRING';
1999+
}
2000+
19922001
const dh = {
19932002
FilterCondition: FilterCondition,
19942003
FilterValue: FilterValue,
@@ -2034,6 +2043,7 @@ const dh = {
20342043
DayOfWeek,
20352044
},
20362045
DateWrapper: DateWrapper,
2046+
ValueType,
20372047
ViewportData,
20382048
VariableType,
20392049
storage: {

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import React from 'react';
2-
import { Item, Picker, Section } from '@deephaven/components';
1+
import React, { useCallback, useState } from 'react';
2+
import { Picker, PickerItemKey, Section } from '@deephaven/components';
33
import { vsPerson } from '@deephaven/icons';
4-
import { Flex, Icon, Text } from '@adobe/react-spectrum';
4+
import { Flex, Icon, Item, Text } from '@adobe/react-spectrum';
55
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
66
import { sampleSectionIdAndClasses } from './utils';
77

8+
// Generate enough items to require scrolling
9+
const items = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
10+
.split('')
11+
.map((key, i) => ({
12+
key,
13+
item: { key: (i + 1) * 100, content: `${key}${key}${key}` },
14+
}));
15+
816
function PersonIcon(): JSX.Element {
917
return (
1018
<Icon>
@@ -14,6 +22,12 @@ function PersonIcon(): JSX.Element {
1422
}
1523

1624
export function Pickers(): JSX.Element {
25+
const [selectedKey, setSelectedKey] = useState<PickerItemKey>();
26+
27+
const onChange = useCallback((key: PickerItemKey): void => {
28+
setSelectedKey(key);
29+
}, []);
30+
1731
return (
1832
// eslint-disable-next-line react/jsx-props-no-spreading
1933
<div {...sampleSectionIdAndClasses('pickers')}>
@@ -24,7 +38,7 @@ export function Pickers(): JSX.Element {
2438
<Item>Aaa</Item>
2539
</Picker>
2640

27-
<Picker label="Mixed Children Types" tooltip>
41+
<Picker label="Mixed Children Types" defaultSelectedKey={999} tooltip>
2842
{/* eslint-disable react/jsx-curly-brace-presence */}
2943
{'String 1'}
3044
{'String 2'}
@@ -76,6 +90,14 @@ export function Pickers(): JSX.Element {
7690
</Item>
7791
</Section>
7892
</Picker>
93+
94+
<Picker
95+
label="Controlled"
96+
selectedKey={selectedKey}
97+
onChange={onChange}
98+
>
99+
{items}
100+
</Picker>
79101
</Flex>
80102
</div>
81103
);

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

Lines changed: 122 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,57 @@
11
import { Key, ReactNode, useCallback, useMemo } from 'react';
2+
import { DOMRef } from '@react-types/shared';
23
import { Flex, Picker as SpectrumPicker, Text } from '@adobe/react-spectrum';
3-
import { isElementOfType } from '@deephaven/react-hooks';
4+
import {
5+
getPositionOfSelectedItem,
6+
findSpectrumPickerScrollArea,
7+
isElementOfType,
8+
usePopoverOnScrollRef,
9+
} from '@deephaven/react-hooks';
10+
import {
11+
EMPTY_FUNCTION,
12+
PICKER_ITEM_HEIGHT,
13+
PICKER_TOP_OFFSET,
14+
} from '@deephaven/utils';
415
import cl from 'classnames';
516
import { Tooltip } from '../../popper';
617
import {
18+
isNormalizedPickerSection,
719
NormalizedSpectrumPickerProps,
820
normalizePickerItemList,
921
normalizeTooltipOptions,
22+
NormalizedPickerItem,
1023
PickerItemOrSection,
11-
PickerItemKey,
1224
TooltipOptions,
13-
NormalizedPickerItem,
14-
isNormalizedPickerSection,
25+
PickerItemKey,
26+
getPickerItemKey,
1527
} from './PickerUtils';
1628
import { PickerItemContent } from './PickerItemContent';
1729
import { Item, Section } from '../shared';
1830

1931
export type PickerProps = {
20-
children: PickerItemOrSection | PickerItemOrSection[];
32+
children:
33+
| PickerItemOrSection
34+
| PickerItemOrSection[]
35+
| NormalizedPickerItem[];
2136
/** Can be set to true or a TooltipOptions to enable item tooltips */
2237
tooltip?: boolean | TooltipOptions;
2338
/** The currently selected key in the collection (controlled). */
2439
selectedKey?: PickerItemKey | null;
2540
/** The initial selected key in the collection (uncontrolled). */
2641
defaultSelectedKey?: PickerItemKey;
42+
/** Function to retrieve initial scroll position when opening the picker */
43+
getInitialScrollPosition?: () => Promise<number | null>;
2744
/**
2845
* Handler that is called when the selection change.
2946
* Note that under the hood, this is just an alias for Spectrum's
3047
* `onSelectionChange`. We are renaming for better consistency with other
3148
* components.
3249
*/
3350
onChange?: (key: PickerItemKey) => void;
51+
52+
/** Handler that is called when the picker is scrolled. */
53+
onScroll?: (event: Event) => void;
54+
3455
/**
3556
* Handler that is called when the selection changes.
3657
* @deprecated Use `onChange` instead
@@ -82,7 +103,10 @@ export function Picker({
82103
tooltip = true,
83104
defaultSelectedKey,
84105
selectedKey,
106+
getInitialScrollPosition,
85107
onChange,
108+
onOpenChange,
109+
onScroll = EMPTY_FUNCTION,
86110
onSelectionChange,
87111
// eslint-disable-next-line camelcase
88112
UNSAFE_className,
@@ -99,52 +123,116 @@ export function Picker({
99123
);
100124

101125
const renderItem = useCallback(
102-
({ key, content, textValue }: NormalizedPickerItem) => (
103-
// The `textValue` prop gets used to provide the content of `<option>`
104-
// elements that back the Spectrum Picker. These are not visible in the UI,
105-
// but are used for accessibility purposes, so we set to an arbitrary
106-
// 'Empty' value so that they are not empty strings.
107-
<Item
108-
key={key as Key}
109-
textValue={textValue === '' || textValue == null ? 'Empty' : textValue}
110-
>
111-
<PickerItemContent>{content}</PickerItemContent>
112-
{tooltipOptions == null || content === '' ? null : (
113-
<Tooltip options={tooltipOptions}>
114-
{createTooltipContent(content)}
115-
</Tooltip>
116-
)}
117-
</Item>
118-
),
126+
(normalizedItem: NormalizedPickerItem) => {
127+
const key = getPickerItemKey(normalizedItem);
128+
const content = normalizedItem.item?.content ?? '';
129+
const textValue = normalizedItem.item?.textValue ?? '';
130+
131+
return (
132+
<Item
133+
// Note that setting the `key` prop explicitly on `Item` elements
134+
// causes the picker to expect `selectedKey` and `defaultSelectedKey`
135+
// to be strings. It also passes the stringified value of the key to
136+
// `onSelectionChange` handlers` regardless of the actual type of the
137+
// key. We can't really get around setting in order to support Windowed
138+
// data, so we'll need to do some manual conversion of keys to strings
139+
// in other places of this component.
140+
key={key as Key}
141+
// The `textValue` prop gets used to provide the content of `<option>`
142+
// elements that back the Spectrum Picker. These are not visible in the UI,
143+
// but are used for accessibility purposes, so we set to an arbitrary
144+
// 'Empty' value so that they are not empty strings.
145+
textValue={textValue === '' ? 'Empty' : textValue}
146+
>
147+
<>
148+
<PickerItemContent>{content}</PickerItemContent>
149+
{tooltipOptions == null || content === '' ? null : (
150+
<Tooltip options={tooltipOptions}>
151+
{createTooltipContent(content)}
152+
</Tooltip>
153+
)}
154+
</>
155+
</Item>
156+
);
157+
},
119158
[tooltipOptions]
120159
);
121160

161+
const getInitialScrollPositionInternal = useCallback(
162+
() =>
163+
getInitialScrollPosition == null
164+
? getPositionOfSelectedItem({
165+
keyedItems: normalizedItems,
166+
// TODO: #1890 & deephaven-plugins#371 add support for sections and
167+
// items with descriptions since they impact the height calculations
168+
itemHeight: PICKER_ITEM_HEIGHT,
169+
selectedKey,
170+
topOffset: PICKER_TOP_OFFSET,
171+
})
172+
: getInitialScrollPosition(),
173+
[getInitialScrollPosition, normalizedItems, selectedKey]
174+
);
175+
176+
const { ref: scrollRef, onOpenChange: popoverOnOpenChange } =
177+
usePopoverOnScrollRef(
178+
findSpectrumPickerScrollArea,
179+
onScroll,
180+
getInitialScrollPositionInternal
181+
);
182+
183+
const onOpenChangeInternal = useCallback(
184+
(isOpen: boolean): void => {
185+
// Attach scroll event handling
186+
popoverOnOpenChange(isOpen);
187+
188+
onOpenChange?.(isOpen);
189+
},
190+
[onOpenChange, popoverOnOpenChange]
191+
);
192+
193+
const onSelectionChangeInternal = useCallback(
194+
(key: PickerItemKey): void => {
195+
// The `key` arg will always be a string due to us setting the `Item` key
196+
// prop in `renderItem`. We need to find the matching item to determine
197+
// the actual key.
198+
const selectedItem = normalizedItems.find(
199+
item => String(getPickerItemKey(item)) === key
200+
);
201+
202+
const actualKey = getPickerItemKey(selectedItem) ?? key;
203+
204+
(onChange ?? onSelectionChange)?.(actualKey);
205+
},
206+
[normalizedItems, onChange, onSelectionChange]
207+
);
208+
122209
return (
123210
<SpectrumPicker
124211
// eslint-disable-next-line react/jsx-props-no-spreading
125212
{...spectrumPickerProps}
213+
// The `ref` prop type defined by React Spectrum is incorrect here
214+
ref={scrollRef as unknown as DOMRef<HTMLDivElement>}
215+
onOpenChange={onOpenChangeInternal}
126216
UNSAFE_className={cl('dh-picker', UNSAFE_className)}
127217
items={normalizedItems}
128-
// Type assertions are necessary for `selectedKey`, `defaultSelectedKey`,
129-
// and `onSelectionChange` due to Spectrum types not accounting for
130-
// `boolean` keys
131-
selectedKey={selectedKey as NormalizedSpectrumPickerProps['selectedKey']}
132-
defaultSelectedKey={
133-
defaultSelectedKey as NormalizedSpectrumPickerProps['defaultSelectedKey']
134-
}
218+
// Spectrum Picker treats keys as strings if the `key` prop is explicitly
219+
// set on `Item` elements. Since we do this in `renderItem`, we need to
220+
// ensure that `selectedKey` and `defaultSelectedKey` are strings in order
221+
// for selection to work.
222+
selectedKey={selectedKey?.toString()}
223+
defaultSelectedKey={defaultSelectedKey?.toString()}
135224
// `onChange` is just an alias for `onSelectionChange`
136225
onSelectionChange={
137-
(onChange ??
138-
onSelectionChange) as NormalizedSpectrumPickerProps['onSelectionChange']
226+
onSelectionChangeInternal as NormalizedSpectrumPickerProps['onSelectionChange']
139227
}
140228
>
141229
{itemOrSection => {
142230
if (isNormalizedPickerSection(itemOrSection)) {
143231
return (
144232
<Section
145-
key={itemOrSection.key}
146-
title={itemOrSection.title}
147-
items={itemOrSection.items}
233+
key={getPickerItemKey(itemOrSection)}
234+
title={itemOrSection.item?.title}
235+
items={itemOrSection.item?.items}
148236
>
149237
{renderItem}
150238
</Section>

0 commit comments

Comments
 (0)