Skip to content

Commit

Permalink
feat: ListView actions (#1968)
Browse files Browse the repository at this point in the history
* Wrapper components for `ActionGroup` and `ActionMenu` to support
primitive items
* `ListActionGroup` and `ListActionMenu` components to support providing
actions prop to `ListView`
* `ListView` `actions` prop support

The branch in [this
PR](deephaven/deephaven-plugins#448) can be used
to see this in action. I also published an alpha this branch
([0.77.1-alpha-listview-actions.4](https://www.npmjs.com/package/@deephaven/components/v/0.77.1-alpha-listview-actions.4))
to make types work in plugins

### Example of standalone `ui.action_group` and `ui.action_menu`
```python
from deephaven import ui


@ui.component
def action_group():
    action, on_action = ui.use_state("")
    selected_keys, set_selected_keys = ui.use_state(['Aaa'])

    selection_text = ui.text("Selection: " + ", ".join(map(str, selected_keys)), grid_column="span 2")

    return 'Action Group', (" - " if action else "") + action, ui.action_group(
        ui.item(
            ui.icon('vsAccount'),
            'Aaa',
            text_value="Aaa"
        ),
        'Bbb',
        'Ccc',
        selection_mode="multiple",
        selected_keys=selected_keys,
        on_action=on_action,
        on_change=set_selected_keys,
    ), selection_text,
    

ag = action_group()


@ui.component
def action_menu():
    action, on_action = ui.use_state("")

    return 'Action Menu', (" - " if action else "") + action, ui.action_menu(
        ui.item(
            ui.icon('vsAccount'),
            'Aaa',
            text_value="Aaa"
        ),
        'Bbb',
        'Ccc',
        on_action=on_action,
        align_self="start"
    )
    

am = action_menu()
```

### Example showing actions in `ui.list_view` with different densities

```python
import deephaven.ui as ui
from deephaven import time_table
import datetime

initial_row_count=2000
icon_names = ['vsAccount']

columns = [
    "Id=new Integer(i)",
    "Display=new String(`Display `+i)",
    "Description=new String(`Description `+i)",
    "Icon=(String) icon_names[0]"
]
# Tables with initial row count of 200 that adds a row every second
column_types_ticking = time_table("PT1S", start_time=datetime.datetime.now() - datetime.timedelta(seconds=initial_row_count)).update([
    columns
])
column_types = empty_table(initial_row_count).update(columns)


#### Component definitions ####

@ui.component
def labeled_lv(label, *args, **kwargs):
    return ui.flex(
        ui.text(label),
        ui.list_view(
            *args,
            **kwargs
        ),
        direction="column",
        flex=1,
        min_width=0,
    )

@ui.component
def ui_list_view_table(data, density):
    value, set_value = ui.use_state([2, 4, 5])

    # Action Groups

    ag_action, set_ag_action = ui.use_state(['', ''])
    on_ag_action=ui.use_callback(lambda a,i : set_ag_action([a,str(i)]), [])

    lv_actions = labeled_lv(
        "Actions (text only)",
        data,
        density=density,
        max_height=5000,
        key_column="Id",
        label_column="Display",
        icon_column="Icon",
        aria_label="List View",
        on_change=set_value,
        selected_keys=value,
        actions=ui.list_action_group(
            'Edit',
            'Delete',
            on_action=on_ag_action,
        ),
    )

    lv_actions_icon = labeled_lv(
        "Actions (icon only)",
        data,
        density=density,
        max_height=5000,
        key_column="Id",
        label_column="Display",
        icon_column="Icon",
        aria_label="List View",
        on_change=set_value,
        selected_keys=value,
        actions=ui.list_action_group(
            ui.item(
                ui.icon('vsEdit'),
                ui.text('Edit'),
                key='Edit',
            ),
            ui.item(
                ui.icon('vsTrash'),
                ui.text('Delete'),
                key='Delete'
            ),
            max_width=80,
            button_label_behavior="collapse",
            overflow_mode="collapse",
            on_action=on_ag_action,
        ),
    )
    
    action_group_text = ui.text("Action: " + ag_action[0] + ", Item: " + ag_action[1])

    # Action Menus

    am_action, set_am_action = ui.use_state(['', ''])
    on_am_action=ui.use_callback(lambda a,i : set_am_action([a,str(i)]), [])

    lv_action_menu = labeled_lv(
        "Action Menu (text only)",
        data,
        density=density,
        max_height=5000,
        key_column="Id",
        label_column="Display",
        icon_column="Icon",
        aria_label="List View",
        selected_keys=value,
        on_change=set_value,
        actions=ui.list_action_menu(
            'Edit',
            'Delete',
            on_action=on_am_action,
        ),
    )

    lv_action_menu2 = labeled_lv(
        "Action Menu (icons)",
        data,
        density=density,
        max_height=5000,
        key_column="Id",
        label_column="Display",
        icon_column="Icon",
        aria_label="List View",
        selected_keys=value,
        on_change=set_value,
        actions=ui.list_action_menu(
             ui.item(
                ui.icon('vsEdit'),
                ui.text('Edit'),
                key='Edit',
                text_value="Edit"
            ),
            ui.item(
                ui.icon('vsTrash'),
                ui.text('Delete'),
                key='Delete',
                text_value="Delete"
            ),
            on_action=on_am_action,
        ),
    )

    action_menu_text = ui.text("Action: " + am_action[0] + ", Item: " + am_action[1])

    return ui.flex(
        ui.flex(
            lv_actions,
            lv_actions_icon,
            direction="row",
        ),
        action_group_text,
        ui.flex(
            lv_action_menu,
            lv_action_menu2,
            direction="row"
        ),
        action_menu_text,
        direction="column",
    )

@ui.component
def examples(data):
    density, set_density = ui.use_state(["COMPACT"])

    return 'Density', ui.action_group(
        'COMPACT', 
        'REGULAR', 
        'SPACIOUS',
        selected_keys=density,
        selection_mode="SINGLE",
        on_change=set_density
    ), ui_list_view_table(data, density[0])

lv_table = examples(column_types)
# lv_table = examples(column_types_ticking)
```
  • Loading branch information
bmingles authored May 15, 2024
1 parent 2246a4a commit 8e325ec
Show file tree
Hide file tree
Showing 20 changed files with 418 additions and 77 deletions.
34 changes: 32 additions & 2 deletions packages/code-studio/src/styleguide/ListViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
RadioGroup,
RadioItem,
useSpectrumThemeProvider,
ListActionGroup,
} from '@deephaven/components';
import { vsAccount, vsPerson } from '@deephaven/icons';
import { vsAccount, vsEdit, vsPerson, vsTrash } from '@deephaven/icons';
import { LIST_VIEW_ROW_HEIGHTS } from '@deephaven/utils';
import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils';

Expand Down Expand Up @@ -90,6 +91,13 @@ export function ListViews(): JSX.Element {
);

const [showIcons, setShowIcons] = useState(true);
const [lastActionKey, setLastActionKey] = useState<ItemKey>('');
const [lastActionItemKey, setLastActionItemKey] = useState<ItemKey>('');

const onAction = useCallback((actionKey: ItemKey, itemKey: ItemKey): void => {
setLastActionKey(actionKey);
setLastActionItemKey(itemKey);
}, []);

const onChange = useCallback((keys: 'all' | Iterable<ItemKey>): void => {
setSelectedKeys(keys);
Expand Down Expand Up @@ -211,7 +219,7 @@ export function ListViews(): JSX.Element {
</Checkbox>
</Flex>

<LabeledFlexContainer label="Controlled">
<LabeledFlexContainer label="Controlled" gridColumn="span 2">
<ListViewNormalized
aria-label="Controlled"
density={density}
Expand All @@ -220,7 +228,29 @@ export function ListViews(): JSX.Element {
selectedKeys={selectedKeys}
showItemIcons={showIcons}
onChange={onChange}
actions={
<ListActionGroup
overflowMode="collapse"
buttonLabelBehavior="collapse"
maxWidth={80}
onAction={onAction}
>
<Item key="Edit">
<Icon>
<FontAwesomeIcon icon={vsEdit} />
</Icon>
<Text>Edit</Text>
</Item>
<Item key="Delete">
<Icon>
<FontAwesomeIcon icon={vsTrash} />
</Icon>
<Text>Delete</Text>
</Item>
</ListActionGroup>
}
/>
{lastActionKey} {lastActionItemKey}
</LabeledFlexContainer>
</Grid>
</div>
Expand Down
61 changes: 61 additions & 0 deletions packages/components/src/spectrum/ActionGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useMemo } from 'react';
import {
ActionGroup as SpectrumActionGroup,
SpectrumActionGroupProps,
} from '@adobe/react-spectrum';
import cl from 'classnames';
import { ItemsOrPrimitiveChildren } from './shared';
import { MultipleItemSelectionProps, wrapItemChildren } from './utils';

export type ActionGroupProps<T> = Omit<
SpectrumActionGroupProps<T>,
| 'children'
| 'selectedKeys'
| 'defaultSelectedKeys'
| 'disabledKeys'
| 'onSelectionChange'
> &
MultipleItemSelectionProps & {
children: ItemsOrPrimitiveChildren<T>;
};

/**
* Augmented version of the Spectrum ActionGroup component that supports
* primitive item children.
*/
export function ActionGroup<T>({
defaultSelectedKeys,
disabledKeys,
children,
selectedKeys,
UNSAFE_className,
onChange,
onSelectionChange,
...props
}: ActionGroupProps<T>): JSX.Element {
const wrappedChildren = useMemo(
() =>
typeof children === 'function'
? children
: wrapItemChildren(children, null),
[children]
);

return (
<SpectrumActionGroup
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
UNSAFE_className={cl('dh-action-group', UNSAFE_className)}
defaultSelectedKeys={
defaultSelectedKeys as SpectrumActionGroupProps<T>['defaultSelectedKeys']
}
disabledKeys={disabledKeys as SpectrumActionGroupProps<T>['disabledKeys']}
selectedKeys={selectedKeys as SpectrumActionGroupProps<T>['selectedKeys']}
onSelectionChange={onChange ?? onSelectionChange}
>
{wrappedChildren}
</SpectrumActionGroup>
);
}

export default ActionGroup;
48 changes: 48 additions & 0 deletions packages/components/src/spectrum/ActionMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMemo } from 'react';
import {
ActionMenu as SpectrumActionMenu,
SpectrumActionMenuProps,
} from '@adobe/react-spectrum';
import cl from 'classnames';
import { ItemsOrPrimitiveChildren } from './shared';
import { ItemKey, wrapItemChildren } from './utils';

export type ActionMenuProps<T> = Omit<
SpectrumActionMenuProps<T>,
'children' | 'disabledKeys'
> & {
disabledKeys?: Iterable<ItemKey>;
children: ItemsOrPrimitiveChildren<T>;
};

/**
* Augmented version of the Spectrum ActionMenu component that supports
* primitive item children.
*/
export function ActionMenu<T>({
disabledKeys,
children,
UNSAFE_className,
...props
}: ActionMenuProps<T>): JSX.Element {
const wrappedChildren = useMemo(
() =>
typeof children === 'function'
? children
: wrapItemChildren(children, null),
[children]
);

return (
<SpectrumActionMenu
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
UNSAFE_className={cl('dh-action-menu', UNSAFE_className)}
disabledKeys={disabledKeys as SpectrumActionMenuProps<T>['disabledKeys']}
>
{wrappedChildren}
</SpectrumActionMenu>
);
}

export default ActionMenu;
27 changes: 27 additions & 0 deletions packages/components/src/spectrum/ListActionGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ActionGroupProps } from './ActionGroup';
import { ItemKey, ItemSelection } from './utils';

export interface ListActionGroupProps<T>
extends Omit<
ActionGroupProps<T>,
'onAction' | 'onChange' | 'onSelectionChange'
> {
/**
* Handler that is called when an item is pressed.
*/
onAction: (actionKey: ItemKey, listItemKey: ItemKey) => void;

/**
* Handler that is called when the selection change.
*/
onChange?: (selection: ItemSelection, listItemKey: ItemKey) => void;
}

/**
* This component doesn't actually render anything. It is a prop container that
* gets passed to `NormalizedListView`. The actual `ActionGroup` elements will
* be created from this component's props on each item in the list view.
*/
export function ListActionGroup<T>(_props: ListActionGroupProps<T>): null {
return null;
}
24 changes: 24 additions & 0 deletions packages/components/src/spectrum/ListActionMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ActionMenuProps } from './ActionMenu';
import { ItemKey } from './utils';

export interface ListActionMenuProps<T>
extends Omit<ActionMenuProps<T>, 'onAction' | 'onOpenChange'> {
/**
* Handler that is called when an item is pressed.
*/
onAction: (actionKey: ItemKey, listItemKey: ItemKey) => void;

/**
* Handler that is called when the the menu is opened or closed.
*/
onOpenChange?: (isOpen: boolean, listItemKey: ItemKey) => void;
}

/**
* This component doesn't actually render anything. It is a prop container that
* gets passed to `NormalizedListView`. The actual `ActionMenu` elements will
* be created from this component's props on each item in the list view.
*/
export function ListActionMenu<T>(_props: ListActionMenuProps<T>): null {
return null;
}
2 changes: 0 additions & 2 deletions packages/components/src/spectrum/buttons.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export {
ActionButton,
type SpectrumActionButtonProps as ActionButtonProps,
ActionGroup,
type SpectrumActionGroupProps as ActionGroupProps,
LogicButton,
type SpectrumLogicButtonProps as LogicButtonProps,
ToggleButton,
Expand Down
2 changes: 0 additions & 2 deletions packages/components/src/spectrum/collections.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export {
ActionBar,
type SpectrumActionBarProps as ActionBarProps,
ActionMenu,
type SpectrumActionMenuProps as ActionMenuProps,
MenuTrigger,
type SpectrumMenuTriggerProps as MenuTriggerProps,
TagGroup,
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/spectrum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export * from './status';
/**
* Custom DH components wrapping React Spectrum components.
*/
export * from './ActionMenu';
export * from './ActionGroup';
export * from './ListActionGroup';
export * from './ListActionMenu';
export * from './listView';
export * from './picker';
export * from './Heading';
Expand Down
37 changes: 10 additions & 27 deletions packages/components/src/spectrum/listView/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { SpectrumListViewProps } from '@adobe/react-spectrum';
import cl from 'classnames';
import { EMPTY_FUNCTION } from '@deephaven/utils';
import {
ItemKey,
ItemSelection,
MultipleItemSelectionProps,
NormalizedItem,
normalizeTooltipOptions,
TooltipOptions,
Expand All @@ -13,38 +12,22 @@ import {
import { ListViewWrapper, ListViewWrapperProps } from './ListViewWrapper';
import { ItemElementOrPrimitive } from '../shared';

export type ListViewProps = {
export type ListViewProps = MultipleItemSelectionProps & {
children: ItemElementOrPrimitive | ItemElementOrPrimitive[];
/** Can be set to true or a TooltipOptions to enable item tooltips */
tooltip?: boolean | TooltipOptions;
selectedKeys?: 'all' | Iterable<ItemKey>;
defaultSelectedKeys?: 'all' | Iterable<ItemKey>;
disabledKeys?: Iterable<ItemKey>;
/**
* Handler that is called when the selection change.
* Note that under the hood, this is just an alias for Spectrum's
* `onSelectionChange`. We are renaming for better consistency with other
* components.
*/
onChange?: (keys: ItemSelection) => void;

/** Handler that is called when the picker is scrolled. */
onScroll?: (event: Event) => void;

/**
* Handler that is called when the selection changes.
* @deprecated Use `onChange` instead
*/
onSelectionChange?: (keys: ItemSelection) => void;
} & Omit<
SpectrumListViewProps<NormalizedItem>,
| 'children'
| 'items'
| 'selectedKeys'
| 'defaultSelectedKeys'
| 'disabledKeys'
| 'onSelectionChange'
>;
SpectrumListViewProps<NormalizedItem>,
| 'children'
| 'items'
| 'selectedKeys'
| 'defaultSelectedKeys'
| 'disabledKeys'
| 'onSelectionChange'
>;

export function ListView({
children,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import cl from 'classnames';
import {
ListActions,
NormalizedItem,
normalizeTooltipOptions,
useRenderNormalizedItem,
Expand All @@ -13,6 +14,7 @@ export interface ListViewNormalizedProps
extends Omit<ListViewProps, 'children'> {
normalizedItems: NormalizedItem[];
showItemIcons: boolean;
actions?: ListActions<unknown>;
}

/**
Expand All @@ -34,6 +36,7 @@ export function ListViewNormalized({
defaultSelectedKeys,
disabledKeys,
showItemIcons,
actions,
UNSAFE_className,
onChange,
onSelectionChange,
Expand All @@ -53,6 +56,7 @@ export function ListViewNormalized({
showItemDescriptions: false,
showItemIcons,
tooltipOptions,
actions,
});

// Spectrum doesn't re-render if only the `renderNormalizedItems` function
Expand Down
21 changes: 18 additions & 3 deletions packages/components/src/spectrum/listView/ListViewWrapper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,37 @@
}

.dh-list-view-wrapper-density-compact {
svg[class*='react-spectrum-ListViewItem-thumbnail'] {
// Ensure icons don't change the item height
svg[class*='spectrum-Icon'] {
height: var(--dh-list-view-item-icon-compact);
width: var(--dh-list-view-item-icon-compact);
}
// Ensure action buttons don't change the item height
button {
height: var(--dh-list-view-item-icon-compact);
}
}

.dh-list-view-wrapper-density-regular {
svg[class*='react-spectrum-ListViewItem-thumbnail'] {
// Ensure icons don't change the item height
svg[class*='spectrum-Icon'] {
height: var(--dh-list-view-item-icon-regular);
width: var(--dh-list-view-item-icon-regular);
}
// Ensure action buttons don't change the item height
button {
height: var(--dh-list-view-item-icon-regular);
}
}

.dh-list-view-wrapper-density-spacious {
svg[class*='react-spectrum-ListViewItem-thumbnail'] {
// Ensure icons don't change the item height
svg[class*='spectrum-Icon'] {
height: var(--dh-list-view-item-icon-spacious);
width: var(--dh-list-view-item-icon-spacious);
}
// Ensure action buttons don't change the item height
button {
height: var(--dh-list-view-item-icon-spacious);
}
}
Loading

0 comments on commit 8e325ec

Please sign in to comment.