Skip to content

Commit 5f49761

Browse files
authored
feat: Picker - initial scroll position (#1942)
NOTE: This PR depends on deephaven/deephaven-plugins#424 in order for element type checks to work for `Item`, `Text`, `Section`, etc. - Refactored non-jsapi Picker to support JSX children with minimal wrapping instead of having to normalize items (#1890 should do the same for ListView, and we should be able to delete some of the normalization code) - Updated scroll position logic to be able to traverse JSX elements - Disable initial scrolling when children contain descriptions or sections **Testing** This illustrates different configurations and shows how initial scroll behavior differs for Pickers with plain items, descriptions, or sections: ```python import deephaven.ui as ui from math import floor import datetime # Ticking table with initial row count of 200 that adds a row every second initial_row_count=1000 items_table = time_table("PT1S", start_time=datetime.datetime.now() - datetime.timedelta(seconds=initial_row_count)).update([ "Id=new Integer(i)", "Display=new String(`Display `+i)", ]) @ui.component def pickers(): value, set_value = ui.use_state('2SSS') def handle_change(v): print(v) set_value(v) on_change = ui.use_callback(handle_change, []) # Picker with text options text = ui.picker( label="Text", children=[ 'Text 1', 'Text 2', 'Text 3' ] ) # Picker with boolean options boolean = ui.picker( label="Boolean", children=[ True, False ] ) # Picker with numeric options numeric = ui.picker( label="Numeric", children=[ 10, 20, 30 ] ) ################ Icons ####################################### # Icons icons = ui.picker( label = "Icons", children = [ item_with_icon('Add', 'vsAdd'), item_with_icon('Remove', 'vsRemove'), ] ) # Icons (default selection) icons_default_selection = ui.picker( label = "Icons (default selection)", default_selected_key="3GGG", children = list(map( lambda args : item(args[1]) if args[0] % 7 > 0 else item_with_icon(args[1], 'vsAccount'), enumerate(generate_item_texts(0, 500)))) ) # Icons (controlled) icons_controlled = ui.picker( label = "Icons (controlled)", selected_key=value, on_change=on_change, children = list(map( lambda args : item(args[1]) if args[0] % 7 > 0 else item_with_icon(args[1], 'vsAccount'), enumerate(generate_item_texts(0, 500)))) ) ################ Descriptions ####################################### # Descriptions (default selection) descriptions = ui.picker( label = "Descriptions", children = list(map(lambda txt : item(txt, True), generate_item_texts(0, 500))) ) # Descriptions (default selection) descriptions_default_selection = ui.picker( label = "Descriptions (default selection)", default_selected_key="3GGG", children = list(map(lambda txt : item(txt, True), generate_item_texts(0, 500))) ) # Descriptions (controlled) descriptions_controlled = ui.picker( label = "Descriptions (controlled)", selected_key=value, on_change=on_change, children = list(map(lambda txt : item(txt, True), generate_item_texts(0, 500))) ) ################ Sections ####################################### # Sections sections = ui.picker( label = "Sections (default selection)", children = [ section(x, i * 10, i * 10 + 9) for i, x in enumerate(generate_item_texts(0, 19)) ] ) # Sections (default selection) sections_default_selection = ui.picker( label = "Sections (default selection)", default_selected_key = "3GGG", children = [ section(x, i * 10, i * 10 + 9) for i, x in enumerate(generate_item_texts(0, 19)) ] ) # Sections (controlled) sections_controlled = ui.picker( label = "Sections (controlled)", selected_key=value, on_change=on_change, children = [ section(x, i * 10, i * 10 + 9) for i, x in enumerate(generate_item_texts(0, 19)) ] ) ################ Tables ####################################### table_value, set_table_value = ui.use_state('Display 824') # Uncontrolled table with default selection table = ui.picker( items_table, key_column="Display", label_column="Display", label="Table", ) # Uncontrolled table with default selection table_default_selection = ui.picker( items_table, key_column="Display", label_column="Display", label="Table (default selection)", default_selected_key="Display 86", ) # Controlled table table_controlled = ui.picker( items_table, key_column="Display", label_column="Display", label="Table (controlled)", on_selection_change=set_table_value, selected_key=table_value, ) return ui.flex( direction="column", UNSAFE_style={"overflow":"scroll"}, children = [ ui.heading("Basic", margin=0), ui.flex( direction='row', children=[ text, boolean, numeric, ] ), ui.heading("Icons", margin=0), ui.flex( direction='row', children=[ icons, icons_default_selection, icons_controlled, ] ), ui.heading("Descriptions", margin=0), ui.flex( direction='row', children=[ descriptions, descriptions_default_selection, descriptions_controlled, ] ), ui.heading("Sections", margin=0), ui.flex( direction='row', children=[ sections, sections_default_selection, sections_controlled, ] ), ui.heading("Table", margin=0), ui.flex( direction='row', children=[ table, table_default_selection, table_controlled, ] ) ], ) pick = pickers() ################ Helpers ####################################### # Generate a list of unique item text for a start / stop range def generate_item_texts(start, stop): characters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") size = len(characters) return [str(floor((i + start) / size)) + (characters[x % size] * 3) for i, x in enumerate(range(start, stop))] @ui.component def item(txt, include_description=False): return ui.item( ui.text(txt, key="label"), ui.text("Description " + txt, key="description", slot="description"), # key=txt, text_value=txt ) if include_description else ui.item(txt, text_value=txt) @ui.component def item_with_icon(txt, icon): return ui.item( text_value=txt, children=[ ui.icon(icon), txt, ] ) @ui.component def section(txt, start, end): return ui.section( title = "Section " + txt, children = list(map(lambda txt : item(txt), generate_item_texts(start, end))) ) ``` resolves #1935
1 parent 3a1f92b commit 5f49761

26 files changed

Lines changed: 1152 additions & 169 deletions

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

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,56 @@
1-
import React, { useCallback, useState } from 'react';
1+
import React, { cloneElement, useCallback, useState } from 'react';
22
import {
33
Flex,
44
Item,
55
Picker,
66
ItemKey,
77
Section,
88
Text,
9+
PickerNormalized,
910
} from '@deephaven/components';
1011
import { vsPerson } from '@deephaven/icons';
1112
import { Icon } from '@adobe/react-spectrum';
1213
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13-
import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils';
14+
import { getPositionOfSelectedItem } from '@deephaven/react-hooks';
15+
import { PICKER_ITEM_HEIGHTS, PICKER_TOP_OFFSET } from '@deephaven/utils';
16+
import {
17+
generateItemElements,
18+
generateNormalizedItems,
19+
sampleSectionIdAndClasses,
20+
} from './utils';
1421

1522
// Generate enough items to require scrolling
1623
const items = [...generateNormalizedItems(52)];
24+
const itemElementsA = [...generateItemElements(0, 51)];
25+
const itemElementsB = [...generateItemElements(52, 103)];
26+
const itemElementsC = [...generateItemElements(104, 155)];
27+
const itemElementsD = [...generateItemElements(156, 207)];
28+
const itemElementsE = [...generateItemElements(208, 259)];
29+
30+
const mixedItemsWithIconsNoDescriptions = [
31+
'String 1',
32+
'String 2',
33+
'String 3',
34+
'',
35+
'Some really long text that should get truncated',
36+
444,
37+
999,
38+
true,
39+
false,
40+
...itemElementsA.map((itemEl, i) =>
41+
i % 5 > 0
42+
? itemEl
43+
: cloneElement(itemEl, {
44+
...itemEl.props,
45+
children: [
46+
<PersonIcon key={`icon-${itemEl.props.children}`} />,
47+
<Text key={`label-${itemEl.props.children}`}>
48+
{itemEl.props.children}
49+
</Text>,
50+
],
51+
})
52+
),
53+
];
1754

1855
function PersonIcon(): JSX.Element {
1956
return (
@@ -26,6 +63,17 @@ function PersonIcon(): JSX.Element {
2663
export function Pickers(): JSX.Element {
2764
const [selectedKey, setSelectedKey] = useState<ItemKey | null>(null);
2865

66+
const getInitialScrollPosition = useCallback(
67+
async () =>
68+
getPositionOfSelectedItem({
69+
keyedItems: items,
70+
itemHeight: PICKER_ITEM_HEIGHTS.medium,
71+
selectedKey,
72+
topOffset: PICKER_TOP_OFFSET,
73+
}),
74+
[selectedKey]
75+
);
76+
2977
const onChange = useCallback((key: ItemKey): void => {
3078
setSelectedKey(key);
3179
}, []);
@@ -37,69 +85,58 @@ export function Pickers(): JSX.Element {
3785

3886
<Flex gap={14}>
3987
<Picker label="Single Child" tooltip={{ placement: 'bottom-end' }}>
40-
<Item>Aaa</Item>
88+
<Item textValue="Aaa">Aaa</Item>
4189
</Picker>
4290

43-
<Picker label="Mixed Children Types" defaultSelectedKey={999} tooltip>
44-
{/* eslint-disable react/jsx-curly-brace-presence */}
45-
{'String 1'}
46-
{'String 2'}
47-
{'String 3'}
48-
{''}
49-
{'Some really long text that should get truncated'}
50-
{/* eslint-enable react/jsx-curly-brace-presence */}
51-
{444}
52-
{999}
53-
{true}
54-
{false}
55-
<Item>Item Aaa</Item>
56-
<Item>Item Bbb</Item>
57-
<Item textValue="Complex Ccc">
58-
<PersonIcon />
59-
<Text>Complex Ccc with text that should be truncated</Text>
60-
</Item>
91+
<Picker label="Mixed Children Types" defaultSelectedKey="999" tooltip>
92+
{mixedItemsWithIconsNoDescriptions}
6193
</Picker>
6294

6395
<Picker label="Sections" tooltip>
6496
{/* eslint-disable react/jsx-curly-brace-presence */}
6597
{'String 1'}
6698
{'String 2'}
6799
{'String 3'}
68-
<Section title="Section A">
69-
<Item>Item Aaa</Item>
70-
<Item>Item Bbb</Item>
100+
<Section title="Section">
101+
<Item textValue="Item Aaa">Item Aaa</Item>
102+
<Item textValue="Item Bbb">Item Bbb</Item>
71103
<Item textValue="Complex Ccc">
72104
<PersonIcon />
73105
<Text>Complex Ccc</Text>
74106
</Item>
75107
</Section>
76108
<Section key="Key B">
77-
<Item>Item Ddd</Item>
78-
<Item>Item Eee</Item>
109+
<Item textValue="Item Ddd">Item Ddd</Item>
110+
<Item textValue="Item Eee">Item Eee</Item>
79111
<Item textValue="Complex Fff">
80112
<PersonIcon />
81113
<Text>Complex Fff</Text>
82114
</Item>
83-
<Item key="Ggg">
115+
<Item textValue="Ggg">
84116
<PersonIcon />
85117
<Text>Label</Text>
86118
<Text slot="description">Description</Text>
87119
</Item>
88-
<Item key="Hhh">
120+
<Item textValue="Hhh">
89121
<PersonIcon />
90122
<Text>Label that causes overflow</Text>
91123
<Text slot="description">Description that causes overflow</Text>
92124
</Item>
93125
</Section>
126+
<Section title="Section A">{itemElementsA}</Section>
127+
<Section title="Section B">{itemElementsB}</Section>
128+
<Section key="Section C">{itemElementsC}</Section>
129+
<Section key="Section D">{itemElementsD}</Section>
130+
<Section title="Section E">{itemElementsE}</Section>
94131
</Picker>
95132

96-
<Picker
133+
<PickerNormalized
97134
label="Controlled"
135+
getInitialScrollPosition={getInitialScrollPosition}
136+
normalizedItems={items}
98137
selectedKey={selectedKey}
99138
onChange={onChange}
100-
>
101-
{items}
102-
</Picker>
139+
/>
103140
</Flex>
104141
</div>
105142
);

0 commit comments

Comments
 (0)