Skip to content

Commit

Permalink
feat: Picker - initial scroll position (#1942)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bmingles authored Apr 24, 2024
1 parent 3a1f92b commit 5f49761
Show file tree
Hide file tree
Showing 26 changed files with 1,152 additions and 169 deletions.
101 changes: 69 additions & 32 deletions packages/code-studio/src/styleguide/Pickers.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
import React, { useCallback, useState } from 'react';
import React, { cloneElement, useCallback, useState } from 'react';
import {
Flex,
Item,
Picker,
ItemKey,
Section,
Text,
PickerNormalized,
} from '@deephaven/components';
import { vsPerson } from '@deephaven/icons';
import { Icon } from '@adobe/react-spectrum';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils';
import { getPositionOfSelectedItem } from '@deephaven/react-hooks';
import { PICKER_ITEM_HEIGHTS, PICKER_TOP_OFFSET } from '@deephaven/utils';
import {
generateItemElements,
generateNormalizedItems,
sampleSectionIdAndClasses,
} from './utils';

// Generate enough items to require scrolling
const items = [...generateNormalizedItems(52)];
const itemElementsA = [...generateItemElements(0, 51)];
const itemElementsB = [...generateItemElements(52, 103)];
const itemElementsC = [...generateItemElements(104, 155)];
const itemElementsD = [...generateItemElements(156, 207)];
const itemElementsE = [...generateItemElements(208, 259)];

const mixedItemsWithIconsNoDescriptions = [
'String 1',
'String 2',
'String 3',
'',
'Some really long text that should get truncated',
444,
999,
true,
false,
...itemElementsA.map((itemEl, i) =>
i % 5 > 0
? itemEl
: cloneElement(itemEl, {
...itemEl.props,
children: [
<PersonIcon key={`icon-${itemEl.props.children}`} />,
<Text key={`label-${itemEl.props.children}`}>
{itemEl.props.children}
</Text>,
],
})
),
];

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

const getInitialScrollPosition = useCallback(
async () =>
getPositionOfSelectedItem({
keyedItems: items,
itemHeight: PICKER_ITEM_HEIGHTS.medium,
selectedKey,
topOffset: PICKER_TOP_OFFSET,
}),
[selectedKey]
);

const onChange = useCallback((key: ItemKey): void => {
setSelectedKey(key);
}, []);
Expand All @@ -37,69 +85,58 @@ export function Pickers(): JSX.Element {

<Flex gap={14}>
<Picker label="Single Child" tooltip={{ placement: 'bottom-end' }}>
<Item>Aaa</Item>
<Item textValue="Aaa">Aaa</Item>
</Picker>

<Picker label="Mixed Children Types" defaultSelectedKey={999} tooltip>
{/* eslint-disable react/jsx-curly-brace-presence */}
{'String 1'}
{'String 2'}
{'String 3'}
{''}
{'Some really long text that should get truncated'}
{/* eslint-enable react/jsx-curly-brace-presence */}
{444}
{999}
{true}
{false}
<Item>Item Aaa</Item>
<Item>Item Bbb</Item>
<Item textValue="Complex Ccc">
<PersonIcon />
<Text>Complex Ccc with text that should be truncated</Text>
</Item>
<Picker label="Mixed Children Types" defaultSelectedKey="999" tooltip>
{mixedItemsWithIconsNoDescriptions}
</Picker>

<Picker label="Sections" tooltip>
{/* eslint-disable react/jsx-curly-brace-presence */}
{'String 1'}
{'String 2'}
{'String 3'}
<Section title="Section A">
<Item>Item Aaa</Item>
<Item>Item Bbb</Item>
<Section title="Section">
<Item textValue="Item Aaa">Item Aaa</Item>
<Item textValue="Item Bbb">Item Bbb</Item>
<Item textValue="Complex Ccc">
<PersonIcon />
<Text>Complex Ccc</Text>
</Item>
</Section>
<Section key="Key B">
<Item>Item Ddd</Item>
<Item>Item Eee</Item>
<Item textValue="Item Ddd">Item Ddd</Item>
<Item textValue="Item Eee">Item Eee</Item>
<Item textValue="Complex Fff">
<PersonIcon />
<Text>Complex Fff</Text>
</Item>
<Item key="Ggg">
<Item textValue="Ggg">
<PersonIcon />
<Text>Label</Text>
<Text slot="description">Description</Text>
</Item>
<Item key="Hhh">
<Item textValue="Hhh">
<PersonIcon />
<Text>Label that causes overflow</Text>
<Text slot="description">Description that causes overflow</Text>
</Item>
</Section>
<Section title="Section A">{itemElementsA}</Section>
<Section title="Section B">{itemElementsB}</Section>
<Section key="Section C">{itemElementsC}</Section>
<Section key="Section D">{itemElementsD}</Section>
<Section title="Section E">{itemElementsE}</Section>
</Picker>

<Picker
<PickerNormalized
label="Controlled"
getInitialScrollPosition={getInitialScrollPosition}
normalizedItems={items}
selectedKey={selectedKey}
onChange={onChange}
>
{items}
</Picker>
/>
</Flex>
</div>
);
Expand Down
Loading

0 comments on commit 5f49761

Please sign in to comment.