Skip to content

Commit 8e325ec

Browse files
authored
feat: ListView actions (#1968)
* 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) ```
1 parent 2246a4a commit 8e325ec

20 files changed

Lines changed: 418 additions & 77 deletions

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
RadioGroup,
1616
RadioItem,
1717
useSpectrumThemeProvider,
18+
ListActionGroup,
1819
} from '@deephaven/components';
19-
import { vsAccount, vsPerson } from '@deephaven/icons';
20+
import { vsAccount, vsEdit, vsPerson, vsTrash } from '@deephaven/icons';
2021
import { LIST_VIEW_ROW_HEIGHTS } from '@deephaven/utils';
2122
import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils';
2223

@@ -90,6 +91,13 @@ export function ListViews(): JSX.Element {
9091
);
9192

9293
const [showIcons, setShowIcons] = useState(true);
94+
const [lastActionKey, setLastActionKey] = useState<ItemKey>('');
95+
const [lastActionItemKey, setLastActionItemKey] = useState<ItemKey>('');
96+
97+
const onAction = useCallback((actionKey: ItemKey, itemKey: ItemKey): void => {
98+
setLastActionKey(actionKey);
99+
setLastActionItemKey(itemKey);
100+
}, []);
93101

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

214-
<LabeledFlexContainer label="Controlled">
222+
<LabeledFlexContainer label="Controlled" gridColumn="span 2">
215223
<ListViewNormalized
216224
aria-label="Controlled"
217225
density={density}
@@ -220,7 +228,29 @@ export function ListViews(): JSX.Element {
220228
selectedKeys={selectedKeys}
221229
showItemIcons={showIcons}
222230
onChange={onChange}
231+
actions={
232+
<ListActionGroup
233+
overflowMode="collapse"
234+
buttonLabelBehavior="collapse"
235+
maxWidth={80}
236+
onAction={onAction}
237+
>
238+
<Item key="Edit">
239+
<Icon>
240+
<FontAwesomeIcon icon={vsEdit} />
241+
</Icon>
242+
<Text>Edit</Text>
243+
</Item>
244+
<Item key="Delete">
245+
<Icon>
246+
<FontAwesomeIcon icon={vsTrash} />
247+
</Icon>
248+
<Text>Delete</Text>
249+
</Item>
250+
</ListActionGroup>
251+
}
223252
/>
253+
{lastActionKey} {lastActionItemKey}
224254
</LabeledFlexContainer>
225255
</Grid>
226256
</div>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useMemo } from 'react';
2+
import {
3+
ActionGroup as SpectrumActionGroup,
4+
SpectrumActionGroupProps,
5+
} from '@adobe/react-spectrum';
6+
import cl from 'classnames';
7+
import { ItemsOrPrimitiveChildren } from './shared';
8+
import { MultipleItemSelectionProps, wrapItemChildren } from './utils';
9+
10+
export type ActionGroupProps<T> = Omit<
11+
SpectrumActionGroupProps<T>,
12+
| 'children'
13+
| 'selectedKeys'
14+
| 'defaultSelectedKeys'
15+
| 'disabledKeys'
16+
| 'onSelectionChange'
17+
> &
18+
MultipleItemSelectionProps & {
19+
children: ItemsOrPrimitiveChildren<T>;
20+
};
21+
22+
/**
23+
* Augmented version of the Spectrum ActionGroup component that supports
24+
* primitive item children.
25+
*/
26+
export function ActionGroup<T>({
27+
defaultSelectedKeys,
28+
disabledKeys,
29+
children,
30+
selectedKeys,
31+
UNSAFE_className,
32+
onChange,
33+
onSelectionChange,
34+
...props
35+
}: ActionGroupProps<T>): JSX.Element {
36+
const wrappedChildren = useMemo(
37+
() =>
38+
typeof children === 'function'
39+
? children
40+
: wrapItemChildren(children, null),
41+
[children]
42+
);
43+
44+
return (
45+
<SpectrumActionGroup
46+
// eslint-disable-next-line react/jsx-props-no-spreading
47+
{...props}
48+
UNSAFE_className={cl('dh-action-group', UNSAFE_className)}
49+
defaultSelectedKeys={
50+
defaultSelectedKeys as SpectrumActionGroupProps<T>['defaultSelectedKeys']
51+
}
52+
disabledKeys={disabledKeys as SpectrumActionGroupProps<T>['disabledKeys']}
53+
selectedKeys={selectedKeys as SpectrumActionGroupProps<T>['selectedKeys']}
54+
onSelectionChange={onChange ?? onSelectionChange}
55+
>
56+
{wrappedChildren}
57+
</SpectrumActionGroup>
58+
);
59+
}
60+
61+
export default ActionGroup;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useMemo } from 'react';
2+
import {
3+
ActionMenu as SpectrumActionMenu,
4+
SpectrumActionMenuProps,
5+
} from '@adobe/react-spectrum';
6+
import cl from 'classnames';
7+
import { ItemsOrPrimitiveChildren } from './shared';
8+
import { ItemKey, wrapItemChildren } from './utils';
9+
10+
export type ActionMenuProps<T> = Omit<
11+
SpectrumActionMenuProps<T>,
12+
'children' | 'disabledKeys'
13+
> & {
14+
disabledKeys?: Iterable<ItemKey>;
15+
children: ItemsOrPrimitiveChildren<T>;
16+
};
17+
18+
/**
19+
* Augmented version of the Spectrum ActionMenu component that supports
20+
* primitive item children.
21+
*/
22+
export function ActionMenu<T>({
23+
disabledKeys,
24+
children,
25+
UNSAFE_className,
26+
...props
27+
}: ActionMenuProps<T>): JSX.Element {
28+
const wrappedChildren = useMemo(
29+
() =>
30+
typeof children === 'function'
31+
? children
32+
: wrapItemChildren(children, null),
33+
[children]
34+
);
35+
36+
return (
37+
<SpectrumActionMenu
38+
// eslint-disable-next-line react/jsx-props-no-spreading
39+
{...props}
40+
UNSAFE_className={cl('dh-action-menu', UNSAFE_className)}
41+
disabledKeys={disabledKeys as SpectrumActionMenuProps<T>['disabledKeys']}
42+
>
43+
{wrappedChildren}
44+
</SpectrumActionMenu>
45+
);
46+
}
47+
48+
export default ActionMenu;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ActionGroupProps } from './ActionGroup';
2+
import { ItemKey, ItemSelection } from './utils';
3+
4+
export interface ListActionGroupProps<T>
5+
extends Omit<
6+
ActionGroupProps<T>,
7+
'onAction' | 'onChange' | 'onSelectionChange'
8+
> {
9+
/**
10+
* Handler that is called when an item is pressed.
11+
*/
12+
onAction: (actionKey: ItemKey, listItemKey: ItemKey) => void;
13+
14+
/**
15+
* Handler that is called when the selection change.
16+
*/
17+
onChange?: (selection: ItemSelection, listItemKey: ItemKey) => void;
18+
}
19+
20+
/**
21+
* This component doesn't actually render anything. It is a prop container that
22+
* gets passed to `NormalizedListView`. The actual `ActionGroup` elements will
23+
* be created from this component's props on each item in the list view.
24+
*/
25+
export function ListActionGroup<T>(_props: ListActionGroupProps<T>): null {
26+
return null;
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ActionMenuProps } from './ActionMenu';
2+
import { ItemKey } from './utils';
3+
4+
export interface ListActionMenuProps<T>
5+
extends Omit<ActionMenuProps<T>, 'onAction' | 'onOpenChange'> {
6+
/**
7+
* Handler that is called when an item is pressed.
8+
*/
9+
onAction: (actionKey: ItemKey, listItemKey: ItemKey) => void;
10+
11+
/**
12+
* Handler that is called when the the menu is opened or closed.
13+
*/
14+
onOpenChange?: (isOpen: boolean, listItemKey: ItemKey) => void;
15+
}
16+
17+
/**
18+
* This component doesn't actually render anything. It is a prop container that
19+
* gets passed to `NormalizedListView`. The actual `ActionMenu` elements will
20+
* be created from this component's props on each item in the list view.
21+
*/
22+
export function ListActionMenu<T>(_props: ListActionMenuProps<T>): null {
23+
return null;
24+
}

packages/components/src/spectrum/buttons.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
export {
22
ActionButton,
33
type SpectrumActionButtonProps as ActionButtonProps,
4-
ActionGroup,
5-
type SpectrumActionGroupProps as ActionGroupProps,
64
LogicButton,
75
type SpectrumLogicButtonProps as LogicButtonProps,
86
ToggleButton,

packages/components/src/spectrum/collections.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
export {
22
ActionBar,
33
type SpectrumActionBarProps as ActionBarProps,
4-
ActionMenu,
5-
type SpectrumActionMenuProps as ActionMenuProps,
64
MenuTrigger,
75
type SpectrumMenuTriggerProps as MenuTriggerProps,
86
TagGroup,

packages/components/src/spectrum/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export * from './status';
1717
/**
1818
* Custom DH components wrapping React Spectrum components.
1919
*/
20+
export * from './ActionMenu';
21+
export * from './ActionGroup';
22+
export * from './ListActionGroup';
23+
export * from './ListActionMenu';
2024
export * from './listView';
2125
export * from './picker';
2226
export * from './Heading';

packages/components/src/spectrum/listView/ListView.tsx

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { SpectrumListViewProps } from '@adobe/react-spectrum';
33
import cl from 'classnames';
44
import { EMPTY_FUNCTION } from '@deephaven/utils';
55
import {
6-
ItemKey,
7-
ItemSelection,
6+
MultipleItemSelectionProps,
87
NormalizedItem,
98
normalizeTooltipOptions,
109
TooltipOptions,
@@ -13,38 +12,22 @@ import {
1312
import { ListViewWrapper, ListViewWrapperProps } from './ListViewWrapper';
1413
import { ItemElementOrPrimitive } from '../shared';
1514

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

3120
/** Handler that is called when the picker is scrolled. */
3221
onScroll?: (event: Event) => void;
33-
34-
/**
35-
* Handler that is called when the selection changes.
36-
* @deprecated Use `onChange` instead
37-
*/
38-
onSelectionChange?: (keys: ItemSelection) => void;
3922
} & Omit<
40-
SpectrumListViewProps<NormalizedItem>,
41-
| 'children'
42-
| 'items'
43-
| 'selectedKeys'
44-
| 'defaultSelectedKeys'
45-
| 'disabledKeys'
46-
| 'onSelectionChange'
47-
>;
23+
SpectrumListViewProps<NormalizedItem>,
24+
| 'children'
25+
| 'items'
26+
| 'selectedKeys'
27+
| 'defaultSelectedKeys'
28+
| 'disabledKeys'
29+
| 'onSelectionChange'
30+
>;
4831

4932
export function ListView({
5033
children,

packages/components/src/spectrum/listView/ListViewNormalized.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMemo } from 'react';
22
import cl from 'classnames';
33
import {
4+
ListActions,
45
NormalizedItem,
56
normalizeTooltipOptions,
67
useRenderNormalizedItem,
@@ -13,6 +14,7 @@ export interface ListViewNormalizedProps
1314
extends Omit<ListViewProps, 'children'> {
1415
normalizedItems: NormalizedItem[];
1516
showItemIcons: boolean;
17+
actions?: ListActions<unknown>;
1618
}
1719

1820
/**
@@ -34,6 +36,7 @@ export function ListViewNormalized({
3436
defaultSelectedKeys,
3537
disabledKeys,
3638
showItemIcons,
39+
actions,
3740
UNSAFE_className,
3841
onChange,
3942
onSelectionChange,
@@ -53,6 +56,7 @@ export function ListViewNormalized({
5356
showItemDescriptions: false,
5457
showItemIcons,
5558
tooltipOptions,
59+
actions,
5660
});
5761

5862
// Spectrum doesn't re-render if only the `renderNormalizedItems` function

0 commit comments

Comments
 (0)